fix: 跨季扩展按平台满足度过滤源并修复恶性重复请求问题 (#408)
* fix: 跨季扩展按平台满足度过滤源并修复恶性重复请求问题 checkEpisodeSatisfied不再在首个不满足平台处break,改为遍历所有平台 通过新参数unsatisfiedOut记录不满足的平台集合。跨季扩展据此仅处理不满足 平台对应源,已满足者直接跳过不再被牵连重复调用handleAnimes。 跨季执行按源分组并发:源间Promise.all并行,同源内各季顺序执行确保 模块级缓存传播。源处理顺序遵循sourceOrderArr。 titleMatches新增threshold参数(默认0.8),maxSeason传入0.6放宽相似度 阈值使与查询翻译差异较大的结果仍能通过匹配参与季号提取。 * fix: 修复跨季场景下身份平台名与优选平台名不一致时的匹配失效 跨季分支 (PR #408) 的 checkEpisodeSatisfied 与 spillover 评分在判断某源是否为目标平台提供数据时,仅依据番剧身份(标题 from 段或 source),未并入命中集所挂的平台标签。当源身份名与配置平台名不一致、但集上挂有目标平台标签时(如 tencent 源、集标题挂 qq),该源被误判为「目标平台无数据」而跳过,导致配置 qq 却无法命中 tencent 源。 现于两处评分前,将候选平台由「身份标签」扩展为「身份标签与所挂集标签去重合并」(与 matchAniAndEp 的 1421 修复口径一致),使身份名(如 tencent)与优选平台名(如 qq)不一致、但集上挂有该标签的源也能被正确识别。该加性改动不影响既有的合并源优选评分(已以 21 例用例验证,含 tencent/qq 受影响案例与正常单源、身份命中、无平台、去重等不受影响案例)。 * fix: 修复未满足平台集合命名空间错配导致跨季扩展被跳过 问题:checkEpisodeSatisfied 在目标平台未满足时向 unsatisfiedOut 写入规范平台名(tPlat),而跨季扩展源过滤分支以源名(source)查询该集合,命名空间不一致使 PLATFORM_ORDER 模式下跨季扩展被静默整体跳过。 修复:在写入端就近收集提供该平台数据的 anime.source(providedSources),未满足时写入源名而非规范名,复用既有候选平台判定(actualPlatform),无新增映射表;仅当某 tPlat 子遍历确有数据且未满足时才写入对应源名,无关源不会被污染。 验证:新增 cross-season-unsatisfied-test.mjs 覆盖修复前后对比(tencent/qq、iqiyi/qiyi 受影响案例)、常规流程(首季命中/_any_/源名即规范名)与边界(多平台仅未满足源扩展、无关源不污染集合、主返回值契约不变)共 36 项全通过;既有跨季平台匹配测试 21/21 不受影响。
This commit is contained in:
1 parent
f19a5d9c4c
commit
e8f22b621f
2 files changed
+46
-19
No files matched your search
@@ -239,7 +239,7 @@ export function matchSeason(anime, queryTitle, season) {
|
||||
* @param {string|null} targetPlatform 期望优先验证的目标平台
|
||||
* @returns {boolean} 是否满足需求
|
||||
*/
|
||||
function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAnimeDetailsMap, targetPlatform) {
|
||||
function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAnimeDetailsMap, targetPlatform, unsatisfiedOut = null) {
|
||||
if (queryEpisode === null || querySeason === null) return true;
|
||||
|
||||
let targetPlatforms = [];
|
||||
@@ -258,17 +258,29 @@ function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAni
|
||||
let isEpisodeSatisfied = false;
|
||||
const seasonCapacities = new Map();
|
||||
let platformHasData = false;
|
||||
const providedSources = new Set();
|
||||
|
||||
for (const anime of animesList) {
|
||||
const actualPlatform = extractPlatformFromTitle(anime.animeTitle) || anime.source;
|
||||
// 候选平台由番剧身份标签(标题 from 段或 source)与所挂集标签共同决定,使身份名(如tencent)与优选平台名(如qq)不一致但集上挂有目标标签的源也能被正确识别为该平台有数据
|
||||
const identityPlatform = extractPlatformFromTitle(anime.animeTitle) || anime.source;
|
||||
const bData = getBangumiDataForMatch(anime, requestAnimeDetailsMap);
|
||||
const epPlatforms = new Set();
|
||||
if (bData?.success && bData.bangumi?.episodes) {
|
||||
for (const ep of bData.bangumi.episodes) {
|
||||
const epPlat = extractEpisodeTitle(ep.episodeTitle);
|
||||
if (epPlat) epPlatforms.add(epPlat);
|
||||
}
|
||||
}
|
||||
const actualPlatform = [...new Set([identityPlatform, ...epPlatforms].filter(Boolean)
|
||||
.flatMap(p => p.split(/[&&]/).map(s => s.trim().toLowerCase())).filter(s => s))].join('&');
|
||||
|
||||
if (tPlat !== '_any_' && getPlatformMatchScore(actualPlatform, tPlat) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
platformHasData = true;
|
||||
providedSources.add(anime.source);
|
||||
|
||||
const bData = getBangumiDataForMatch(anime, requestAnimeDetailsMap);
|
||||
if (bData?.success && bData.bangumi?.episodes) {
|
||||
const validEps = bData.bangumi.episodes.filter(ep => !globals.episodeTitleFilter.test(ep.episodeTitle));
|
||||
const filtered = filterSameEpisodeTitle(validEps);
|
||||
@@ -311,7 +323,9 @@ function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAni
|
||||
}
|
||||
if (totalValidEpisodes < queryEpisode) {
|
||||
allSatisfied = false;
|
||||
break;
|
||||
if (unsatisfiedOut) {
|
||||
for (const s of providedSources) unsatisfiedOut.add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,9 +810,10 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
|
||||
}
|
||||
|
||||
// 判断当前获取的季度是否已包含用户指定的集数
|
||||
const isEpisodeSatisfied = checkEpisodeSatisfied(curAnimes, querySeason, queryEpisode, requestAnimeDetailsMap, targetPlatform);
|
||||
const unsatisfiedPlatforms = new Set();
|
||||
const isEpisodeSatisfied = checkEpisodeSatisfied(curAnimes, querySeason, queryEpisode, requestAnimeDetailsMap, targetPlatform, unsatisfiedPlatforms);
|
||||
|
||||
// 若未包含且用户指定了季度,推导最大季并并行映射 S2、S3 等结果,合并同一次搜索下的后续季以辅助跨季匹配
|
||||
// 若未包含且用户指定了季度,推导最大季并扩展至后续季以辅助跨季匹配
|
||||
if (!isEpisodeSatisfied && querySeason !== null) {
|
||||
let maxSeason = querySeason;
|
||||
for (const source of globals.sourceOrderArr) {
|
||||
@@ -808,9 +823,9 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
|
||||
const list = (item && Array.isArray(item.list)) ? item.list : [item];
|
||||
for (const a of list) {
|
||||
if (!a) continue;
|
||||
// 仅从与查询相关的条目中提取季号,避免无关源的搜索结果污染 maxSeason
|
||||
// 用titleMatches过滤与查询无关的条目,仅从相关结果中提取季号
|
||||
const testTitle = a.animeTitle || a.title || a.name || a.name_cn || "";
|
||||
if (testTitle && !titleMatches(testTitle, queryTitle, null, true)) continue;
|
||||
if (testTitle && !titleMatches(testTitle, queryTitle, null, true, 0.6)) continue;
|
||||
const s = extractSeasonNumberFromAnimeTitle(testTitle).season;
|
||||
if (s !== null && s > maxSeason) maxSeason = s;
|
||||
}
|
||||
@@ -821,18 +836,25 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
|
||||
if (maxSeason > querySeason) {
|
||||
log("info", `[system] [LogVar-API] Episode ${queryEpisode} not satisfied in Season ${querySeason}. Parallel mapping to S${querySeason + 1}~S${maxSeason}...`);
|
||||
const expandPromises = [];
|
||||
for (let s = querySeason + 1; s <= maxSeason; s++) {
|
||||
for (const source of globals.sourceOrderArr) {
|
||||
if (!resultData[source]) continue;
|
||||
// 在PLATFORM_ORDER模式下,跳过已满足平台的对应源
|
||||
if (targetPlatform && !unsatisfiedPlatforms.has(source)) continue;
|
||||
// 源间并发、源内顺序,防止同源并发导致模块级缓存竞态
|
||||
expandPromises.push((async () => {
|
||||
const seasonAnimes = [];
|
||||
await executeSourceHandlers(resultData, queryTitle, seasonAnimes, requestAnimeDetailsMap, s, preferAnimeId, preferSource);
|
||||
// 缓存额外季度的结果
|
||||
if (seasonAnimes.length > 0) {
|
||||
setSearchCache(`${queryTitle}_S${s}`, seasonAnimes.map(({ links, ...pureAnime }) => pureAnime), requestAnimeDetailsMap);
|
||||
const sourceResults = [];
|
||||
for (let s = querySeason + 1; s <= maxSeason; s++) {
|
||||
const seasonAnimes = [];
|
||||
await executeSourceHandlers({ [source]: resultData[source] }, queryTitle, seasonAnimes, requestAnimeDetailsMap, s, preferAnimeId, preferSource);
|
||||
if (seasonAnimes.length > 0) {
|
||||
setSearchCache(`${queryTitle}_S${s}`, seasonAnimes.map(({ links, ...pureAnime }) => pureAnime), requestAnimeDetailsMap);
|
||||
}
|
||||
sourceResults.push(seasonAnimes);
|
||||
}
|
||||
return seasonAnimes;
|
||||
return sourceResults;
|
||||
})());
|
||||
}
|
||||
const expandedResults = await Promise.all(expandPromises);
|
||||
const expandedResults = (await Promise.all(expandPromises)).flat();
|
||||
for (const res of expandedResults) {
|
||||
curAnimes.push(...res);
|
||||
}
|
||||
@@ -1456,10 +1478,15 @@ async function matchAniAndEp(season, episode, year, searchData, title, req, plat
|
||||
if (!bestRes.episode && season && episode) {
|
||||
const spilloverRes = findCrossSeasonEpisodeMap(searchData, title, year, season, episode, platform, detailStore);
|
||||
if (spilloverRes.resEpisode) {
|
||||
// 候选平台由番剧身份标签与命中集所挂平台标签共同决定,与 matchAniAndEp 评分口径保持一致
|
||||
const spillIdentity = extractPlatformFromTitle(spilloverRes.resAnime.animeTitle) || spilloverRes.resAnime.source;
|
||||
const spillEpPlatform = spilloverRes.resEpisode ? extractEpisodeTitle(spilloverRes.resEpisode.episodeTitle) : null;
|
||||
const spillCandidate = [...new Set([spillIdentity, spillEpPlatform].filter(Boolean)
|
||||
.flatMap(p => p.split(/[&&]/).map(s => s.trim().toLowerCase())).filter(s => s))].join('&');
|
||||
bestRes = {
|
||||
episode: spilloverRes.resEpisode,
|
||||
anime: spilloverRes.resAnime,
|
||||
score: platform ? getPlatformMatchScore(extractPlatformFromTitle(spilloverRes.resAnime.animeTitle) || spilloverRes.resAnime.source, platform) : 1
|
||||
score: platform ? getPlatformMatchScore(spillCandidate, platform) : 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ export function getExplicitSeasonNumber(text) {
|
||||
* @param {number|null} parsedSeason - 解析出的目标季度
|
||||
* @returns {boolean} 是否匹配
|
||||
*/
|
||||
export function titleMatches(title, query, parsedSeason = null, forceNonStrict = false) {
|
||||
export function titleMatches(title, query, parsedSeason = null, forceNonStrict = false, threshold = 0.8) {
|
||||
if (title == null || query == null) return false;
|
||||
|
||||
const titleText = String(title);
|
||||
@@ -351,7 +351,7 @@ export function titleMatches(title, query, parsedSeason = null, forceNonStrict =
|
||||
}
|
||||
}
|
||||
|
||||
return (matchCount / kw.length) > 0.8;
|
||||
return (matchCount / kw.length) > threshold;
|
||||
});
|
||||
|
||||
// 年份噪音兜底:前序策略均未命中时,尝试去年份后再次包含匹配
|
||||
|
||||
Reference in new issue
Block a user