diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index ef51fd5..1068d33 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -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 diff --git a/danmu_api/sources/dandan.js b/danmu_api/sources/dandan.js index 17783f8..9c8a88a 100644 --- a/danmu_api/sources/dandan.js +++ b/danmu_api/sources/dandan.js @@ -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) => { diff --git a/danmu_api/utils/merge-util.js b/danmu_api/utils/merge-util.js index 674c0b5..3843548 100644 --- a/danmu_api/utils/merge-util.js +++ b/danmu_api/utils/merge-util.js @@ -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); diff --git a/danmu_api/worker.js b/danmu_api/worker.js index a21c031..62860d4 100644 --- a/danmu_api/worker.js +++ b/danmu_api/worker.js @@ -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();