Files
MoonTVPlus/src/app/api/openlist/refresh/route.ts
2025-12-25 23:34:54 +08:00

345 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
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';
export const runtime = 'nodejs';
/**
* POST /api/openlist/refresh
* 刷新私人影库元数据(后台任务模式)
*/
export async function POST(request: NextRequest) {
try {
// 权限检查
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
// 获取请求参数
const body = await request.json().catch(() => ({}));
const clearMetaInfo = body.clearMetaInfo === true; // 是否清空 metainfo
// 获取配置
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);
});
return NextResponse.json({
success: true,
taskId,
message: '扫描任务已启动',
});
} catch (error) {
console.error('启动刷新任务失败:', error);
return NextResponse.json(
{ error: '启动失败', details: (error as Error).message },
{ status: 500 }
);
}
}
/**
* 执行扫描任务
*/
async function performScan(
taskId: string,
url: string,
rootPath: string,
tmdbApiKey: string,
tmdbProxy?: string,
username?: string,
password?: string,
clearMetaInfo?: boolean
): Promise<void> {
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;
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
// 更新进度
updateScanTaskProgress(taskId, i + 1, folders.length, folder.name);
// 如果是立即扫描(不清空 metainfo且文件夹已存在跳过
if (!clearMetaInfo && metaInfo.folders[folder.name]) {
existingCount++;
continue;
}
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}`);
// 搜索 TMDB使用清理后的标题
const searchResult = await searchTMDB(
tmdbApiKey,
searchQuery,
tmdbProxy
);
if (searchResult.code === 200 && searchResult.result) {
const result = searchResult.result;
// 基础信息
const folderInfo: any = {
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}`;
folderInfo.tmdb_id = seasonDetails.season.id; // 使用季度的ID
}
// 使用季度的海报(如果有)
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[folder.name] = folderInfo;
newCount++;
} else {
// 记录失败的文件夹
metaInfo.folders[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[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;
}
}