巴哈姆特专项优化 (#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:
1 parent
df1e1d47a3
commit
90de8a29c5
7 files changed
+311
-127
No files matched your search
+15
-5
@@ -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=
|
||||
|
||||
@@ -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`表示不缓存 |
|
||||
|
||||
@@ -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,宽松模糊匹配)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,4 +109,4 @@ export const globals = new Proxy({}, {
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return Object.getOwnPropertyDescriptor(Globals.getConfig(), prop);
|
||||
}
|
||||
});
|
||||
});
|
||||
+43
-17
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user