diff --git a/src/app/api/cron/route.ts b/src/app/api/cron/route.ts index 9fa6a02..e3922f6 100644 --- a/src/app/api/cron/route.ts +++ b/src/app/api/cron/route.ts @@ -6,6 +6,7 @@ import { getConfig, refineConfig } from '@/lib/config'; import { db, getStorage } from '@/lib/db'; import { fetchVideoDetail } from '@/lib/fetchVideoDetail'; import { refreshLiveChannels } from '@/lib/live'; +import { startOpenListRefresh } from '@/lib/openlist-refresh'; import { SearchResult } from '@/lib/types'; export const runtime = 'nodejs'; @@ -330,21 +331,9 @@ async function refreshOpenList() { console.log(`开始 OpenList 定时扫描(间隔: ${scanInterval} 分钟)`); - // 调用扫描接口(立即扫描模式,不清空 metainfo) - const response = await fetch(`${process.env.SITE_BASE || 'http://localhost:3000'}/api/openlist/refresh`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ clearMetaInfo: false }), - }); - - if (!response.ok) { - throw new Error(`扫描请求失败: ${response.status}`); - } - - const result = await response.json(); - console.log('OpenList 定时扫描已启动,任务ID:', result.taskId); + // 直接调用扫描函数(立即扫描模式,不清空 metainfo) + const { taskId } = await startOpenListRefresh(false); + console.log('OpenList 定时扫描已启动,任务ID:', taskId); } catch (err) { console.error('OpenList 定时扫描失败:', err); } diff --git a/src/app/api/openlist/refresh/route.ts b/src/app/api/openlist/refresh/route.ts index 6e01023..3e771e9 100644 --- a/src/app/api/openlist/refresh/route.ts +++ b/src/app/api/openlist/refresh/route.ts @@ -3,25 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; -import { getConfig } from '@/lib/config'; -import { generateFolderKey } from '@/lib/crypto'; -import { db } from '@/lib/db'; -import { OpenListClient } from '@/lib/openlist.client'; -import { - getCachedMetaInfo, - invalidateMetaInfoCache, - MetaInfo, - setCachedMetaInfo, -} from '@/lib/openlist-cache'; -import { - cleanupOldTasks, - completeScanTask, - createScanTask, - failScanTask, - updateScanTaskProgress, -} from '@/lib/scan-task'; -import { parseSeasonFromTitle } from '@/lib/season-parser'; -import { searchTMDB, getTVSeasonDetails } from '@/lib/tmdb.search'; +import { startOpenListRefresh } from '@/lib/openlist-refresh'; export const runtime = 'nodejs'; @@ -39,55 +21,10 @@ export async function POST(request: NextRequest) { // 获取请求参数 const body = await request.json().catch(() => ({})); - const clearMetaInfo = body.clearMetaInfo === true; // 是否清空 metainfo + const clearMetaInfo = body.clearMetaInfo === true; - // 获取配置 - const config = await getConfig(); - const openListConfig = config.OpenListConfig; - - if ( - !openListConfig || - !openListConfig.Enabled || - !openListConfig.URL || - !openListConfig.Username || - !openListConfig.Password - ) { - return NextResponse.json( - { error: 'OpenList 未配置或未启用' }, - { status: 400 } - ); - } - - const tmdbApiKey = config.SiteConfig.TMDBApiKey; - const tmdbProxy = config.SiteConfig.TMDBProxy; - - if (!tmdbApiKey) { - return NextResponse.json( - { error: 'TMDB API Key 未配置' }, - { status: 400 } - ); - } - - // 清理旧任务 - cleanupOldTasks(); - - // 创建后台任务 - const taskId = createScanTask(); - - // 启动后台扫描 - performScan( - taskId, - openListConfig.URL, - openListConfig.RootPath || '/', - tmdbApiKey, - tmdbProxy, - openListConfig.Username, - openListConfig.Password, - clearMetaInfo - ).catch((error) => { - console.error('[OpenList Refresh] 后台扫描失败:', error); - failScanTask(taskId, (error as Error).message); - }); + // 启动扫描任务 + const { taskId } = await startOpenListRefresh(clearMetaInfo); return NextResponse.json({ success: true, @@ -102,260 +39,3 @@ export async function POST(request: NextRequest) { ); } } - -/** - * 执行扫描任务 - */ -async function performScan( - taskId: string, - url: string, - rootPath: string, - tmdbApiKey: string, - tmdbProxy?: string, - username?: string, - password?: string, - clearMetaInfo?: boolean -): Promise { - const client = new OpenListClient(url, username!, password!); - - // 立即更新进度,确保任务可被查询 - updateScanTaskProgress(taskId, 0, 0); - - try { - // 1. 根据参数决定是否读取现有数据 - let metaInfo: MetaInfo; - - if (clearMetaInfo) { - // 重新扫描:清空现有数据 - metaInfo = { - folders: {}, - last_refresh: Date.now(), - }; - } else { - // 立即扫描:保留现有数据,从数据库读取 - try { - const metainfoContent = await db.getGlobalValue('video.metainfo'); - if (metainfoContent) { - metaInfo = JSON.parse(metainfoContent); - } else { - metaInfo = { - folders: {}, - last_refresh: Date.now(), - }; - } - } catch (error) { - console.error('[OpenList Refresh] 读取现有 metainfo 失败:', error); - metaInfo = { - folders: {}, - last_refresh: Date.now(), - }; - } - } - - // 清除缓存,确保后续读取的是新数据 - invalidateMetaInfoCache(rootPath); - - // 2. 列出根目录下的所有文件夹(强制刷新 OpenList 缓存) - // 循环获取所有页的数据 - const folders: any[] = []; - let currentPage = 1; - const pageSize = 100; - let total = 0; - - while (true) { - const listResponse = await client.listDirectory(rootPath, currentPage, pageSize, true); - - if (listResponse.code !== 200) { - throw new Error('OpenList 列表获取失败'); - } - - total = listResponse.data.total; - const pageFolders = listResponse.data.content.filter((item) => item.is_dir); - folders.push(...pageFolders); - - // 如果已经获取了所有数据,退出循环 - if (folders.length >= total) { - break; - } - - currentPage++; - } - - // 更新任务进度 - updateScanTaskProgress(taskId, 0, folders.length); - - // 3. 遍历文件夹,搜索 TMDB - let newCount = 0; - let existingCount = 0; - let errorCount = 0; - - // 收集已存在的 key,用于冲突检测 - const existingKeys = new Set(Object.keys(metaInfo.folders)); - - // 创建文件夹名到 key 的映射(用于查找已存在的文件夹) - const folderNameToKey = new Map(); - for (const [key, info] of Object.entries(metaInfo.folders)) { - folderNameToKey.set(info.folderName, key); - } - - for (let i = 0; i < folders.length; i++) { - const folder = folders[i]; - - // 更新进度 - updateScanTaskProgress(taskId, i + 1, folders.length, folder.name); - - // 如果是立即扫描(不清空 metainfo),且文件夹已存在,跳过 - if (!clearMetaInfo && folderNameToKey.has(folder.name)) { - existingCount++; - continue; - } - - // 生成文件夹的 key - const folderKey = generateFolderKey(folder.name, existingKeys); - existingKeys.add(folderKey); - - try { - // 解析文件夹名称,提取季度信息和年份 - const seasonInfo = parseSeasonFromTitle(folder.name); - const searchQuery = seasonInfo.cleanTitle || folder.name; - - console.log(`[OpenList Refresh] 处理文件夹: ${folder.name}`); - console.log(`[OpenList Refresh] 清理后标题: ${searchQuery}, 季度: ${seasonInfo.seasonNumber}, 年份: ${seasonInfo.year}`); - - // 搜索 TMDB(使用清理后的标题和年份) - const searchResult = await searchTMDB( - tmdbApiKey, - searchQuery, - tmdbProxy, - seasonInfo.year || undefined - ); - - if (searchResult.code === 200 && searchResult.result) { - const result = searchResult.result; - - // 基础信息 - const folderInfo: any = { - folderName: folder.name, // 保存原始文件夹名称 - tmdb_id: result.id, - title: result.title || result.name || folder.name, - poster_path: result.poster_path, - release_date: result.release_date || result.first_air_date || '', - overview: result.overview, - vote_average: result.vote_average, - media_type: result.media_type, - last_updated: Date.now(), - failed: false, - }; - - // 如果是电视剧且识别到季度编号,获取该季度的详细信息 - if (result.media_type === 'tv' && seasonInfo.seasonNumber) { - try { - const seasonDetails = await getTVSeasonDetails( - tmdbApiKey, - result.id, - seasonInfo.seasonNumber, - tmdbProxy - ); - - if (seasonDetails.code === 200 && seasonDetails.season) { - folderInfo.season_number = seasonDetails.season.season_number; - folderInfo.season_name = seasonDetails.season.name; - - // 如果是第二季及以后,替换标题和ID - if (seasonDetails.season.season_number > 1) { - folderInfo.title = `${folderInfo.title} ${seasonDetails.season.name}`; - } - - // 使用季度的海报(如果有) - if (seasonDetails.season.poster_path) { - folderInfo.poster_path = seasonDetails.season.poster_path; - } - // 使用季度的简介(如果有) - if (seasonDetails.season.overview) { - folderInfo.overview = seasonDetails.season.overview; - } - // 使用季度的首播日期(如果有) - if (seasonDetails.season.air_date) { - folderInfo.release_date = seasonDetails.season.air_date; - } - } else { - console.warn(`[OpenList Refresh] 获取季度 ${seasonInfo.seasonNumber} 详情失败`); - // 即使获取季度详情失败,也保存季度编号 - folderInfo.season_number = seasonInfo.seasonNumber; - } - } catch (error) { - console.error(`[OpenList Refresh] 获取季度详情异常:`, error); - // 即使出错,也保存季度编号 - folderInfo.season_number = seasonInfo.seasonNumber; - } - } - - metaInfo.folders[folderKey] = folderInfo; - newCount++; - } else { - // 记录失败的文件夹 - metaInfo.folders[folderKey] = { - folderName: folder.name, // 保存原始文件夹名称 - tmdb_id: 0, - title: folder.name, - poster_path: null, - release_date: '', - overview: '', - vote_average: 0, - media_type: 'movie', - last_updated: Date.now(), - failed: true, - }; - errorCount++; - } - - // 避免请求过快 - await new Promise((resolve) => setTimeout(resolve, 300)); - } catch (error) { - console.error(`[OpenList Refresh] 处理文件夹失败: ${folder.name}`, error); - // 记录失败的文件夹 - metaInfo.folders[folderKey] = { - folderName: folder.name, // 保存原始文件夹名称 - tmdb_id: 0, - title: folder.name, - poster_path: null, - release_date: '', - overview: '', - vote_average: 0, - media_type: 'movie', - last_updated: Date.now(), - failed: true, - }; - errorCount++; - } - } - - // 4. 保存 metainfo 到数据库 - metaInfo.last_refresh = Date.now(); - - const metainfoContent = JSON.stringify(metaInfo); - await db.setGlobalValue('video.metainfo', metainfoContent); - - // 5. 更新缓存 - invalidateMetaInfoCache(rootPath); - setCachedMetaInfo(rootPath, metaInfo); - - // 6. 更新配置 - const config = await getConfig(); - config.OpenListConfig!.LastRefreshTime = Date.now(); - config.OpenListConfig!.ResourceCount = Object.keys(metaInfo.folders).length; - await db.saveAdminConfig(config); - - // 完成任务 - completeScanTask(taskId, { - total: folders.length, - new: newCount, - existing: existingCount, - errors: errorCount, - }); - } catch (error) { - console.error('[OpenList Refresh] 扫描失败:', error); - failScanTask(taskId, (error as Error).message); - throw error; - } -} diff --git a/src/lib/openlist-refresh.ts b/src/lib/openlist-refresh.ts new file mode 100644 index 0000000..5fb8540 --- /dev/null +++ b/src/lib/openlist-refresh.ts @@ -0,0 +1,290 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ + +import { getConfig } from '@/lib/config'; +import { generateFolderKey } from '@/lib/crypto'; +import { db } from '@/lib/db'; +import { OpenListClient } from '@/lib/openlist.client'; +import { + getCachedMetaInfo, + invalidateMetaInfoCache, + MetaInfo, + setCachedMetaInfo, +} from '@/lib/openlist-cache'; +import { + cleanupOldTasks, + completeScanTask, + createScanTask, + failScanTask, + updateScanTaskProgress, +} from '@/lib/scan-task'; +import { parseSeasonFromTitle } from '@/lib/season-parser'; +import { searchTMDB, getTVSeasonDetails } from '@/lib/tmdb.search'; + +/** + * 启动 OpenList 刷新任务 + */ +export async function startOpenListRefresh(clearMetaInfo: boolean = false): Promise<{ taskId: string }> { + const config = await getConfig(); + const openListConfig = config.OpenListConfig; + + if ( + !openListConfig || + !openListConfig.Enabled || + !openListConfig.URL || + !openListConfig.Username || + !openListConfig.Password + ) { + throw new Error('OpenList 未配置或未启用'); + } + + const tmdbApiKey = config.SiteConfig.TMDBApiKey; + const tmdbProxy = config.SiteConfig.TMDBProxy; + + if (!tmdbApiKey) { + throw new Error('TMDB API Key 未配置'); + } + + cleanupOldTasks(); + const taskId = createScanTask(); + + performScan( + taskId, + openListConfig.URL, + openListConfig.RootPath || '/', + tmdbApiKey, + tmdbProxy, + openListConfig.Username, + openListConfig.Password, + clearMetaInfo + ).catch((error) => { + console.error('[OpenList Refresh] 后台扫描失败:', error); + failScanTask(taskId, (error as Error).message); + }); + + return { taskId }; +} + +/** + * 执行扫描任务 + */ +async function performScan( + taskId: string, + url: string, + rootPath: string, + tmdbApiKey: string, + tmdbProxy?: string, + username?: string, + password?: string, + clearMetaInfo?: boolean +): Promise { + const client = new OpenListClient(url, username!, password!); + + updateScanTaskProgress(taskId, 0, 0); + + try { + let metaInfo: MetaInfo; + + if (clearMetaInfo) { + metaInfo = { + folders: {}, + last_refresh: Date.now(), + }; + } else { + try { + const metainfoContent = await db.getGlobalValue('video.metainfo'); + if (metainfoContent) { + metaInfo = JSON.parse(metainfoContent); + } else { + metaInfo = { + folders: {}, + last_refresh: Date.now(), + }; + } + } catch (error) { + console.error('[OpenList Refresh] 读取现有 metainfo 失败:', error); + metaInfo = { + folders: {}, + last_refresh: Date.now(), + }; + } + } + + invalidateMetaInfoCache(rootPath); + + const folders: any[] = []; + let currentPage = 1; + const pageSize = 100; + let total = 0; + + while (true) { + const listResponse = await client.listDirectory(rootPath, currentPage, pageSize, true); + + if (listResponse.code !== 200) { + throw new Error('OpenList 列表获取失败'); + } + + total = listResponse.data.total; + const pageFolders = listResponse.data.content.filter((item) => item.is_dir); + folders.push(...pageFolders); + + if (folders.length >= total) { + break; + } + + currentPage++; + } + + updateScanTaskProgress(taskId, 0, folders.length); + + let newCount = 0; + let existingCount = 0; + let errorCount = 0; + + const existingKeys = new Set(Object.keys(metaInfo.folders)); + + const folderNameToKey = new Map(); + for (const [key, info] of Object.entries(metaInfo.folders)) { + folderNameToKey.set(info.folderName, key); + } + + for (let i = 0; i < folders.length; i++) { + const folder = folders[i]; + + updateScanTaskProgress(taskId, i + 1, folders.length, folder.name); + + if (!clearMetaInfo && folderNameToKey.has(folder.name)) { + existingCount++; + continue; + } + + const folderKey = generateFolderKey(folder.name, existingKeys); + existingKeys.add(folderKey); + + try { + const seasonInfo = parseSeasonFromTitle(folder.name); + const searchQuery = seasonInfo.cleanTitle || folder.name; + + console.log(`[OpenList Refresh] 处理文件夹: ${folder.name}`); + console.log(`[OpenList Refresh] 清理后标题: ${searchQuery}, 季度: ${seasonInfo.seasonNumber}, 年份: ${seasonInfo.year}`); + + const searchResult = await searchTMDB( + tmdbApiKey, + searchQuery, + tmdbProxy, + seasonInfo.year || undefined + ); + + if (searchResult.code === 200 && searchResult.result) { + const result = searchResult.result; + + const folderInfo: any = { + folderName: folder.name, + tmdb_id: result.id, + title: result.title || result.name || folder.name, + poster_path: result.poster_path, + release_date: result.release_date || result.first_air_date || '', + overview: result.overview, + vote_average: result.vote_average, + media_type: result.media_type, + last_updated: Date.now(), + failed: false, + }; + + if (result.media_type === 'tv' && seasonInfo.seasonNumber) { + try { + const seasonDetails = await getTVSeasonDetails( + tmdbApiKey, + result.id, + seasonInfo.seasonNumber, + tmdbProxy + ); + + if (seasonDetails.code === 200 && seasonDetails.season) { + folderInfo.season_number = seasonDetails.season.season_number; + folderInfo.season_name = seasonDetails.season.name; + + if (seasonDetails.season.season_number > 1) { + folderInfo.title = `${folderInfo.title} ${seasonDetails.season.name}`; + } + + if (seasonDetails.season.poster_path) { + folderInfo.poster_path = seasonDetails.season.poster_path; + } + if (seasonDetails.season.overview) { + folderInfo.overview = seasonDetails.season.overview; + } + if (seasonDetails.season.air_date) { + folderInfo.release_date = seasonDetails.season.air_date; + } + } else { + console.warn(`[OpenList Refresh] 获取季度 ${seasonInfo.seasonNumber} 详情失败`); + folderInfo.season_number = seasonInfo.seasonNumber; + } + } catch (error) { + console.error(`[OpenList Refresh] 获取季度详情异常:`, error); + folderInfo.season_number = seasonInfo.seasonNumber; + } + } + + metaInfo.folders[folderKey] = folderInfo; + newCount++; + } else { + metaInfo.folders[folderKey] = { + folderName: folder.name, + tmdb_id: 0, + title: folder.name, + poster_path: null, + release_date: '', + overview: '', + vote_average: 0, + media_type: 'movie', + last_updated: Date.now(), + failed: true, + }; + errorCount++; + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + } catch (error) { + console.error(`[OpenList Refresh] 处理文件夹失败: ${folder.name}`, error); + metaInfo.folders[folderKey] = { + folderName: folder.name, + tmdb_id: 0, + title: folder.name, + poster_path: null, + release_date: '', + overview: '', + vote_average: 0, + media_type: 'movie', + last_updated: Date.now(), + failed: true, + }; + errorCount++; + } + } + + metaInfo.last_refresh = Date.now(); + + const metainfoContent = JSON.stringify(metaInfo); + await db.setGlobalValue('video.metainfo', metainfoContent); + + invalidateMetaInfoCache(rootPath); + setCachedMetaInfo(rootPath, metaInfo); + + const config = await getConfig(); + config.OpenListConfig!.LastRefreshTime = Date.now(); + config.OpenListConfig!.ResourceCount = Object.keys(metaInfo.folders).length; + await db.saveAdminConfig(config); + + completeScanTask(taskId, { + total: folders.length, + new: newCount, + existing: existingCount, + errors: errorCount, + }); + } catch (error) { + console.error('[OpenList Refresh] 扫描失败:', error); + failScanTask(taskId, (error as Error).message); + throw error; + } +}