Spaces:
Sleeping
Sleeping
| import { getLocalMovies, saveLocalMovie, syncFromMovies } 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); | |
| } | |
| } | |
| // Định dạng tên tập phim | |
| function formatEpisodeName(name) { | |
| if (!name) return name; | |
| let str = name.trim(); | |
| if (/^\d+$/.test(str)) { | |
| const num = parseInt(str, 10); | |
| return `Tập ${num.toString().padStart(2, '0')}`; | |
| } | |
| const match = str.match(/^(tập|tap)\s+(\d+)$/i); | |
| if (match) { | |
| const num = parseInt(match[2], 10); | |
| return `Tập ${num.toString().padStart(2, '0')}`; | |
| } | |
| return str; | |
| } | |
| // Lọc server phát phim | |
| 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; | |
| } | |
| // Gộp tập phim | |
| function mergeEpisodes(existingServerList, crawledServerList) { | |
| let addedCount = 0; | |
| const existingList = JSON.parse(JSON.stringify(existingServerList || [])); | |
| for (const crawledServer of crawledServerList) { | |
| let existingServer = existingList.find(s => s.server_name === crawledServer.server_name); | |
| if (!existingServer) { | |
| crawledServer.server_data.forEach(ep => { | |
| ep.name = formatEpisodeName(ep.name); | |
| }); | |
| existingList.push(crawledServer); | |
| addedCount += crawledServer.server_data.length; | |
| continue; | |
| } | |
| existingServer.server_data.forEach(ep => { | |
| ep.name = formatEpisodeName(ep.name); | |
| }); | |
| const existingMap = new Map(existingServer.server_data.map((e, idx) => [e.name, { ep: e, idx }])); | |
| for (const crawledEp of crawledServer.server_data) { | |
| crawledEp.name = formatEpisodeName(crawledEp.name); | |
| const matched = existingMap.get(crawledEp.name); | |
| if (matched) { | |
| existingServer.server_data[matched.idx] = { | |
| ...crawledEp, | |
| introStart: matched.ep.introStart, | |
| introEnd: matched.ep.introEnd, | |
| outroStart: matched.ep.outroStart, | |
| outroEnd: matched.ep.outroEnd | |
| }; | |
| } else { | |
| existingServer.server_data.push(crawledEp); | |
| addedCount++; | |
| } | |
| } | |
| } | |
| return { merged: existingList, addedCount }; | |
| } | |
| 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 { slug, selectedEpisodes } = await request.json(); | |
| if (!slug) { | |
| return new Response(JSON.stringify({ error: 'Missing slug parameter' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); | |
| } | |
| const localMovies = await getLocalMovies(); | |
| const res = await fetchWithTimeout(`https://phimapi.com/phim/${slug}`); | |
| if (!res.ok) { | |
| throw new Error(`Phim không tồn tại trên KKPhim (HTTP ${res.status})`); | |
| } | |
| const data = await res.json(); | |
| if (!data.status || !data.movie) { | |
| throw new Error('Dữ liệu API KKPhim bị rỗng hoặc không đúng định dạng'); | |
| } | |
| const crawledMovie = data.movie; | |
| const crawledEpisodes = cleanAndFilterServers(data.episodes || []); | |
| // Lọc tập phim nếu admin chọn cụ thể | |
| if (Array.isArray(selectedEpisodes) && selectedEpisodes.length > 0) { | |
| crawledEpisodes.forEach(server => { | |
| if (server.server_data) { | |
| server.server_data = server.server_data.filter(ep => { | |
| const formattedName = formatEpisodeName(ep.name); | |
| return selectedEpisodes.includes(formattedName); | |
| }); | |
| } | |
| }); | |
| } | |
| const existing = localMovies[slug]; | |
| let status = 'success'; | |
| let addedCount = 0; | |
| let message = ''; | |
| if (existing) { | |
| // So sánh gộp tập phim | |
| const { merged, addedCount: added } = mergeEpisodes(existing.episodes, crawledEpisodes); | |
| addedCount = added; | |
| const hasSelectedEpisodes = Array.isArray(selectedEpisodes) && selectedEpisodes.length > 0; | |
| if (addedCount > 0 || hasSelectedEpisodes) { | |
| existing.movie = { | |
| ...existing.movie, | |
| ...crawledMovie, | |
| episode_current: crawledMovie.episode_current || existing.movie.episode_current, | |
| episode_total: crawledMovie.episode_total || existing.movie.episode_total || null, | |
| modified: { time: new Date().toISOString() } | |
| }; | |
| existing.episodes = merged; | |
| await saveLocalMovie(slug, existing); | |
| status = 'updated'; | |
| message = hasSelectedEpisodes && addedCount === 0 | |
| ? `Thành công (Cập nhật liên kết ${selectedEpisodes.length} tập)` | |
| : `Thành công (Cập nhật +${addedCount} tập mới)`; | |
| } else { | |
| status = 'skipped'; | |
| message = 'Bỏ qua (Đã đầy đủ tập, không có tập mới)'; | |
| } | |
| } else { | |
| // Phim mới hoàn toàn | |
| crawledEpisodes.forEach(srv => { | |
| if (srv.server_data) { | |
| srv.server_data.forEach(ep => { | |
| ep.name = formatEpisodeName(ep.name); | |
| }); | |
| } | |
| }); | |
| const newMovie = { | |
| movie: crawledMovie, | |
| episodes: crawledEpisodes | |
| }; | |
| await saveLocalMovie(slug, newMovie); | |
| status = 'success'; | |
| message = 'Thành công (Phim mới)'; | |
| } | |
| // Tự động đồng bộ các danh mục Thể loại, Quốc gia, Diễn viên và Server ngay lập tức! | |
| await syncFromMovies('genres'); | |
| await syncFromMovies('countries'); | |
| await syncFromMovies('actors'); | |
| await syncFromMovies('servers'); | |
| return new Response(JSON.stringify({ | |
| success: true, | |
| status, | |
| slug, | |
| name: crawledMovie.name, | |
| added: addedCount, | |
| message | |
| }), { headers: { 'Content-Type': 'application/json' } }); | |
| } catch (err) { | |
| return new Response(JSON.stringify({ success: false, error: err.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); | |
| } | |
| } | |