Spaces:
Sleeping
Sleeping
| import { getLocalMovies } from '../../../lib/localDb.js'; | |
| // Cấu hình timeout cho fetch | |
| async function fetchWithTimeout(resource, options = {}) { | |
| const { timeout = 8000 } = options; | |
| const controller = new AbortController(); | |
| const id = setTimeout(() => controller.abort(), timeout); | |
| try { | |
| const response = await fetch(resource, { | |
| ...options, | |
| signal: controller.signal | |
| }); | |
| return response; | |
| } finally { | |
| clearTimeout(id); | |
| } | |
| } | |
| // Trích xuất slug từ URL hoặc chuỗi nhập vào | |
| function extractSlug(input) { | |
| if (!input) return null; | |
| const trimmed = input.trim(); | |
| if (!trimmed) return null; | |
| try { | |
| if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { | |
| const url = new URL(trimmed); | |
| const pathname = url.pathname; | |
| const parts = pathname.split('/').filter(Boolean); | |
| if (parts.length > 0) { | |
| return parts[parts.length - 1]; | |
| } | |
| } | |
| } catch (e) {} | |
| const parts = trimmed.split('/').filter(Boolean); | |
| if (parts.length > 0) { | |
| return parts[parts.length - 1]; | |
| } | |
| return trimmed; | |
| } | |
| // Hàm gộp tập phim để nhẩm tính số tập mới | |
| function checkEpisodesDiff(existingEpisodes, crawledEpisodes) { | |
| let addedCount = 0; | |
| const existingList = existingEpisodes || []; | |
| for (const crawledServer of crawledEpisodes) { | |
| let existingServer = existingList.find(s => s.server_name === crawledServer.server_name); | |
| if (!existingServer) { | |
| addedCount += crawledServer.server_data?.length || 0; | |
| continue; | |
| } | |
| const existingNames = new Set((existingServer.server_data || []).map(e => e.name)); | |
| for (const crawledEp of crawledServer.server_data || []) { | |
| if (!existingNames.has(crawledEp.name)) { | |
| addedCount++; | |
| } | |
| } | |
| } | |
| return addedCount; | |
| } | |
| // Dọn dẹp máy chủ | |
| function cleanAndFilterServers(episodes) { | |
| if (!Array.isArray(episodes)) return []; | |
| const tempMap = new Map(); | |
| for (const srv of episodes) { | |
| if (!srv || !srv.server_name) continue; | |
| let rawName = srv.server_name.trim(); | |
| let cleanName = ''; | |
| if (/vietsub/i.test(rawName)) { | |
| cleanName = 'Vietsub'; | |
| } else if (/thuyết minh|thuyet minh/i.test(rawName)) { | |
| cleanName = 'Thuyết Minh'; | |
| } else if (/lồng tiếng|long tieng/i.test(rawName)) { | |
| cleanName = 'Lồng Tiếng'; | |
| } else { | |
| cleanName = rawName.replace(/#\s*/g, '').replace(/\([^)]*\)/g, '').trim(); | |
| if (!cleanName) cleanName = 'Vietsub'; | |
| } | |
| if (cleanName !== 'Vietsub' && cleanName !== 'Thuyết Minh' && cleanName !== 'Lồng Tiếng') { | |
| continue; | |
| } | |
| if (!tempMap.has(cleanName)) { | |
| srv.server_name = cleanName; | |
| tempMap.set(cleanName, srv); | |
| } | |
| } | |
| const result = []; | |
| if (tempMap.has('Vietsub')) result.push(tempMap.get('Vietsub')); | |
| if (tempMap.has('Thuyết Minh')) result.push(tempMap.get('Thuyết Minh')); | |
| if (result.length < 2 && tempMap.has('Lồng Tiếng')) { | |
| result.push(tempMap.get('Lồng Tiếng')); | |
| } | |
| if (result.length === 0 && episodes.length > 0) { | |
| const fallbackSrv = episodes[0]; | |
| fallbackSrv.server_name = fallbackSrv.server_name.replace(/#\s*/g, '').replace(/\([^)]*\)/g, '').trim() || 'Vietsub'; | |
| result.push(fallbackSrv); | |
| } | |
| return result; | |
| } | |
| export async function POST({ request }) { | |
| // Check quyền admin từ cookie auth_token | |
| const cookies = request.headers.get('cookie') || ''; | |
| const hasToken = cookies.includes('auth_token='); | |
| if (!hasToken) { | |
| return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); | |
| } | |
| try { | |
| const { type, urls, pageStart, pageEnd, countrySlug, genreSlug } = await request.json(); | |
| const localMovies = await getLocalMovies(); | |
| const slugsToCrawl = []; | |
| if (type === 'url') { | |
| const urlList = Array.isArray(urls) ? urls : [urls]; | |
| for (const rawUrl of urlList) { | |
| const slug = extractSlug(rawUrl); | |
| if (slug) { | |
| try { | |
| const res = await fetchWithTimeout(`https://phimapi.com/phim/${slug}`); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| if (data.status && data.movie) { | |
| const crawledMovie = data.movie; | |
| const crawledEpisodes = cleanAndFilterServers(data.episodes || []); | |
| let crawledEpCount = 0; | |
| if (crawledEpisodes.length > 0) { | |
| crawledEpCount = Math.max(...crawledEpisodes.map(s => s.server_data?.length || 0)); | |
| } | |
| const existing = localMovies[slug]; | |
| let expectedNew = crawledEpCount; | |
| let exists = false; | |
| let skip = false; | |
| let skipReason = ''; | |
| if (existing) { | |
| exists = true; | |
| const addedCount = checkEpisodesDiff(existing.episodes, crawledEpisodes); | |
| expectedNew = addedCount; | |
| if (addedCount === 0) { | |
| skip = true; | |
| skipReason = 'Đã đủ tập'; | |
| } | |
| } | |
| slugsToCrawl.push({ | |
| raw: crawledMovie.name, | |
| slug, | |
| totalEpisodes: crawledEpCount, | |
| expectedNew, | |
| exists, | |
| skip, | |
| skipReason | |
| }); | |
| } | |
| } catch (e) { | |
| slugsToCrawl.push({ | |
| raw: slug, | |
| slug, | |
| totalEpisodes: 0, | |
| expectedNew: 0, | |
| exists: false, | |
| skip: false, | |
| skipReason: `Lỗi quét: ${e.message}` | |
| }); | |
| } | |
| } | |
| } | |
| } else if (type === 'country') { | |
| const start = parseInt(pageStart || '1'); | |
| const end = parseInt(pageEnd || '1'); | |
| for (let p = start; p <= end; p++) { | |
| try { | |
| const res = await fetchWithTimeout(`https://phimapi.com/v1/api/quoc-gia/${countrySlug}?page=${p}`); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const resData = await res.json(); | |
| const items = resData.data?.items || []; | |
| for (const item of items) { | |
| if (item.slug && !slugsToCrawl.some(s => s.slug === item.slug)) { | |
| const existing = localMovies[item.slug]; | |
| let skip = false; | |
| let skipReason = ''; | |
| let expectedNew = 1; | |
| let exists = false; | |
| if (existing) { | |
| exists = true; | |
| const localStatus = (existing.movie?.episode_current || '').toLowerCase(); | |
| const remoteStatus = (item.episode_current || '').toLowerCase(); | |
| const isStatusEqual = localStatus === remoteStatus && remoteStatus !== ''; | |
| const isBothCompleted = (localStatus.includes('hoàn tất') || localStatus.includes('full') || localStatus.includes('complete') || localStatus.includes('12/12')) && | |
| (remoteStatus.includes('hoàn tất') || remoteStatus.includes('full') || remoteStatus.includes('complete') || remoteStatus.includes('12/12')); | |
| if (isStatusEqual || isBothCompleted) { | |
| skip = true; | |
| skipReason = remoteStatus.includes('hoàn tất') ? 'Phim đã hoàn tất' : 'Số tập trùng khớp'; | |
| expectedNew = 0; | |
| } | |
| } | |
| slugsToCrawl.push({ | |
| raw: item.name, | |
| slug: item.slug, | |
| totalEpisodes: item.episode_total || (item.episode_current ? item.episode_current : '?'), | |
| expectedNew, | |
| exists, | |
| skip, | |
| skipReason | |
| }); | |
| } | |
| } | |
| } catch (e) { | |
| console.error(`Error scanning page ${p}:`, e); | |
| } | |
| } | |
| } else if (type === 'genre') { | |
| const start = parseInt(pageStart || '1'); | |
| const end = parseInt(pageEnd || '1'); | |
| for (let p = start; p <= end; p++) { | |
| try { | |
| let apiUrl = `https://phimapi.com/v1/api/the-loai/${genreSlug}?page=${p}`; | |
| const listSlugs = ['hoat-hinh', 'phim-chieu-rap', 'phim-le', 'phim-bo', 'tv-shows']; | |
| if (listSlugs.includes(genreSlug)) { | |
| apiUrl = `https://phimapi.com/v1/api/danh-sach/${genreSlug}?page=${p}`; | |
| } | |
| const res = await fetchWithTimeout(apiUrl); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const resData = await res.json(); | |
| const items = resData.data?.items || []; | |
| for (const item of items) { | |
| if (item.slug && !slugsToCrawl.some(s => s.slug === item.slug)) { | |
| const existing = localMovies[item.slug]; | |
| let skip = false; | |
| let skipReason = ''; | |
| let expectedNew = 1; | |
| let exists = false; | |
| if (existing) { | |
| exists = true; | |
| const localStatus = (existing.movie?.episode_current || '').toLowerCase(); | |
| const remoteStatus = (item.episode_current || '').toLowerCase(); | |
| const isStatusEqual = localStatus === remoteStatus && remoteStatus !== ''; | |
| const isBothCompleted = (localStatus.includes('hoàn tất') || localStatus.includes('full') || localStatus.includes('complete') || localStatus.includes('12/12')) && | |
| (remoteStatus.includes('hoàn tất') || remoteStatus.includes('full') || remoteStatus.includes('complete') || remoteStatus.includes('12/12')); | |
| if (isStatusEqual || isBothCompleted) { | |
| skip = true; | |
| skipReason = remoteStatus.includes('hoàn tất') ? 'Phim đã hoàn tất' : 'Số tập trùng khớp'; | |
| expectedNew = 0; | |
| } | |
| } | |
| slugsToCrawl.push({ | |
| raw: item.name, | |
| slug: item.slug, | |
| totalEpisodes: item.episode_total || (item.episode_current ? item.episode_current : '?'), | |
| expectedNew, | |
| exists, | |
| skip, | |
| skipReason | |
| }); | |
| } | |
| } | |
| } catch (e) { | |
| console.error(`Error scanning genre page ${p}:`, e); | |
| } | |
| } | |
| } else if (type === 'batch') { | |
| const start = parseInt(pageStart || '1'); | |
| const end = parseInt(pageEnd || '1'); | |
| for (let p = start; p <= end; p++) { | |
| try { | |
| const res = await fetchWithTimeout(`https://phimapi.com/danh-sach/phim-moi-cap-nhat?page=${p}`); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| const items = data.items || []; | |
| for (const item of items) { | |
| if (item.slug && !slugsToCrawl.some(s => s.slug === item.slug)) { | |
| const existing = localMovies[item.slug]; | |
| let skip = false; | |
| let skipReason = ''; | |
| let expectedNew = 1; | |
| let exists = false; | |
| if (existing) { | |
| exists = true; | |
| // Nhẩm tính thử | |
| } | |
| slugsToCrawl.push({ | |
| raw: item.name, | |
| slug: item.slug, | |
| totalEpisodes: item.episode_total || '?', | |
| expectedNew, | |
| exists, | |
| skip, | |
| skipReason | |
| }); | |
| } | |
| } | |
| } catch (e) { | |
| console.error(`Error scanning batch page ${p}:`, e); | |
| } | |
| } | |
| } else if (type === 'all') { | |
| // Vì scan all có thể mất thời gian nếu lấy hết nghìn slugs, ta chỉ quét trang 1-2 để đảm bảo không timeout | |
| // Hoặc trả về nhanh 1 danh sách | |
| const res = await fetchWithTimeout(`https://phimapi.com/danh-sach/phim-moi-cap-nhat?page=1`); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| const items = data.items || []; | |
| for (const item of items) { | |
| slugsToCrawl.push({ | |
| raw: item.name, | |
| slug: item.slug, | |
| totalEpisodes: '?', | |
| expectedNew: 1, | |
| exists: !!localMovies[item.slug], | |
| skip: false, | |
| skipReason: '' | |
| }); | |
| } | |
| } | |
| } | |
| return new Response(JSON.stringify({ success: true, count: slugsToCrawl.length, slugs: slugsToCrawl }), { | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } catch (err) { | |
| return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); | |
| } | |
| } | |