为TMDB支持分页查询和别名查询,提升结果匹配率 (#109)

TMDB你个草台班子我真是受够你了!
This commit is contained in:
Elegy233 authored and GitHub committed 2025-12-11 00:06:46 +08:00
1 parent f1bb15ef20
commit 18f46b9a4a
1 file changed
+272 -18
+272 -18
View File
@@ -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;
}
}
}