巴哈姆特专项优化 (#74)

* 增加反代模式判断

* 增强SOURCE_ORDER和PROXY_URL环境变量

1.SOURCE_ORDER中用户可以为bahamut添加后缀@tc实现不繁转简;
2.PROXY_URL新增反代模式判断

* TMDB搜索增加类型判断

并在搜索流程中增加检查点实现可被中断

* 增加反代模式,第二次搜索并行执行并可被打断

并统一搜索构建

* Update README.md

* Update .env.example

* Update README.md

* Update .env.example

* 规范化,繁转简控制改为单独的环境变量DANMU_SIMPLIFIED

* 规范化,繁转简控制改为单独的环境变量DANMU_SIMPLIFIED

* 增加PROXY_URL控制变量

* 增加反代模式判断

* 规范化调整

* 更新DANMU_SIMPLIFIED

* Update .env.example

* 去除不必要的

* 实际并不需要修改这个文件

* Update globals.js

* Update globals.js

* Update globals.js
This commit is contained in:
Elegy233 authored and GitHub committed 2025-10-27 22:34:17 +08:00
1 parent df1e1d47a3
commit 90de8a29c5
7 files changed
+311 -127

No files matched your search

+15 -5
View File
@@ -105,6 +105,12 @@ CONVERT_TOP_BOTTOM_TO_SCROLL=false
# 可选值:true, false
CONVERT_COLOR_TO_WHITE=false
# 是否将繁体弹幕转换为简体,目前只对巴哈姆特源生效
# 默认值:true(转换)
# 说明:禁用后,不进行巴哈弹幕繁转简
# 可选值:true, false
DANMU_SIMPLIFIED=true
# ==================== 弹幕输出格式配置 ====================
# 弹幕输出格式(全局默认格式)
@@ -145,17 +151,21 @@ STRICT_TITLE_MATCH=false
# ==================== 代理配置 ====================
# 代理地址(用于访问受限资源)
# 默认值:空(不使用代理)
# 格式:http://proxy-server:port 或 https://proxy-server:port
# 说明:目前只对巴哈姆特生效,如果使用 Docker 部署并且访问不了 bahamut 源,请配置代理地址
# 代理/反代地址(用于访问受限资源)
# 默认值:空(不使用代理/反代)
# 格式:
# - 设置为代理为 http://proxy-server:port 或 https://proxy-server:port
# - 设置为反代为 RP@http://proxy-server:port 或 RP@https://proxy-server
# 说明:目前只对巴哈姆特生效,如果使用 Docker 部署并且访问不了 bahamut 源,请配置代理/反代地址
# 反代教程参考:https://github.com/wan0ge/bahamut-api-proxy
# PROXY_URL=
# ==================== TMDB 配置 ====================
# TMDB API Key(用于巴哈姆特源的译名转换)
# 默认值:空(不使用 TMDB)
# 说明:配置后当巴哈第一次搜索无结果时自动从 TMDB 获取日语原名搜索
# 说明:配置后并行从 TMDB 获取日语原名搜索巴哈
# 如果TMDB条目类型不是动画或制作地区不是jp则不会进行巴哈搜索
# 可以解决巴哈译名不同导致的搜索无结果问题(例如:间谍过家家 vs 間諜家家酒)
# 获取方法参考:https://www.ugnas.com/tutorial-detail/id-226.html
# TMDB_API_KEY=
+3 -2
View File
@@ -321,8 +321,9 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
| CONVERT_TOP_BOTTOM_TO_SCROLL | 【可选】是否将顶部和底部弹幕转换为浮动弹幕,默认为`false`(不转换),启用后顶部弹幕(ct=5)和底部弹幕(ct=4)会被转换为浮动弹幕(ct=1),可选值:`true`、`false` |
| CONVERT_COLOR_TO_WHITE | 【可选】是否将彩色弹幕转换为纯白弹幕,默认为`false`(不转换),启用后所有非白色的弹幕颜色会被转换为纯白色(16777215),可选值:`true`、`false` |
| DANMU_OUTPUT_FORMAT | 【可选】弹幕输出格式,默认为`json`,可选值:`json`(JSON格式)、`xml`(XML格式),支持通过查询参数`?format=xml`或`?format=json`覆盖此设置,优先级:查询参数 > 环境变量 > 默认值 |
| PROXY_URL | 【可选】代理地址,示例: `http://127.0.0.1:7897` ,目前只对巴哈姆特生效(注意:如果巴哈姆特请求不通,会拖慢搜索返回速度,如需使用bahamut源请在SOURCE_ORDER环境变量中手动添加`bahamut`)如果你使用docker部署并且访问不了bahamut源,请配置代理地址;vercel/netlify/cf中理应都自然能联通,不用填写 |
| TMDB_API_KEY | 【可选】TMDB API Key地址,目前只对巴哈姆特生效,配置后当巴哈第一次搜索无结果时自动从TMDB获取日语原名搜索,可以解决巴哈译名不同导致的搜索无结果问题,例如大陆常用译名`间谍过家家`在巴哈译名为`間諜家家酒`,正常搜索无法搜索到,配置后可以解决这一问题但会稍微影响请求速度,[TMDBAPI](https://www.themoviedb.org/settings/api)获取方法参考:[TMDB API Key申请 - 绿联NAS私有云](https://www.ugnas.com/tutorial-detail/id-226.html) |
| DANMU_SIMPLIFIED | 【可选】是否将繁体弹幕转换为简体,目前只对巴哈姆特生效,默认为`true`(转换),可选值:`false`(不转换) |
| PROXY_URL | 【可选】巴哈代理/反代地址,示例: `http://127.0.0.1:7897`(代理) `RP@http://127.0.0.1`(反代),目前只对巴哈姆特生效(注意:如果巴哈姆特请求不通,会拖慢搜索返回速度,如需使用bahamut源请在SOURCE_ORDER环境变量中手动添加`bahamut`)如果你使用docker部署并且访问不了bahamut源,请配置代理地址或者反代([反代教程](https://github.com/wan0ge/bahamut-api-proxy));vercel/netlify/cf中理应都自然能联通,不用填写 |
| TMDB_API_KEY | 【可选】TMDB API Key地址,目前只对巴哈姆特生效,配置后并行从TMDB获取日语原名搜索巴哈(如果TMDB条目类型不是动画或制作地区不是jp则不会进行巴哈搜索)可以解决巴哈译名不同导致的搜索无结果问题,例如大陆常用译名`间谍过家家`在巴哈译名为`間諜家家酒`,正常搜索无法搜索到,配置后可以解决这一问题但会稍微影响请求速度,[TMDBAPI](https://www.themoviedb.org/settings/api)获取方法参考:[TMDB API Key申请 - 绿联NAS私有云](https://www.ugnas.com/tutorial-detail/id-226.html) |
| RATE_LIMIT_MAX_REQUESTS | 【可选】限流配置:1分钟内同一IP最大请求次数,默认为`3`,设置为`0`表示不限流 |
| LOG_LEVEL | 【可选】日志级别,默认为`info`,可选值:`error`(仅错误)、`warn`(错误和警告)、`info`(所有日志),生产环境建议使用`warn`,调试时使用`info` |
| SEARCH_CACHE_MINUTES | 【可选】搜索结果缓存时间(分钟),默认为`1`,避免短期内重复的不必要API请求,同时保证获取最新的结果列表,可根据需要调整:Vercel/Cloudflare建议`1-5`分钟,Docker可设置`5-30`分钟,设置为`0`表示不缓存 |
+3 -2
View File
@@ -191,7 +191,8 @@ export class Envs {
episodeTitleFilter: this.resolveEpisodeTitleFilter(env), // 剧集标题正则过滤
blockedWords: this.get('BLOCKED_WORDS', '', 'string'), // 屏蔽词列表
groupMinute: Math.min(this.get('GROUP_MINUTE', 1, 'number'), 30), // 分钟内合并去重(默认 1,最大值30,0表示不去重)
proxyUrl: this.get('PROXY_URL', '', 'string'), // 代理地址
proxyUrl: this.get('PROXY_URL', '', 'string', true), // 代理/反代地址
danmuSimplified: this.get('DANMU_SIMPLIFIED', true, 'boolean'), // 弹幕繁体转简体开关
tmdbApiKey: this.get('TMDB_API_KEY', '', 'string', true), // TMDB API KEY
redisUrl: this.get('UPSTASH_REDIS_REST_URL', '', 'string', true), // upstash redis url
redisToken: this.get('UPSTASH_REDIS_REST_TOKEN', '', 'string', true), // upstash redis url
@@ -206,4 +207,4 @@ export class Envs {
strictTitleMatch: this.get('STRICT_TITLE_MATCH', false, 'boolean') // 严格标题匹配模式配置(默认 false,宽松模糊匹配)
};
}
}
}
+1 -1
View File
@@ -109,4 +109,4 @@ export const globals = new Proxy({}, {
getOwnPropertyDescriptor(target, prop) {
return Object.getOwnPropertyDescriptor(Globals.getConfig(), prop);
}
});
});
+43 -17
View File
@@ -274,29 +274,54 @@ function createProxyServer() {
if (queryObject.url) {
const targetUrl = queryObject.url;
console.log('Target URL:', targetUrl);
console.log('[Proxy Server] Target URL:', targetUrl);
// 从环境变量获取代理地址
const proxyUrl = process.env.PROXY_URL;
const urlObj = new URL(targetUrl);
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: 'GET'
// 从环境变量获取代理配置
let proxyConfig = process.env.PROXY_URL;
const originalUrlObj = new URL(targetUrl);
let options = {
hostname: originalUrlObj.hostname,
port: originalUrlObj.port || (originalUrlObj.protocol === 'https:' ? 443 : 80),
path: originalUrlObj.pathname + originalUrlObj.search,
method: 'GET',
headers: { ...req.headers } // 传递原始请求头
};
// Host 头必须被移除,以便 protocol.request 根据 options.hostname 设置正确的值
delete options.headers.host;
let protocol = originalUrlObj.protocol === 'https:' ? https : http;
// 如果设置了代理,则使用代理
if (proxyUrl) {
options.agent = new HttpsProxyAgent(proxyUrl);
console.log('Using proxy:', proxyUrl);
// 检查反代模式 (RP@)
if (proxyConfig && proxyConfig.startsWith("RP@")) {
console.log('[Proxy Server] Reverse proxy mode detected');
const reverseProxyUrlStr = proxyConfig.substring(3).trim().replace(/\/+$/, '');
try {
const reverseUrlObj = new URL(reverseProxyUrlStr);
options.hostname = reverseUrlObj.hostname;
options.port = reverseUrlObj.port || (reverseUrlObj.protocol === 'https:' ? 443 : 80);
// 路径合并:/reverse/proxy/path + /original/path?query
options.path = (reverseUrlObj.pathname.replace(/\/$/, '')) + originalUrlObj.pathname + originalUrlObj.search;
protocol = reverseUrlObj.protocol === 'https:' ? https : http;
console.log(`[Proxy Server] Rewriting to RP: ${protocol === https ? 'https' : 'http'}://${options.hostname}:${options.port}${options.path}`);
} catch (e) {
console.error('[Proxy Server] Invalid RP@ URL:', reverseProxyUrlStr, e.message);
res.statusCode = 500;
res.end('Proxy Error: Invalid Reverse Proxy URL');
return;
}
} else if (proxyConfig) {
// 代理模式:使用 HttpsProxyAgent
console.log('[Proxy Server] Using proxy agent:', proxyConfig);
options.agent = new HttpsProxyAgent(proxyConfig);
} else {
console.log('No proxy configured, direct connection');
// 直连模式
console.log('[Proxy Server] No proxy configured, direct connection');
}
const protocol = urlObj.protocol === 'https:' ? https : http;
const proxyReq = protocol.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
@@ -316,6 +341,7 @@ function createProxyServer() {
});
}
// --- 启动函数 ---
// 同步启动(最优/默认路径,适用于常规已兼容环境)
function startServerSync() {
+144 -68
View File
@@ -11,85 +11,156 @@ import { strictTitleMatch } from "../utils/common-util.js";
// =====================
// 获取巴哈姆特弹幕
// =====================
// 具体搜索部分
export default class BahamutSource extends BaseSource {
async search(keyword) {
try {
// 在函数内部进行简转繁
const traditionalizedKeyword = traditionalized(keyword);
// TMDB 搜索直接使用传入的原始 keyword
const tmdbSearchKeyword = keyword;
// 使用 traditionalizedKeyword 进行巴哈姆特搜索
const encodedKeyword = encodeURIComponent(traditionalizedKeyword);
const url = globals.proxyUrl
? `http://127.0.0.1:5321/proxy?url=https://api.gamer.com.tw/mobile_app/anime/v1/search.php?kw=${encodedKeyword}`
: `https://api.gamer.com.tw/mobile_app/anime/v1/search.php?kw=${encodedKeyword}`;
log("info", `[Bahamut] 原始搜索词: ${keyword}`);
log("info", `[Bahamut] 巴哈使用搜索词: ${traditionalizedKeyword}`);
log("info", `[Bahamut] 传入原始搜索词: ${keyword}`);
log("info", `[Bahamut] 使用巴哈搜索词: ${traditionalizedKeyword}`);
// 创建一个 AbortController 用于取消 TMDB 流程
const tmdbAbortController = new AbortController();
const originalResp = await httpGet(url, {
headers: {
"Content-Type": "application/json",
"User-Agent": "Anime/2.29.2 (7N5749MM3F.tw.com.gamer.anime; build:972; iOS 26.0.0) Alamofire/5.6.4",
},
});
// 第一次搜索:繁体词搜索
const originalSearchPromise = (async () => {
try {
const apiUrl = `/mobile_app/anime/v1/search.php?kw=${encodedKeyword}`;
const originalApiUrl = `https://api.gamer.com.tw${apiUrl}`;
const url = globals.proxyUrl
? `http://127.0.0.1:5321/proxy?url=${encodeURIComponent(originalApiUrl)}`
: originalApiUrl;
const originalResp = await httpGet(url, {
headers: {
"Content-Type": "application/json",
"User-Agent": "Anime/2.29.2 (7N5749MM3F.tw.com.gamer.anime; build:972; iOS 26.0.0) Alamofire/5.6.4",
},
});
// 如果原始搜索有结果,直接返回(并在结果上标注实际用于搜索的字符串)
if (
originalResp &&
originalResp.data &&
originalResp.data.anime &&
originalResp.data.anime.length > 0
) {
const anime = originalResp.data.anime;
// 实际用于 bahamut 搜索的关键字(用于后续匹配参考)
for (const a of anime) {
try {
a._originalQuery = keyword;
a._searchUsedTitle = traditionalizedKeyword;
} catch (e) {}
// 如果原始搜索有结果,中断 TMDB 流程
if (
originalResp &&
originalResp.data &&
originalResp.data.anime &&
originalResp.data.anime.length > 0
) {
tmdbAbortController.abort();
const anime = originalResp.data.anime;
for (const a of anime) {
try {
a._originalQuery = keyword;
a._searchUsedTitle = traditionalizedKeyword;
} catch (e) {}
}
log("info", `bahamutSearchresp (original): ${JSON.stringify(anime)}`);
log("info", `[Bahamut] 返回 ${anime.length} 条结果 (source: original)`);
return { success: true, data: anime, source: 'original' };
}
log("info", `[Bahamut] 原始搜索成功,但未返回任何结果 (source: original)`);
return { success: false, source: 'original' };
} catch (error) {
// ️捕获原始搜索错误,但不阻塞 TMDB 搜索
log("error", "[Bahamut] 原始搜索失败:", {
message: error.message,
name: error.name,
stack: error.stack,
});
return { success: false, source: 'original' };
}
log("info", `bahamutSearchresp (original): ${JSON.stringify(anime)}`);
log("info", `[Bahamut] 返回 ${anime.length} 条结果 (source: original)`);
return anime;
})();
// 第二次搜索:TMDB转换后搜索(并行执行)
const tmdbSearchPromise = (async () => {
try {
// 延迟100毫秒,避免与原始搜索争抢同一连接池
await new Promise(resolve => setTimeout(resolve, 100));
// 内部中断检查 (点1)
if (tmdbAbortController.signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
// 如果类型不符合会返回null,自然跳过后续搜索
const tmdbTitle = await getTmdbJaOriginalTitle(tmdbSearchKeyword, tmdbAbortController.signal);
// 检查 getTmdbJaOriginalTitle 执行期间是否被中断
if (tmdbAbortController.signal.aborted) {
log("info", "[Bahamut] 原始搜索成功,取消TMDB日语原名获取");
throw new DOMException('Aborted', 'AbortError');
}
if (!tmdbTitle) {
log("info", "[Bahamut] TMDB转换未返回结果,取消日语原名搜索");
return { success: false, source: 'tmdb' };
}
// 内部中断检查 (点2)
if (tmdbAbortController.signal.aborted) {
log("info", "[Bahamut] 原始搜索成功,取消日语原名搜索");
throw new DOMException('Aborted', 'AbortError');
}
log("info", `[Bahamut] 使用日语原名进行搜索: ${tmdbTitle}`);
const encodedTmdbTitle = encodeURIComponent(tmdbTitle);
const tmdbApiUrl = `/mobile_app/anime/v1/search.php?kw=${encodedTmdbTitle}`;
const tmdbOriginalUrl = `https://api.gamer.com.tw${tmdbApiUrl}`;
const tmdbSearchUrl = globals.proxyUrl
? `http://127.0.0.1:5321/proxy?url=${encodeURIComponent(tmdbOriginalUrl)}`
: tmdbOriginalUrl;
const tmdbResp = await httpGet(tmdbSearchUrl, {
headers: {
"Content-Type": "application/json",
"User-Agent": "Anime/2.29.2 (7N5749MM3F.tw.com.gamer.anime; build:972; iOS 26.0.0) Alamofire/5.6.4",
},
});
if (tmdbResp && tmdbResp.data && tmdbResp.data.anime && tmdbResp.data.anime.length > 0) {
const anime = tmdbResp.data.anime;
for (const a of anime) {
try {
a._originalQuery = keyword;
a._searchUsedTitle = tmdbTitle;
} catch (e) {}
}
log("info", `bahamutSearchresp (TMDB): ${JSON.stringify(anime)}`);
log("info", `[Bahamut] 返回 ${anime.length} 条结果 (source: tmdb)`);
return { success: true, data: anime, source: 'tmdb' };
}
log("info", `[Bahamut] 日语原名搜索成功,但未返回任何结果 (source: tmdb)`);
return { success: false, source: 'tmdb' };
} catch (error) {
// 捕获被中断的错误
if (error.name === 'AbortError') {
log("info", "[Bahamut] 原始搜索成功,中断日语原名搜索");
return { success: false, source: 'tmdb', aborted: true };
}
// 抛出其他错误(例如 httpGet 超时)
throw error;
}
})();
// 如果两个搜索同时完成,优先采用原始搜索结果
const [originalResult, tmdbResult] = await Promise.all([
originalSearchPromise,
tmdbSearchPromise
]);
// 优先返回原始搜索结果
if (originalResult.success) {
return originalResult.data;
}
// 原始搜索没有结果时,才调用 TMDB 转换(顺序执行)
log("info", "[Bahamut] 原始搜索未返回结果,尝试转换TMDB标题...");
const tmdbTitle = await getTmdbJaOriginalTitle(tmdbSearchKeyword); // 使用原始 keyword (tmdbSearchKeyword)
if (!tmdbTitle) {
log("info", "[Bahamut] TMDB转换未返回标题,中止搜索并转入备用方案.");
return [];
}
log("info", `[Bahamut] 使用TMDB标题进行搜索: ${tmdbTitle}`);
// 确保 TMDB 标题也被编码
const encodedTmdbTitle = encodeURIComponent(tmdbTitle);
const tmdbSearchUrl = globals.proxyUrl
? `http://127.0.0.1:5321/proxy?url=https://api.gamer.com.tw/mobile_app/anime/v1/search.php?kw=${encodedTmdbTitle}`
: `https://api.gamer.com.tw/mobile_app/anime/v1/search.php?kw=${encodedTmdbTitle}`;
const tmdbResp = await httpGet(tmdbSearchUrl, {
headers: {
"Content-Type": "application/json",
"User-Agent": "Anime/2.29.2 (7N5749MM3F.tw.com.gamer.anime; build:972; iOS 26.0.0) Alamofire/5.6.4",
},
});
if (tmdbResp && tmdbResp.data && tmdbResp.data.anime && tmdbResp.data.anime.length > 0) {
const anime = tmdbResp.data.anime;
// 保留 original query 与 实际用于 bahamut 搜索的标题(TMDB 的标题)
for (const a of anime) {
try {
a._originalQuery = keyword;
a._searchUsedTitle = tmdbTitle;
} catch (e) {}
}
log("info", `bahamutSearchresp (TMDB): ${JSON.stringify(anime)}`);
log("info", `[Bahamut] 返回 ${anime.length} 条结果 (source: tmdb)`);
return anime;
// 原始搜索无结果,返回TMDB搜索结果
if (tmdbResult.success) {
return tmdbResult.data;
}
log("info", "[Bahamut] 原始搜索和基于TMDB的搜索均未返回任何结果");
@@ -107,8 +178,10 @@ export default class BahamutSource extends BaseSource {
async getEpisodes(id) {
try {
// 构建剧集信息 URL
const targetUrl = `https://api.gamer.com.tw/anime/v1/video.php?videoSn=${id}`;
const url = globals.proxyUrl ? `http://127.0.0.1:5321/proxy?url=${encodeURIComponent(targetUrl)}` : targetUrl;
const resp = await httpGet(url, {
headers: {
"Content-Type": "application/json",
@@ -278,8 +351,10 @@ export default class BahamutSource extends BaseSource {
let danmus = [];
try {
// 构建弹幕 URL
const targetUrl = `https://api.gamer.com.tw/anime/v1/danmu.php?geo=TW%2CHK&videoSn=${id}`;
const url = globals.proxyUrl ? `http://127.0.0.1:5321/proxy?url=${encodeURIComponent(targetUrl)}` : targetUrl;
const resp = await httpGet(url, {
headers: {
"Content-Type": "application/json",
@@ -309,8 +384,9 @@ export default class BahamutSource extends BaseSource {
return comments.map(c => ({
cid: Number(c.sn),
p: `${Math.round(c.time / 10).toFixed(2)},${positionToMode[c.position] || c.tp},${parseInt(c.color.slice(1), 16)},[bahamut]`,
m: simplized(c.text),
// 根据 globals.danmuSimplified 控制是否繁转简
m: globals.danmuSimplified ? simplized(c.text) : c.text,
t: Math.round(c.time / 10)
}));
}
}
}
+102 -32
View File
@@ -7,54 +7,110 @@ import { httpGet } from "./http-util.js";
// ---------------------
// 使用TMDB API 查询日语原名搜索bahamut相关函数
export async function getTmdbJaOriginalTitle(title) {
export async function getTmdbJaOriginalTitle(title, signal = null) {
if (!globals.tmdbApiKey) {
log("info", "[TMDB] 未配置API密钥,跳过TMDB搜索");
return null;
}
try {
// ---------------------
// 相似度函数
// ---------------------
function similarity(s1, s2) {
// 内部函数:判断单个媒体是否为动画或日语内容
const isValidContent = (mediaInfo) => {
const genreIds = mediaInfo.genre_ids || [];
const genres = mediaInfo.genres || [];
const allGenreIds = genreIds.length > 0 ? genreIds : genres.map(g => g.id);
const originalLanguage = mediaInfo.original_language || '';
const ANIMATION_GENRE_ID = 16;
// 动画类型直接通过
if (allGenreIds.includes(ANIMATION_GENRE_ID)) {
return { isValid: true, reason: "明确动画类型(genre_id: 16)" };
}
// 日语内容通过(涵盖日剧、日影、日综艺)
if (originalLanguage === 'ja') {
return { isValid: true, reason: `原始语言为日语(ja),可能是日剧/日影/日综艺` };
}
return {
isValid: false,
reason: `非动画且非日语内容(language: ${originalLanguage}, genres: ${allGenreIds.join(',')})`
};
};
// 内部函数:批量验证搜索结果
const validateResults = (results) => {
if (!results || results.length === 0) {
return {
hasValid: false,
validCount: 0,
totalCount: 0,
details: "搜索结果为空"
};
}
let validCount = 0;
const validItems = [];
for (const item of results) {
const validation = isValidContent(item);
if (validation.isValid) {
validCount++;
const itemTitle = item.name || item.title || "未知";
validItems.push(`${itemTitle}(${validation.reason})`);
}
}
return {
hasValid: validCount > 0,
validCount: validCount,
totalCount: results.length,
details: validCount > 0
? `找到${validCount}个符合条件的内容: ${validItems.slice(0, 3).join(', ')}${validCount > 3 ? '...' : ''}`
: `所有${results.length}个结果均不符合条件(非动画且非日语)`
};
};
// 相似度计算函数
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 editDistance = (s1, s2) => {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
const editDistance = (str1, str2) => {
str1 = str1.toLowerCase();
str2 = str2.toLowerCase();
const costs = [];
for (let i = 0; i <= s1.length; i++) {
for (let i = 0; i <= str1.length; i++) {
let lastValue = i;
for (let j = 0; j <= s2.length; j++) {
for (let j = 0; j <= str2.length; j++) {
if (i === 0) {
costs[j] = j;
} else if (j > 0) {
let newValue = costs[j - 1];
if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
if (str1.charAt(i - 1) !== str2.charAt(j - 1)) {
newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
}
costs[j - 1] = lastValue;
lastValue = newValue;
}
}
if (i > 0) costs[s2.length] = lastValue;
if (i > 0) costs[str2.length] = lastValue;
}
return costs[s2.length];
return costs[str2.length];
};
return (longer.length - editDistance(longer, shorter)) / longer.length;
}
};
// ---------------------
// 第一步: 中文搜索
// ---------------------
// 第一步:TMDB搜索
const searchUrlZh = `https://api.tmdb.org/3/search/multi?api_key=${globals.tmdbApiKey}&query=${encodeURIComponent(title)}&language=zh-CN`;
log("info", `[TMDB] 正在搜索: ${title}`);
log("info", `[TMDB] 正在搜索(中文): ${title}`);
// 内部中断检查
if (signal && signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
const respZh = await httpGet(searchUrlZh, {
headers: {
"Content-Type": "application/json",
@@ -63,18 +119,28 @@ export async function getTmdbJaOriginalTitle(title) {
});
if (!respZh || !respZh.data) {
log("info", "[TMDB] 中文搜索结果为空");
log("info", "[TMDB] TMDB搜索结果为空");
return null;
}
const dataZh = typeof respZh.data === "string" ? JSON.parse(respZh.data) : respZh.data;
if (!dataZh.results || dataZh.results.length === 0) {
log("info", "[TMDB] 中文搜索未找到任何结果");
log("info", "[TMDB] TMDB未找到任何结果");
return null;
}
// 找到最相似的结果(使用中文标题)
// 第二步:类型验证(宽松策略:只要有一个符合就继续)
const validationResult = validateResults(dataZh.results);
if (!validationResult.hasValid) {
log("info", `[TMDB] 类型判断未通过,跳过后续搜索: ${validationResult.details}`);
return null;
}
log("info", `[TMDB] 类型判断通过: ${validationResult.details}`);
// 第三步:找到最相似的结果
let bestMatch = dataZh.results[0];
let bestScore = 0;
@@ -87,14 +153,16 @@ export async function getTmdbJaOriginalTitle(title) {
}
}
log("info", `[TMDB] 最佳匹配(中文): ${bestMatch.name || bestMatch.title}, 相似度: ${(bestScore * 100).toFixed(2)}%`);
log("info", `[TMDB] TMDB最佳匹配: ${bestMatch.name || bestMatch.title}, 相似度: ${(bestScore * 100).toFixed(2)}%`);
// ---------------------
// 第二步: 使用匹配到的ID,用日语语言查询详情页获取原名
// ---------------------
// 第四步:获取日语详情
const mediaType = bestMatch.media_type || (bestMatch.name ? "tv" : "movie");
const detailUrl = `https://api.tmdb.org/3/${mediaType}/${bestMatch.id}?api_key=${globals.tmdbApiKey}&language=ja-JP`;
// 内部中断检查
if (signal && signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
const detailResp = await httpGet(detailUrl, {
headers: {
"Content-Type": "application/json",
@@ -103,7 +171,6 @@ export async function getTmdbJaOriginalTitle(title) {
});
if (!detailResp || !detailResp.data) {
// 获取详情失败,返回中文搜索结果标题
const fallbackTitle = bestMatch.name || bestMatch.title;
log("info", `[TMDB] 使用中文搜索结果标题: ${fallbackTitle}`);
return fallbackTitle;
@@ -111,13 +178,16 @@ export async function getTmdbJaOriginalTitle(title) {
const detail = typeof detailResp.data === "string" ? JSON.parse(detailResp.data) : detailResp.data;
// 优先使用日语原名 original_name/original_title
const jaOriginalTitle = detail.original_name || detail.original_title || detail.name || detail.title;
log("info", `[TMDB] 找到日语原名: ${jaOriginalTitle} (中文匹配相似度: ${(bestScore * 100).toFixed(2)}%)`);
log("info", `[TMDB] 找到日语原名: ${jaOriginalTitle} (相似度: ${(bestScore * 100).toFixed(2)}%)`);
return jaOriginalTitle;
} catch (error) {
if (error.name === 'AbortError') {
log("info", "[TMDB] 搜索已被中断");
return null;
}
log("error", "[TMDB] Search error:", {
message: error.message,
name: error.name,
@@ -125,4 +195,4 @@ export async function getTmdbJaOriginalTitle(title) {
});
return null;
}
}
}