From c81f63993c44effc503f4481a0a886a56542cb18 Mon Sep 17 00:00:00 2001 From: mz289 <37443741+mz289@users.noreply.github.com> Date: Sat, 12 Sep 2026 07:50:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20fongmi=20=E6=8E=A5=E5=8F=A3=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=B9=E9=85=8D=E5=A2=9E=E5=8A=A0=E5=AD=A3=E6=95=B0?= =?UTF-8?q?=E6=AF=94=E5=AF=B9=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=B7=A8=E5=AD=A3?= =?UTF-8?q?=E5=90=8C=E5=8F=B7=E9=9B=86=E9=94=99=E9=85=8D=20(#471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: fongmi 接口自动匹配增加季数比对,修复跨季同号集错配 episode 文本中的季号此前被丢弃(S02E05 只取 E 后集数),第一季/第二季同号集得分完全相同,自动首条由源返回顺序决定:线上实测 庆余年 S01E01 与 S02E01 返回完全一致、S01E05 与 S02E05 亦逐字节相同。现从分集文本提取显式季号,与候选剧名的季标注比对:同季 +5000、跨季 -12000;任一侧无季标注不调整,电影/综艺日期场景保持原行为。 * Update worker.test.js --------- Co-authored-by: xiangyang Co-authored-by: huangxd- <649924757@qq.com> --- danmu_api/apis/clients/fongmi-api.js | 35 +++++++++++++++++++++++-- danmu_api/worker.test.js | 39 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/danmu_api/apis/clients/fongmi-api.js b/danmu_api/apis/clients/fongmi-api.js index c3aa2da..832e93f 100644 --- a/danmu_api/apis/clients/fongmi-api.js +++ b/danmu_api/apis/clients/fongmi-api.js @@ -2,7 +2,7 @@ import { globals } from "../../configs/globals.js"; import { jsonResponse } from "../../utils/http-util.js"; import { log } from "../../utils/log-util.js"; import { simplized } from "../../utils/zh-util.js"; -import { convertChineseNumber, extractEpisodeTitle, extractEpisodeNumberFromTitle, normalizeSpaces } from "../../utils/common-util.js"; +import { convertChineseNumber, extractEpisodeTitle, extractEpisodeNumberFromTitle, extractSeasonNumberFromAnimeTitle, getExplicitSeasonNumber, normalizeSpaces } from "../../utils/common-util.js"; import { filterSameEpisodeTitle, getBangumiDataForMatch, searchAnime } from "../dandan-api.js"; // ===================== @@ -157,6 +157,26 @@ function extractFongmiEpisodeNumber(episode) { return null; } +/** + * 从 FongMi 集数文本中提取显式季号。 + * 支持 S02E05 / 第2季第3集 / Season 2 / Part 2 / 2x05 等写法。 + * 仅接受 1~50 的合理季号,避免把综艺日期"第20180512期"等误判为季。 + * @param {string} episode 原始集数文本 + * @returns {number|null} 季号,无法识别时返回 null + */ +function extractFongmiSeasonNumber(episode) { + if (!episode) return null; + const text = String(episode); + const season = getExplicitSeasonNumber(text); + if (season !== null && Number.isFinite(season) && season >= 1 && season <= 50) return season; + const match = text.match(/(?:^|[\s_\-])(\d{1,2})x(\d{1,4})(?:$|[\s_\-.])/i); + if (match) { + const seasonNum = parseInt(match[1], 10); + if (seasonNum >= 1 && seasonNum <= 50) return seasonNum; + } + return null; +} + /** * 为 FongMi 标题生成搜索关键词列表。 * 先保留原始标题,再追加正则清洗后的标题作为回退搜索词。 @@ -276,7 +296,7 @@ async function parseFongmiRequestParams(url, req) { /** * 计算单个候选分集与目标分集的匹配得分。 - * 当前综合标题、集数、日期做排序,后续可以继续扩展更多维度。 + * 当前综合标题、季数、集数、日期做排序,后续可以继续扩展更多维度。 * @param {Object} anime 候选剧集对象 * @param {Object} episode 候选分集对象 * @param {string} targetEpisode FongMi 传入的分集文本 @@ -305,6 +325,15 @@ function scoreFongmiEpisodeMatch(anime, episode, targetEpisode, index) { if (targetNum !== null && episodeNum !== null && targetNum === episodeNum) score += 7000; if (targetNum !== null && Number.isFinite(episodeIndexNum) && targetNum === episodeIndexNum) score += 4000; + // 季数比对:目标与候选都带显式季号时严格区分(如"剧名 S02E05"应优先命中"第二季"条目)。 + // 集数加分对跨季同号集完全同分,胜负此前只由源返回顺序决定;这里同季加分、跨季重罚, + // 保证跨季候选排到任何同季候选之后。任一侧无季标注则不调整,保持原有行为。 + const targetSeason = extractFongmiSeasonNumber(targetEpisode); + const animeSeason = extractSeasonNumberFromAnimeTitle(anime?.animeTitle || "").season; + if (targetSeason !== null && animeSeason !== null) { + score += targetSeason === animeSeason ? 5000 : -12000; + } + const targetDate = extractDateDigits(normalizedTargetEpisode); const episodeDate = extractDateDigits(episodeTitle); if (targetDate && episodeDate && targetDate === episodeDate) score += 9000; @@ -418,3 +447,5 @@ export async function getFongmiDanmaku(url, req) { log("info", `[system] [fongmi] name=${name}, episode=${episode}, candidates=${items.length}`); return jsonResponse(items, 200); } + +export { extractFongmiEpisodeNumber, extractFongmiSeasonNumber, scoreFongmiEpisodeMatch }; diff --git a/danmu_api/worker.test.js b/danmu_api/worker.test.js index c0e20db..5a67a31 100644 --- a/danmu_api/worker.test.js +++ b/danmu_api/worker.test.js @@ -45,6 +45,7 @@ import { convertToDanmakuJson, handleDanmusLike, splitBlockedWords, parseBlocked import { Segment, SegmentListResponse } from "./models/dandan-model.js" import { initBangumiData, searchBangumiData, clearBangumiDataCache, dedupeBangumiSearchResults } from "./utils/bangumi-data-util.js"; import { generateNipaplaySignature, parseNipaplayRelatedLinks, resolveNipaplayLink, applyShiftToDanmu } from "./utils/nipaplay-util.js"; +import { extractFongmiSeasonNumber, scoreFongmiEpisodeMatch } from "./apis/clients/fongmi-api.js"; import { localDanmuJsContent } from './ui/js/localdanmu.js'; import { buildLocalDanmuResourceKey, groupLocalDanmuResources, parseLocalDanmu, normalizeLocalSeason } from './utils/local-danmu-parser.js'; import { handleLocalDanmuUpload, handleLocalDanmuList, handleLocalDanmuDelete, handleLocalDanmuGet } from './apis/local-danmu-api.js'; @@ -3124,9 +3125,47 @@ test('worker.js API endpoints', async (t) => { // assert.strictEqual(applyShiftToDanmu(null, 5), null, '空对象直接返回'); // }); +test('fongmi-api season aware scoring', () => { + // 季号提取: SxxExx / 第x季 / Season N / 2x05; 综艺日期与纯集数不误判 + assert.equal(extractFongmiSeasonNumber('人生切割术 S02E05'), 2); + assert.equal(extractFongmiSeasonNumber('Show.S02.E05.2160p.WEB-DL.mkv'), 2); + assert.equal(extractFongmiSeasonNumber('庆余年 第2季第03集'), 2); + assert.equal(extractFongmiSeasonNumber('Show Season 3 EP01'), 3); + assert.equal(extractFongmiSeasonNumber('剧名 2x05'), 2); + assert.equal(extractFongmiSeasonNumber('1920x1080'), null); + assert.equal(extractFongmiSeasonNumber('凡人修仙传 第01集'), null); + assert.equal(extractFongmiSeasonNumber('奔跑吧 第20180512期'), null); + assert.equal(extractFongmiSeasonNumber(''), null); + const mk = (animeTitle, episodeTitle, index) => ({ anime: { animeTitle }, episode: { episodeTitle }, index }); + const scoreOf = (c, target) => scoreFongmiEpisodeMatch(c.anime, c.episode, target, c.index); + // 跨季同号集: 集数加分对第一/二季完全同分(11196), 修复后第二季必须稳定胜出, 不再由源返回顺序决定 + const targetS2 = '人生切割术 S02E05'; + const s2e5 = mk('人生切割术 第二季(2025)【电视剧】from renren', '【renren】 第05集', 4); + const s1e5 = mk('人生切割术 第一季(2022)【电视剧】from renren', '【renren】 第05集', 4); + assert.equal(scoreOf(s2e5, targetS2), 196 + 7000 + 4000 + 5000); + assert.equal(scoreOf(s1e5, targetS2), 196 + 7000 + 4000 - 12000); + // 候选枚举顺序翻转也不影响自动首条 + for (const ordered of [[s1e5, s2e5], [s2e5, s1e5]]) { + const best = ordered.map(c => ({ ...c, score: scoreOf(c, targetS2) })) + .sort((a, b) => b.score - a.score)[0]; + assert.ok(best.anime.animeTitle.includes('第二季'), 'S02E05 自动首条必须是第二季'); + } + // 目标 S01 时同样必须回到第一季 + const best = [s1e5, s2e5].map(c => ({ ...c, score: scoreOf(c, '人生切割术 S01E05') })) + .sort((a, b) => b.score - a.score)[0]; + assert.ok(best.anime.animeTitle.includes('第一季'), 'S01E05 自动首条必须是第一季'); + + // 目标带季但候选剧名无季标注: 不调整, 保持原有行为 + const anon = mk('人生切割术(2022)【韩剧】from hanjutv', '【hanjutv】 第5集', 4); + assert.equal(scoreOf(anon, targetS2), 196 + 7000 + 4000); + + // 目标无季标注: 完全不受影响(向后兼容); 文本包含加分(+4500)为原有行为 + const plain = mk('凡人修仙传', '第05集', 4); + assert.equal(scoreOf(plain, '凡人修仙传 第05集'), 196 + 7000 + 4000 + 4500); +}); const comment = '标题警告‼️ 中文弹幕 😀 \uFFFD'; const json = JSON.stringify({ count: 1, comments: [{ p: '1.00,1,16777215,[qiyi]', m: comment }] }, null, 2);