1 parent
f1bb15ef20
commit
18f46b9a4a
1 file changed
+272
-18
+272
-18
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user