dptxa-proxy / src /pages /api /admin /scan.js
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
12.9 kB
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' } });
}
}