diff --git a/danmu_api/utils/tmdb-util.js b/danmu_api/utils/tmdb-util.js index 1402d3c..80d5d8b 100644 --- a/danmu_api/utils/tmdb-util.js +++ b/danmu_api/utils/tmdb-util.js @@ -35,9 +35,58 @@ async function tmdbApiGet(url) { } // 使用 TMDB API 查询片名 -export async function searchTmdbTitles(title, mediaType = "multi") { - const url = `search/${mediaType}?api_key=${globals.tmdbApiKey}&query=${encodeURIComponent(title)}&language=zh-CN`; - return await tmdbApiGet(url); +export async function searchTmdbTitles(title, mediaType = "multi", options = {}) { + const { + page = 1, // 起始页码 + maxPages = 3, // 最多获取几页结果 + signal = null // 中断信号 + } = options; + + // 如果指定了具体页码,只获取单页 + if (options.page !== undefined) { + const url = `search/${mediaType}?api_key=${globals.tmdbApiKey}&query=${encodeURIComponent(title)}&language=zh-CN&page=${page}`; + return await tmdbApiGet(url); + } + + // 默认获取多页合并结果 + const allResults = []; + + for (let currentPage = 1; currentPage <= maxPages; currentPage++) { + // 检查是否中断 + if (signal && signal.aborted) { + throw new DOMException('Aborted', 'AbortError'); + } + + const url = `search/${mediaType}?api_key=${globals.tmdbApiKey}&query=${encodeURIComponent(title)}&language=zh-CN&page=${currentPage}`; + const response = await tmdbApiGet(url); + + if (!response || !response.data) { + break; + } + + const data = typeof response.data === "string" ? JSON.parse(response.data) : response.data; + + if (!data.results || data.results.length === 0) { + break; + } + + allResults.push(...data.results); + + // 如果当前页结果少于20条,说明没有更多结果了 + if (data.results.length < 20) { + break; + } + } + + log("info", `[TMDB] 共获取到 ${allResults.length} 条搜索结果(最多${maxPages}页)`); + + // 返回与原格式兼容的结构 + return { + data: { + results: allResults + }, + status: 200 + }; } // 使用 TMDB API 获取日语详情 @@ -52,6 +101,100 @@ export async function getTmdbExternalIds(mediaType, tmdbId) { return await tmdbApiGet(url); } +// 使用 TMDB API 获取别名 +async function getTmdbAlternativeTitles(mediaType, tmdbId) { + const url = `${mediaType}/${tmdbId}/alternative_titles?api_key=${globals.tmdbApiKey}`; + return await tmdbApiGet(url); +} + +// 从别名中提取中文别名相关函数 +function extractChineseTitleFromAlternatives(altData, mediaType) { + if (!altData || !altData.data) return null; + + // TV 剧集的别名在 results 字段,电影在 titles 字段 + const titles = altData.data.results || altData.data.titles || []; + + if (!Array.isArray(titles) || titles.length === 0) { + return null; + } + + // 优先级:CN(中国大陆)> TW(台湾)> HK(香港)> SG(新加坡) + const priorityRegions = ['CN', 'TW', 'HK', 'SG']; + + for (const region of priorityRegions) { + const match = titles.find(t => { + const iso = t.iso_3166_1 || t.iso_639_1; + const title = t.title || t.name || ""; + return iso === region && title && !isNonChinese(title); + }); + + if (match) { + const chineseTitle = match.title || match.name; + log("info", `[TMDB] 从别名中找到中文标题 (${region}): ${chineseTitle}`); + return chineseTitle; + } + } + + // 如果没有找到指定地区的,查找任何包含中文的别名 + const anyChineseTitle = titles.find(t => { + const title = t.title || t.name || ""; + return title && !isNonChinese(title); + }); + + if (anyChineseTitle) { + const title = anyChineseTitle.title || anyChineseTitle.name; + log("info", `[TMDB] 从别名中找到中文标题 (其他地区): ${title}`); + return title; + } + + return null; +} + +// 别名获取判断相关函数 +async function getChineseTitleForResult(result, signal) { + const resultTitle = result.name || result.title || ""; + + // 如果已经是中文标题,直接返回 + if (!isNonChinese(resultTitle)) { + return resultTitle; + } + + // 非中文标题,尝试获取中文别名 + log("info", `[TMDB] 检测到非中文标题 "${resultTitle}", 尝试获取中文别名`); + + const mediaType = result.media_type || (result.name ? "tv" : "movie"); + + try { + // 在发起别名请求前检查是否已中断 + if (signal && signal.aborted) { + throw new DOMException('Aborted', 'AbortError'); + } + + const altResp = await getTmdbAlternativeTitles(mediaType, result.id); + + // 别名请求返回后再次检查(请求期间可能被中断) + if (signal && signal.aborted) { + throw new DOMException('Aborted', 'AbortError'); + } + + const chineseTitle = extractChineseTitleFromAlternatives(altResp, mediaType); + + if (chineseTitle) { + log("info", `[TMDB] 将使用中文别名进行相似匹配: ${chineseTitle}`); + return chineseTitle; + } else { + log("info", `[TMDB] 未找到中文别名,使用原标题: ${resultTitle}`); + return resultTitle; + } + } catch (error) { + if (error.name === 'AbortError') { + throw error; + } + log("error", `[TMDB] 获取别名失败: ${error.message}`); + return resultTitle; // 失败则返回原标题 + } +} + // 使用TMDB API 查询日语原名搜索bahamut相关函数 export async function getTmdbJaOriginalTitle(title, signal = null) { if (!globals.tmdbApiKey) { @@ -119,9 +262,34 @@ export async function getTmdbJaOriginalTitle(title, signal = null) { // 相似度计算函数 const similarity = (s1, s2) => { - const longer = s1.length > s2.length ? s1 : s2; - const shorter = s1.length > s2.length ? s2 : s1; - if (longer.length === 0) return 1.0; + // 标准化处理 + const normalize = (str) => { + return str.toLowerCase() + .replace(/\s+/g, '') + .replace(/[::、,。!?;""''()【】《》]/g, '') + .trim(); + }; + + const n1 = normalize(s1); + const n2 = normalize(s2); + + // 完全匹配 + if (n1 === n2) return 1.0; + + // 包含关系检查 + const shorter = n1.length < n2.length ? n1 : n2; + const longer = n1.length >= n2.length ? n1 : n2; + + if (longer.includes(shorter) && shorter.length > 0) { + // 如果有连词则得到一定加分 + const lengthRatio = shorter.length / longer.length; + return 0.6 + (lengthRatio * 0.30); + } + + // 编辑距离计算 + const longer2 = s1.length > s2.length ? s1 : s2; + const shorter2 = s1.length > s2.length ? s2 : s1; + if (longer2.length === 0) return 1.0; const editDistance = (str1, str2) => { str1 = str1.toLowerCase(); @@ -146,7 +314,7 @@ export async function getTmdbJaOriginalTitle(title, signal = null) { return costs[str2.length]; }; - return (longer.length - editDistance(longer, shorter)) / longer.length; + return (longer2.length - editDistance(longer2, shorter2)) / longer2.length; }; // 第一步:TMDB搜索 @@ -156,8 +324,9 @@ export async function getTmdbJaOriginalTitle(title, signal = null) { if (signal && signal.aborted) { throw new DOMException('Aborted', 'AbortError'); } - const respZh = await searchTmdbTitles(title); - + + const respZh = await searchTmdbTitles(title, "multi", { signal }); + if (!respZh || !respZh.data) { log("info", "[TMDB] TMDB搜索结果为空"); return null; @@ -180,20 +349,105 @@ export async function getTmdbJaOriginalTitle(title, signal = null) { log("info", `[TMDB] 类型判断通过: ${validationResult.details}`); - // 第三步:找到最相似的结果 - let bestMatch = dataZh.results[0]; - let bestScore = 0; + // 第三步:找到最相似的结果 + let bestMatch = null; + let bestScore = -1; + let bestMatchChineseTitle = null; + let alternativeTitleFetchCount = 0; // 别名获取计数器 + const MAX_ALTERNATIVE_FETCHES = 5; // 最多获取5个别名 + let skipAlternativeFetch = false; // 是否跳过后续别名获取 for (const result of dataZh.results) { const resultTitle = result.name || result.title || ""; - const score = similarity(title, resultTitle); - if (score > bestScore) { - bestScore = score; + if (!resultTitle) continue; + + // 先计算原标题的相似度(包括original_name/original_title) + const directScore = similarity(title, resultTitle); + const originalTitle = result.original_name || result.original_title || ""; + const originalScore = originalTitle ? similarity(title, originalTitle) : 0; + const initialScore = Math.max(directScore, originalScore); + + // 如果原标题已经100%匹配,标记跳过后续所有别名搜索 + if (initialScore === 1.0 && !skipAlternativeFetch) { + skipAlternativeFetch = true; + log("info", `[TMDB] 匹配检查 "${resultTitle}" - 相似度: 100.00% (完全匹配,跳过后续所有别名搜索)`); + if (initialScore > bestScore) { + bestScore = initialScore; + bestMatch = result; + bestMatchChineseTitle = resultTitle; + } + continue; + } + + // 获取可用的中文标题(如果需要会自动获取别名) + let chineseTitle; + let finalScore; + + // 如果已经找到100%匹配或已是中文,直接使用原标题不获取别名 + if (skipAlternativeFetch || !isNonChinese(resultTitle)) { + chineseTitle = resultTitle; + finalScore = initialScore; + + if (skipAlternativeFetch && isNonChinese(resultTitle)) { + log("info", `[TMDB] 匹配检查 "${resultTitle}" - 相似度: ${(finalScore * 100).toFixed(2)}% (已找到完全匹配,跳过别名搜索)`); + } else { + log("info", `[TMDB] 匹配检查 "${resultTitle}" - 相似度: ${(finalScore * 100).toFixed(2)}%`); + } + } else { + // 非中文且未达到别名获取上限,尝试获取别名 + if (alternativeTitleFetchCount < MAX_ALTERNATIVE_FETCHES) { + try { + chineseTitle = await getChineseTitleForResult(result, signal); + // 如果实际获取了别名(即返回值与原标题不同),计数器+1 + if (chineseTitle !== resultTitle) { + alternativeTitleFetchCount++; + } + } catch (error) { + if (error.name === 'AbortError') { + throw error; + } + log("error", `[TMDB] 处理结果失败: ${error.message}`); + chineseTitle = resultTitle; + } + } else { + // 超过上限,直接使用原标题 + chineseTitle = resultTitle; + log("info", `[TMDB] 已达到别名获取上限(${MAX_ALTERNATIVE_FETCHES}),使用原标题: ${resultTitle}`); + } + + // 计算相似度(使用中文标题) + const finalDirectScore = similarity(title, chineseTitle); + finalScore = Math.max(finalDirectScore, originalScore); + + // 日志输出 + const displayInfo = chineseTitle !== resultTitle + ? `"${resultTitle}" (别名: ${chineseTitle})` + : `"${resultTitle}"`; + log("info", `[TMDB] 匹配检查 ${displayInfo} - 相似度: ${(finalScore * 100).toFixed(2)}%`); + + // 如果别名匹配达到100%,标记跳过后续所有别名搜索 + if (finalScore === 1.0 && !skipAlternativeFetch) { + skipAlternativeFetch = true; + log("info", `[TMDB] 通过别名找到完全匹配,跳过后续所有别名搜索`); + } + } + + // 更新最佳匹配 + if (finalScore > bestScore) { + bestScore = finalScore; bestMatch = result; + bestMatchChineseTitle = chineseTitle; } } - log("info", `[TMDB] TMDB最佳匹配: ${bestMatch.name || bestMatch.title}, 相似度: ${(bestScore * 100).toFixed(2)}%`); + // 相似度阈值检查 + const MIN_SIMILARITY = 0.2; + if (!bestMatch || bestScore < MIN_SIMILARITY) { + log("info", `[TMDB] 最佳匹配相似度过低或未找到匹配 (${bestMatch ? (bestScore * 100).toFixed(2) + '%' : 'N/A'}),跳过`); + return null; + } + + log("info", `[TMDB] TMDB最佳匹配: ${bestMatchChineseTitle}, 相似度: ${(bestScore * 100).toFixed(2)}%`); // 第四步:获取日语详情 const mediaType = bestMatch.media_type || (bestMatch.name ? "tv" : "movie"); @@ -213,7 +467,7 @@ export async function getTmdbJaOriginalTitle(title, signal = null) { const detail = typeof detailResp.data === "string" ? JSON.parse(detailResp.data) : detailResp.data; const jaOriginalTitle = detail.original_name || detail.original_title || detail.name || detail.title; - log("info", `[TMDB] 找到日语原名: ${jaOriginalTitle} (相似度: ${(bestScore * 100).toFixed(2)}%)`); + log("info", `[TMDB] 找到日语原名: ${jaOriginalTitle}`); return jaOriginalTitle; @@ -283,4 +537,4 @@ export async function getTMDBChineseTitle(title, season = null, episode = null) log("error", '查询 TMDB 时出错:', error); return title; } -} \ No newline at end of file +}