fix: 完善跨季匹配与平台过滤逻辑,修复备用匹配状态及集标题解析 (#392)

* fix: 跨季扩展阻塞与备用匹配 isMatched 状态修复

checkEpisodeSatisfied 指定平台无匹配数据时返回 false:
指定目标平台(如 qiyi)但搜索结果均不匹配该平台时,不再误判为满足。
新增 anyPlatformHadData 标记,无匹配平台时返回 false 以触发跨季扩展。

备用匹配成功后设置 isMatched:
fallbackMatchAniAndEp 成功匹配剧集后 isMatched 未正确置为 true,
导致客户端收到匹配结果但标记为未匹配。

* fix: checkEpisodeSatisfied 优选平台无匹配时回退到不区分平台重查而非直接返回false

当 PLATFORM_ORDER 指定的平台与搜索结果源不匹配时(例如 PLATFORM_ORDER=qiyi 但搜索结果来自 renren),
anyPlatformHadData 为 false,原先直接返回 false 会错误触发跨季扩展(S5~S10)和 spillover 匹配,
造成大量无谓 API 请求。

修复:anyPlatformHadData 为 false 且有 targetPlatform 时,以 targetPlatform=null 递归调用自身,
回退到 _any_ 模式重新查验全部可用数据,再依据集数是否满足决定是否触发扩展。
这同时正确覆盖两种场景:
- 当前季集数不足(如 S1E31 仅 11 集)→ 正确触发扩展
- 当前季集数充足但平台不匹配(如 S4E3 在 renren 有 24 集且含 E3)→ 正确抑制扩展

* fix: matchAniAndEp与findCrossSeasonEpisodeMap中平台不匹配源时回退到无平台过滤

当PLATFORM_ORDER与搜索结果源不匹配时(例如PLATFORM_ORDER=renren但搜到iqiyi结果):
1. matchAniAndEp: findEpisodeByNumber因平台过滤丢弃所有集数返回null后,
   新增回退:检查候选源是否匹配指定平台,不匹配则以platform=null重试。
   这避免了bestRes为空而错误触发findCrossSeasonEpisodeMap spillover。
2. findCrossSeasonEpisodeMap: 平台不匹配时以currentSeason++ + continue
   替代break,允许跨季映射在后续匹配季中继续查找而非直接退出。

保护措施:isPreferredAnime的原有回退逻辑保留作为最后一层兜底。

* fix: extractEpisodeNumberFromTitle 支持第X话和第X回格式

此前仅识别 /第(\d+)集/,导致 dandan 等源使用第31话格式的集标题
无法被提取集号,进而 findEpisodeByNumber 对所有分季 dandan 条目返回 null。
跨季扩展虽然正确拉取了 S3 dandan 条目的 E31(第31话 决裂),但
extractEpisodeNumberFromTitle 无法提取数字 31,导致匹配被跳过,
最终落在能识别第31集的 bahamut 不分季条目上。

修复将正则扩展为 /第(\d+)[集话回]/,同时覆盖:
- 集:标准中文格式(如第31集)
- 话:日式动画格式(如第31话)
- 回:旧式分回格式(如第12回)
不匹配 期/章/卷,避免错配综艺/漫画/OVA分卷格式。

* fix: titleMatches 对无季号标题放宽季数守卫,避免不分季长剧被全部拦截

titleMatches 在 querySeason>1 时原先对无季号标题作 (null||1)!=querySeason
拦截,导致 iqiyi 搜索航海王 S2E62 时全部 16 个结果因标题中无季号被过滤,
searchData 为空,匹配链路崩溃。

修复:仅当源标题明确含季号且与目标季不一致时才拦截,无季号的不分季长剧
交由上游源处理器(如 iqiyi 季数预过滤)决定是否保留。

影响分析:
- 不分季长剧(querySeason>1):修复前被拦截 → 修复后通过 → 正确
- 分季剧S1标题(querySeason=2):修复前拦截 → 修复后仍拦截 → 不变
- 分季剧S2标题(querySeason=2):修复前通过 → 修复后仍通过 → 不变
- 分季剧S3标题(querySeason=2):修复前拦截 → 修复后仍拦截 → 不变
- querySeason=1或null:不受此分支影响,原有行为完全保持
This commit is contained in:
Elegy233 authored and GitHub committed 2026-07-26 21:04:12 +08:00
1 parent f4d6e5781b
commit d4d4c9fffa
2 files changed
+28 -7

No files matched your search

+23 -3
View File
@@ -252,6 +252,7 @@ function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAni
}
let allSatisfied = true;
let anyPlatformHadData = false;
for (const tPlat of targetPlatforms) {
let isEpisodeSatisfied = false;
@@ -296,8 +297,9 @@ function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAni
}
if (!platformHasData) {
continue;
continue;
}
anyPlatformHadData = true;
if (!isEpisodeSatisfied) {
let totalValidEpisodes = 0;
@@ -311,6 +313,11 @@ function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAni
}
}
// 优先平台无匹配数据时回退到不区分平台重新检查全部可用数据
if (!anyPlatformHadData && targetPlatform) {
return checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAnimeDetailsMap, null);
}
return allSatisfied;
}
@@ -1220,7 +1227,8 @@ function findCrossSeasonEpisodeMap(searchData, title, year, season, episode, pla
if (absoluteMatch) {
if (platform && getPlatformMatchScore(extractEpisodeTitle(absoluteMatch.episodeTitle), platform) === 0) {
break;
currentSeason++;
continue;
}
log("info", `[system] [spillover] 跨季溢出查找命中 (按绝对标题数字) -> 所在季:S${currentSeason} 集标题:${absoluteMatch.episodeTitle}`);
bestRes = {
@@ -1234,7 +1242,8 @@ function findCrossSeasonEpisodeMap(searchData, title, year, season, episode, pla
if (currentTargetEpisode <= allEps.length) {
const targetEp = allEps[currentTargetEpisode - 1];
if (platform && getPlatformMatchScore(extractEpisodeTitle(targetEp.episodeTitle), platform) === 0) {
break;
currentSeason++;
continue;
}
log("info", `[system] [spillover] 跨季溢出查找命中 (按相对排位计算) -> 所在季:S${currentSeason} 集标题:${targetEp.episodeTitle}`);
bestRes = {
@@ -1359,6 +1368,14 @@ async function matchAniAndEp(season, episode, year, searchData, title, req, plat
// 匹配集数
matchedEpisode = findEpisodeByNumber(filteredEpisodes, episode, targetEpisode, platform);
// 当指定平台与候选动画源不匹配导致过滤后无匹配时,回退到不区分平台提取集数
if (!matchedEpisode && platform) {
const actualAnimePlatform = extractPlatformFromTitle(anime.animeTitle) || anime.source;
if (getPlatformMatchScore(actualAnimePlatform, platform) === 0) {
matchedEpisode = findEpisodeByNumber(filteredEpisodes, episode, targetEpisode, null);
}
}
// 如果当前是用户的优选偏好,但由于平台配置限制导致未命中目标平台,则放宽条件无视平台限制提取集数
if (!matchedEpisode && isPreferredAnime) {
log("info", `[system] [match] 优选剧集未命中目标平台 ${platform},放宽条件提取集数`);
@@ -1745,6 +1762,9 @@ export async function matchAnime(url, req, clientIp) {
resAnime = __ret.resAnime;
// 跨季集数顺延映射的结果不更新 lastSearch,防止无关番剧污染 lastSelectMap 偏好记录
spilloverMatched = __ret.isSpillover;
if (resAnime) {
resData["isMatched"] = true;
}
}
}
+5 -4
View File
@@ -320,8 +320,9 @@ export function titleMatches(title, query, parsedSeason = null) {
const titleSeason = getExplicitSeasonNumber(titleText);
if (querySeason > 1) {
// 搜索指定续作(>1)时,标题必须明确包含该季度标识
if ((titleSeason || 1) !== querySeason) return false;
// 搜索指定续作(>1)时,仅当源标题明确包含季号时才校验季号一致性
// 源标题无季号的不分季长剧不应被此规则拦截,交由上游源处理器决定
if (titleSeason !== null && titleSeason !== querySeason) return false;
} else if (querySeason === 1) {
// 搜索第1季时,拦截明确标明为其他季度(如第2季、第3季)的结果
if (titleSeason !== null && titleSeason !== 1) return false;
@@ -452,8 +453,8 @@ export function extractSeasonNumberFromAnimeTitle(animeTitle) {
export function extractEpisodeNumberFromTitle(episodeTitle) {
if (!episodeTitle) return null;
// 匹配格式:第1集、第01集、第10集等
const chineseMatch = episodeTitle.match(/第(\d+)集/);
// 匹配格式:第1集、第01集、第1话、第1回等
const chineseMatch = episodeTitle.match(/第(\d+)[集话回]/);
if (chineseMatch) {
return parseInt(chineseMatch[1], 10);
}