Files
MoonTVPlus/src/lib/openlist-refresh.ts

426 lines
13 KiB
TypeScript
Raw Normal View History

2025-12-31 00:52:15 +08:00
/* 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';
2026-01-01 20:57:44 +08:00
import parseTorrentName from 'parse-torrent-name';
2026-01-10 10:25:14 +08:00
import type { AdminConfig } from '@/lib/admin.types';
/**
*
*/
function getRootPaths(openListConfig: AdminConfig['OpenListConfig']): string[] {
if (!openListConfig) {
return ['/'];
}
// 如果有新字段 RootPaths直接使用
if (openListConfig.RootPaths && openListConfig.RootPaths.length > 0) {
return openListConfig.RootPaths;
}
// 如果只tPath返回单元素数组
if (openListConfig.RootPath) {
return [openListConfig.RootPath];
}
// 默认值
return ['/'];
}
/**
*
*/
async function migrateToMultiRoot(openListConfig: NonNullable<AdminConfig['OpenListConfig']>): Promise<void> {
const oldRootPath = openListConfig.RootPath!;
console.log('[OpenList Migration] 检测到旧版配置,开始迁移...');
// 1. 读取现有 metainfo
const metainfoContent = await db.getGlobalValue('video.metainfo');
if (metainfoContent) {
const metaInfo: MetaInfo = JSON.parse(metainfoContent);
// 2. 迁移 folderName加上原根路径前缀
for (const [key, info] of Object.entries(metaInfo.folders)) {
const oldFolderName = info.folderName;
const newFolderName = `${oldRootPath}${oldRootPath.endsWith('/') ? '' : '/'}${oldFolderName}`;
info.folderName = newFolderName;
console.log(`[Migration] ${oldFolderName} -> ${newFolderName}`);
}
// 3. 保存迁移后的 metainfo
await db.setGlobalValue('video.metainfo', JSON.stringify(metaInfo));
console.log('[OpenList Migration] MetaInfo 迁移完成');
}
// 4. 更新配置RootPath -> RootPaths
const config = await getConfig();
config.OpenListConfig!.RootPaths = [oldRootPath];
delete config.OpenListConfig!.RootPath;
await db.saveAdminConfig(config);
console.log('[OpenList Migration] 配置迁移完成');
}
2025-12-31 00:52:15 +08:00
/**
* 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;
2026-01-17 21:18:19 +08:00
const tmdbReverseProxy = config.SiteConfig.TMDBReverseProxy;
2025-12-31 00:52:15 +08:00
if (!tmdbApiKey) {
throw new Error('TMDB API Key 未配置');
}
2026-01-10 10:25:14 +08:00
// 检测是否需要迁移
if (openListConfig.RootPath && !openListConfig.RootPaths) {
await migrateToMultiRoot(openListConfig);
// 重新加载配置
const newConfig = await getConfig();
Object.assign(openListConfig, newConfig.OpenListConfig);
}
2025-12-31 00:52:15 +08:00
cleanupOldTasks();
const taskId = createScanTask();
2026-01-10 10:25:14 +08:00
const rootPaths = getRootPaths(openListConfig);
// 顺序扫描多个根目录
performMultiRootScan(
2025-12-31 00:52:15 +08:00
taskId,
openListConfig.URL,
2026-01-10 10:25:14 +08:00
rootPaths,
2025-12-31 00:52:15 +08:00
tmdbApiKey,
tmdbProxy,
2026-01-17 21:18:19 +08:00
tmdbReverseProxy,
2025-12-31 00:52:15 +08:00
openListConfig.Username,
openListConfig.Password,
2026-01-01 20:57:44 +08:00
clearMetaInfo,
openListConfig.ScanMode || 'hybrid'
2025-12-31 00:52:15 +08:00
).catch((error) => {
console.error('[OpenList Refresh] 后台扫描失败:', error);
failScanTask(taskId, (error as Error).message);
});
return { taskId };
}
2026-01-10 10:25:14 +08:00
/**
*
*/
async function performMultiRootScan(
taskId: string,
url: string,
rootPaths: string[],
tmdbApiKey: string,
tmdbProxy: string | undefined,
2026-01-17 21:18:19 +08:00
tmdbReverseProxy: string | undefined,
2026-01-10 10:25:14 +08:00
username: string,
password: string,
clearMetaInfo: boolean,
scanMode: 'torrent' | 'name' | 'hybrid'
): Promise<void> {
for (let i = 0; i < rootPaths.length; i++) {
const rootPath = rootPaths[i];
console.log(`[OpenList Refresh] 扫描根目录 (${i + 1}/${rootPaths.length}): ${rootPath}`);
try {
await performScan(
taskId,
url,
rootPath,
tmdbApiKey,
tmdbProxy,
2026-01-17 21:18:19 +08:00
tmdbReverseProxy,
2026-01-10 10:25:14 +08:00
username,
password,
clearMetaInfo && i === 0, // 只在第一个根目录时清除
scanMode
);
} catch (error) {
console.error(`[OpenList Refresh] 根目录 ${rootPath} 扫描失败:`, error);
// 继续扫描其他根目录
}
}
}
2025-12-31 00:52:15 +08:00
/**
*
*/
async function performScan(
taskId: string,
url: string,
rootPath: string,
tmdbApiKey: string,
tmdbProxy?: string,
2026-01-17 21:18:19 +08:00
tmdbReverseProxy?: string,
2025-12-31 00:52:15 +08:00
username?: string,
password?: string,
2026-01-01 20:57:44 +08:00
clearMetaInfo?: boolean,
scanMode: 'torrent' | 'name' | 'hybrid' = 'hybrid'
2025-12-31 00:52:15 +08:00
): Promise<void> {
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(),
};
}
}
2026-01-10 10:25:14 +08:00
invalidateMetaInfoCache();
2025-12-31 00:52:15 +08:00
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<string>(Object.keys(metaInfo.folders));
const folderNameToKey = new Map<string, string>();
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);
2026-01-10 10:25:14 +08:00
// folderName 存储完整路径(包含根目录)
const fullFolderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${folder.name}`;
if (!clearMetaInfo && folderNameToKey.has(fullFolderPath)) {
2025-12-31 00:52:15 +08:00
existingCount++;
continue;
}
2026-01-10 10:25:14 +08:00
const folderKey = generateFolderKey(fullFolderPath, existingKeys);
2025-12-31 00:52:15 +08:00
existingKeys.add(folderKey);
try {
2026-01-01 20:57:44 +08:00
let searchQuery: string;
let seasonNumber: number | null = null;
let year: number | null = null;
let searchResult: any;
2025-12-31 00:52:15 +08:00
2026-01-01 20:57:44 +08:00
if (scanMode === 'torrent' || scanMode === 'hybrid') {
const torrentInfo = parseTorrentName(folder.name);
searchQuery = torrentInfo.title || folder.name;
seasonNumber = torrentInfo.season || null;
year = torrentInfo.year || null;
2025-12-31 00:52:15 +08:00
2026-01-01 20:57:44 +08:00
console.log(`[OpenList Refresh] 种子库模式 - 文件夹: ${folder.name}`);
console.log(`[OpenList Refresh] 解析结果 - 标题: ${searchQuery}, 季度: ${seasonNumber}, 年份: ${year}`);
2026-01-17 21:18:19 +08:00
searchResult = await searchTMDB(tmdbApiKey, searchQuery, tmdbProxy, year || undefined, tmdbReverseProxy);
2026-01-01 20:57:44 +08:00
}
if (scanMode === 'name' || (scanMode === 'hybrid' && (!searchResult || searchResult.code !== 200 || !searchResult.result))) {
const seasonInfo = parseSeasonFromTitle(folder.name);
searchQuery = seasonInfo.cleanTitle || folder.name;
seasonNumber = seasonInfo.seasonNumber;
year = seasonInfo.year;
console.log(`[OpenList Refresh] 名字匹配模式 - 文件夹: ${folder.name}`);
console.log(`[OpenList Refresh] 清理后标题: ${searchQuery}, 季度: ${seasonNumber}, 年份: ${year}`);
2026-01-17 21:18:19 +08:00
searchResult = await searchTMDB(tmdbApiKey, searchQuery, tmdbProxy, year || undefined, tmdbReverseProxy);
2026-01-01 20:57:44 +08:00
}
2025-12-31 00:52:15 +08:00
if (searchResult.code === 200 && searchResult.result) {
const result = searchResult.result;
const folderInfo: any = {
2026-01-10 10:25:14 +08:00
folderName: fullFolderPath,
2025-12-31 00:52:15 +08:00
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,
};
2026-01-01 20:57:44 +08:00
if (result.media_type === 'tv' && seasonNumber) {
2025-12-31 00:52:15 +08:00
try {
const seasonDetails = await getTVSeasonDetails(
tmdbApiKey,
result.id,
2026-01-01 20:57:44 +08:00
seasonNumber,
2026-01-17 21:18:19 +08:00
tmdbProxy,
tmdbReverseProxy
2025-12-31 00:52:15 +08:00
);
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 {
2026-01-01 20:57:44 +08:00
console.warn(`[OpenList Refresh] 获取季度 ${seasonNumber} 详情失败`);
folderInfo.season_number = seasonNumber;
2025-12-31 00:52:15 +08:00
}
} catch (error) {
console.error(`[OpenList Refresh] 获取季度详情异常:`, error);
2026-01-01 20:57:44 +08:00
folderInfo.season_number = seasonNumber;
2025-12-31 00:52:15 +08:00
}
}
metaInfo.folders[folderKey] = folderInfo;
newCount++;
} else {
metaInfo.folders[folderKey] = {
2026-01-10 10:25:14 +08:00
folderName: fullFolderPath,
2025-12-31 00:52:15 +08:00
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] = {
2026-01-10 10:25:14 +08:00
folderName: fullFolderPath,
2025-12-31 00:52:15 +08:00
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);
2026-01-10 10:25:14 +08:00
invalidateMetaInfoCache();
setCachedMetaInfo(metaInfo);
2025-12-31 00:52:15 +08:00
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;
}
}