放宽合并工具3D2D的处理,增强API路径前缀智能处理逻辑(适配danmaku-anywhere项目请求),修复链接解析错误占位符字符串 (#254)

* dandan.js:从tags中提取3D/2D追加到类型

* merge-util.js:放宽没有3D2D标识时的处理

* worker.js:增强API路径前缀智能处理逻辑,适配Danmaku Anywhere项目仅/v2/的请求

旧逻辑面对http://127.0.0.1:9321/********/v2/search/anime会错误补全为http://127.0.0.1:9321/********/api/v2/v2/search/anime

* dandan-api.js:修复了当请求直链 URL 时,API 错误返回占位符字符串

这个问题导致了danmaku-anywhere项目在解析图片时报错,详情:https://github.com/Mr-Quin/danmaku-anywhere/issues/343
This commit is contained in:
Elegy233 authored and GitHub committed 2026-03-25 22:20:42 +08:00
1 parent e74749b3f9
commit 57987959c2
4 files changed
+70 -45

No files matched your search

+6 -6
View File
@@ -251,13 +251,13 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(:\d+)?(\/[^\s]*)?$/;
if (urlRegex.test(queryTitle)) {
const tmpAnime = Anime.fromJson({
"animeId": 111,
"bangumiId": "string",
"animeId": 0,
"bangumiId": "0",
"animeTitle": queryTitle,
"type": "type",
"typeDescription": "string",
"imageUrl": "string",
"startDate": "2025-08-08T13:25:11.189Z",
"type": "",
"typeDescription": "链接解析",
"imageUrl": "",
"startDate": "",
"episodeCount": 1,
"rating": 0,
"isFavorited": true
+27 -12
View File
@@ -27,7 +27,7 @@ const DandanUserAgent = `LogVar Danmu API/${globals.version}`
// 获取弹弹play弹幕
// =====================
export default class DandanSource extends BaseSource {
/**
* 搜索动画条目
* 包含常规搜索、TMDB 日语原名搜索,以及去除季度信息后的降级搜索策略
@@ -120,7 +120,7 @@ export default class DandanSource extends BaseSource {
}
const animes = resp.data.animes;
// 标记 TMDB 来源,供后续处理环节识别以跳过常规标题匹配
for (const anime of animes) {
anime.isTmdbSource = true;
@@ -161,7 +161,7 @@ export default class DandanSource extends BaseSource {
// 当搜索无结果且包含季度信息时,尝试剥离季度信息后重新搜索
if (!isFallback) {
const strippedKeyword = keyword.replace(/(?:第\s*[0-9一二三四五六七八九十百千万]+\s*[季期部])|(?:S(?:eason)?\s*\d+)|(?:Part\s*\d+)/gi, '').trim();
if (strippedKeyword && strippedKeyword !== keyword) {
log("info", `[Dandan] 尝试去除季度信息进行降级搜索: ${strippedKeyword}`);
return await this.search(strippedKeyword, true);
@@ -203,10 +203,10 @@ export default class DandanSource extends BaseSource {
}
const bangumiData = resp.data.bangumi;
// 提取剧集列表,确保它是数组
const episodes = Array.isArray(bangumiData.episodes) ? bangumiData.episodes : [];
// 提取标题别名列表
// 数据源格式: [{"language":"主标题","title":"雨天遇见狸"}, ...]
const titles = Array.isArray(bangumiData.titles) ? bangumiData.titles.map(t => t.title) : [];
@@ -216,7 +216,22 @@ export default class DandanSource extends BaseSource {
// 提取番剧类型信息,用于相关作品无法从搜索接口获取该字段时的数据补全
const type = bangumiData.type || null;
const typeDescription = bangumiData.typeDescription || null;
let typeDescription = bangumiData.typeDescription || null;
// 识别 3D 与 2D 标签并追加至类型描述
let is3D = false;
let is2D = false;
if (bangumiData.tags && Array.isArray(bangumiData.tags)) {
bangumiData.tags.forEach(tag => {
if (tag.name && tag.name.toUpperCase().includes('3D')) is3D = true;
if (tag.name && tag.name.toUpperCase().includes('2D')) is2D = true;
});
}
if (is3D) {
typeDescription = "3D" + (typeDescription || "");
} else if (is2D) {
typeDescription = "2D" + (typeDescription || "");
}
// 提取封面图片 URL,用于 episodes 接口返回结果缺少 imageUrl 时的数据补全
const imageUrl = bangumiData.imageUrl || null;
@@ -447,12 +462,12 @@ export default class DandanSource extends BaseSource {
const url = rel.url;
const shift = rel.shift || 0;
const sourceInfo = this.parseRelatedUrl(url);
if (!sourceInfo) continue;
const { sourceName } = sourceInfo;
const coreUrl = getCoreIdentifier(url, sourceName);
// 构建唯一键存储偏移量,包含平台与核心标识,防止同平台多链接导致数据覆盖
relatedShifts[`${sourceName}:${coreUrl}`] = shift;
@@ -472,7 +487,7 @@ export default class DandanSource extends BaseSource {
if (firstColonIndex === -1) return false;
const mSource = part.substring(0, firstColonIndex);
const mId = part.substring(firstColonIndex + 1);
// 来源标识必须一致
if (mSource !== sourceName) return false;
@@ -505,7 +520,7 @@ export default class DandanSource extends BaseSource {
allDanmus = allDanmus.concat(extra);
}
}
// 汇总日志:仅在产生实际拉取任务时,输出拉取总数与原生基础数据
const totalCount = allDanmus.length;
const dandanCount = stats['dandan'] || 0;
@@ -562,7 +577,7 @@ export default class DandanSource extends BaseSource {
let comments = [];
let sourceInstance = null;
let platName = sourceName; // 映射标准平台名称用于去重工具
// 匹配对应的源实例,并处理标准平台名
if (sourceName === 'tencent') { sourceInstance = tencentSource; platName = 'qq'; }
else if (sourceName === 'iqiyi') { sourceInstance = iqiyiSource; platName = 'qiyi'; }
@@ -618,7 +633,7 @@ export default class DandanSource extends BaseSource {
}
return c;
}
return {
cid: c.cid,
p: `${c.p.replace(/([A-Za-z]+)([0-9a-fA-F]{6})/, (_, platform, hexColor) => {
+14 -13
View File
@@ -739,6 +739,7 @@ function checkTheatricalExemption(titleA, titleB, typeDescA, typeDescB) {
/**
* 校验媒体类型是否冲突 (真人 vs 动漫, TV vs Movie, 3D vs 2D)
* 包含维数通配符逻辑:无明确 3D/2D 标识的条目视为通配符,允许进行任何关联
* @returns {boolean} true=冲突, false=兼容
*/
function checkMediaTypeMismatch(titleA, titleB, typeDescA, typeDescB, countA, countB, sourceA = '', sourceB = '') {
@@ -752,23 +753,23 @@ function checkMediaTypeMismatch(titleA, titleB, typeDescA, typeDescB, countA, co
let is2DA = (typeDescA || '').includes('2D');
let is2DB = (typeDescB || '').includes('2D');
// 维数动态探测逻辑 (Dynamic Dimension Detection):
// 如果有一方在类型中明确是 3D,则被授权从另一方的标题中主动探测 3D 关键词
if (is3DA && !is3DB) {
// 状态判定:检查是否为无维数标识的通配符状态
const isWildcardA = !is3DA && !is2DA;
const isWildcardB = !is3DB && !is2DB;
// 动态探测:仅当一方具备明确维数,另一方为通配符时,尝试从通配符方标题中提取维数
if (!isWildcardA && isWildcardB) {
if (/3[dD]/.test(titleB)) is3DB = true;
} else if (is3DB && !is3DA) {
else if (/2[dD]/.test(titleB)) is2DB = true;
} else if (!isWildcardB && isWildcardA) {
if (/3[dD]/.test(titleA)) is3DA = true;
else if (/2[dD]/.test(titleA)) is2DA = true;
}
// 同样适用 2D 的动态探测:明确有 2D 标签时,才去对方标题里找 2D
if (is2DA && !is2DB) {
if (/2[dD]/.test(titleB)) is2DB = true;
} else if (is2DB && !is2DA) {
if (/2[dD]/.test(titleA)) is2DA = true;
}
// 检测 3D 与 2D 的维数属性冲突:任意一方带 3D 且另一方不带 3D(即视为 2D)则判定为冲突
if (is3DA !== is3DB) return true;
// 维数冲突校验:双方最终都具有确切维数时,才执行严格比对
const finalHasDimA = is3DA || is2DA;
const finalHasDimB = is3DB || is2DB;
if (finalHasDimA && finalHasDimB && is3DA !== is3DB) return true;
const mediaA = getStrictMediaType(titleA, typeDescA);
const mediaB = getStrictMediaType(titleB, typeDescB);
+23 -14
View File
@@ -99,14 +99,14 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
'/api/v2/comment',
'/api/v2/segmentcomment'
];
// 只有当path包含指定接口关键字时才添加到请求记录数组
if (targetPaths.some(targetPath => path.includes(targetPath))) {
// 更新今日请求计数
// 从 reqRecords 最后一个元素获取上一个请求的时间
const lastRecord = globals.reqRecords.length > 0 ? globals.reqRecords[globals.reqRecords.length - 1] : null;
const currentDate = new Date().toDateString();
if (lastRecord) {
const lastDate = new Date(lastRecord.timestamp).toDateString();
console.log("currentDate: ", currentDate);
@@ -242,39 +242,48 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
&& !path.startsWith('/api/ai')) {
log("info", `[Path Check] Starting path normalization for: "${path}"`);
const pathBeforeCleanup = path; // 保存清理前的路径检查是否修改
// 1. 清理:应对"用户填写/api/v2"+"客户端添加/api/v2"导致的重复前缀
// 清理:应对"用户填写/api/v2"+"客户端添加/api/v2"导致的重复前缀
path = path.replace(/\/+/g, '/');
while (path.startsWith('/api/v2/api/v2/')) {
log("info", `[Path Check] Found redundant /api/v2 prefix. Cleaning...`);
// 从第二个 /api/v2 的位置开始截取,相当于移除第一个
path = path.substring('/api/v2'.length);
}
// 打印日志:只有在发生清理时才显示清理后的路径,否则显示"无需清理"
if (path !== pathBeforeCleanup) {
log("info", `[Path Check] Path after cleanup: "${path}"`);
} else {
log("info", `[Path Check] Path after cleanup: No cleanup needed.`);
}
// 2. 补全:如果路径缺少前缀(例如请求原始路径为 /search/anime),则补全
// 补全:如果路径缺少前缀(例如请求原始路径为 /search/anime 或 /v2/search/anime),则智能补全
const pathBeforePrefixCheck = path;
if (!path.startsWith('/api/v2') && path !== '/' && !path.startsWith('/api/logs')
&& !path.startsWith('/api/env') && !path.startsWith('/api/cache')
&& !path.startsWith('/api/cookie') && !path.startsWith('/api/config')
&& !path.startsWith('/api/ai')) {
log("info", `[Path Check] Path is missing /api/v2 prefix. Adding...`);
path = '/api/v2' + path;
if (path.startsWith('/v2/') || path === '/v2') {
log("info", `[Path Check] Path is missing /api prefix. Adding /api...`);
path = '/api' + path;
} else if (path.startsWith('/api/') || path === '/api') {
log("info", `[Path Check] Path is missing /v2 prefix. Adding /v2...`);
path = '/api/v2' + path.substring(4);
} else {
log("info", `[Path Check] Path is missing /api/v2 prefix. Adding /api/v2...`);
path = '/api/v2' + (path.startsWith('/') ? path : '/' + path);
}
}
// 打印日志:只有在发生添加前缀时才显示添加后的路径,否则显示"无需补全"
if (path === pathBeforePrefixCheck) {
log("info", `[Path Check] Prefix Check: No prefix addition needed.`);
}
log("info", `[Path Check] Final normalized path: "${path}"`);
}
// GET /
if (path === "/" && method === "GET") {
return handleUI();
@@ -424,7 +433,7 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
// 从请求体获取segment数据
const requestBody = await req.json();
let segment;
// 尝试解析JSON
try {
segment = Segment.fromJson(requestBody);
@@ -483,7 +492,7 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
}
// ========== Cookie 管理 API ==========
// GET /api/cookie/status - 获取Cookie状态
if (path === "/api/cookie/status" && method === "GET") {
return handleCookieStatus();