Skip to content

Commit 17b3f4d

Browse files
authored
fix: 优化自动匹配映射表开放规则排序,同源季下按起始集数降序优先 (huangxd-#435)
调整 `resolveAutoMatchMapping` 中开放规则的排序逻辑:当多个开放规则指向同一源标题和季数时,优先匹配起始集数更大的规则(即更具体的区间),仅在相同起始集数时保持声明顺序。这解决了同季多个映射段重叠时规则选择错误的问题,并更新 README 说明文档,新增对应单元测试覆盖该场景。
1 parent 1b719f4 commit 17b3f4d

3 files changed

Lines changed: 28 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
465465
| STRICT_TITLE_MATCH | 【可选】是否启用严格标题匹配模式,默认为`false`(宽松模糊匹配),启用后只匹配标题开头或完全匹配的结果。例如:搜索"遮天"时,`false`会匹配"古惑仔3之只手遮天",`true`只匹配"遮天"、"遮天 第一季"等。可选值:`true`、`false` |
466466
| TITLE_TO_CHINESE | 【可选】是否在match自动匹配时将外语标题转换成中文标题,适用于网盘没有刮削的资源,默认值:false(不转换),说明:需配合TMDB_API_KEY使用 |
467467
| TITLE_MAPPING_TABLE | 【可选】剧名映射表,用于自动匹配时替换标题进行搜索,格式:原始标题->映射标题;原始标题->映射标题;... ,例如:"唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华" |
468-
| AUTO_MATCH_MAPPING_TABLE | 【可选】自动匹配映射表,仅作用于 `POST /api/v2/match`,多条规则用分号分隔。开放映射 `永生 S05E02 -> 永生 S01E58` 会在源第 5 季内按集数递增映射;有限范围 `永生 S05E02~03 -> 永生 S01E58~59` 只映射包含两端的等长范围。支持目标结果优选 `海贼王 S02E01 -> 航海王(1999)【动漫】 S01E62` 和平台优选 `航海王 S01E01 -> 航海王 S01E01 @qiyi`。同一输入优先采用有限范围规则,同类型冲突按配置顺序;整体优先级为当前源季手动偏好 > 本映射表 > `TITLE_MAPPING_TABLE` > 普通匹配,`default` 偏好不阻断映射。限定候选不可用时回退同目标标题,映射目标失败时按原始请求重新匹配。普通搜索、收藏缓存和弹幕时间偏移不受影响。 |
468+
| AUTO_MATCH_MAPPING_TABLE | 【可选】自动匹配映射表,仅作用于 `POST /api/v2/match`,多条规则用分号分隔。开放映射 `永生 S05E02 -> 永生 S01E58` 会在源第 5 季内按集数递增映射;同标题同季度可配置多个开放规则,后面起始集数的规则会从该集开始覆盖前面的规则,例如 `一念永恒 S01E53 -> 一念永恒 S02E01;一念永恒 S01E107 -> 一念永恒 S03E01`;有限范围 `永生 S05E02~03 -> 永生 S01E58~59` 只映射包含两端的等长范围。支持目标结果优选 `海贼王 S02E01 -> 航海王(1999)【动漫】 S01E62` 和平台优选 `航海王 S01E01 -> 航海王 S01E01 @qiyi`。同一输入优先采用有限范围规则,规则起始集数相同按配置顺序;整体优先级为当前源季手动偏好 > 本映射表 > `TITLE_MAPPING_TABLE` > 普通匹配,`default` 偏好不阻断映射。限定候选不可用时回退同目标标题,映射目标失败时按原始请求重新匹配。普通搜索、收藏缓存和弹幕时间偏移不受影响。 |
469469
| TITLE_NOISE_FILTER | 【可选】剧名杂音清理规则,按正则表达式清理搜索与匹配阶段的剧名杂音词(如`百花杀(真彩)`→`百花杀`),默认值如下,设为空值可禁用 |
470470
| ANIME_TITLE_SIMPLIFIED | 【可选】是否在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索,默认值:false(不转换),可选值:`true`、`false` |
471471
| BLOCKED_WORDS | 【可选】弹幕屏蔽词列表,默认为空,示例如下 |

danmu_api/utils/auto-match-mapping-util.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,18 @@ export function resolveAutoMatchMapping(rules, { title, season, episode }) {
135135
if (episodeNumber < rule.sourceStartEpisode) return false;
136136
return rule.sourceEndEpisode === null || episodeNumber <= rule.sourceEndEpisode;
137137
});
138-
matches.sort((left, right) => Number(right.bounded) - Number(left.bounded) || left.order - right.order);
138+
// Open rules describe a mapping from their source start episode onward.
139+
// When several such rules share a source title/season, the latest start
140+
// episode is the most specific transition point. Keep declaration order
141+
// only as the tie-breaker for rules with the same specificity.
142+
matches.sort((left, right) => {
143+
const boundedOrder = Number(right.bounded) - Number(left.bounded);
144+
if (boundedOrder !== 0) return boundedOrder;
145+
if (!left.bounded && left.sourceStartEpisode !== right.sourceStartEpisode) {
146+
return right.sourceStartEpisode - left.sourceStartEpisode;
147+
}
148+
return left.order - right.order;
149+
});
139150

140151
const rule = matches[0];
141152
if (!rule) return null;

danmu_api/worker.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,21 @@ test('worker.js API endpoints', async (t) => {
374374
assert.equal(resolveAutoMatchMapping(parsed.rules, { title: '测试', season: 1, episode: 2 }).targetEpisode, 20);
375375
});
376376

377+
await t.test('uses the latest open-rule transition for repeated source title and season', () => {
378+
const parsed = parseAutoMatchMappingRules([
379+
'一念永恒 S01E53->一念永恒 S02E01',
380+
'一念永恒 S01E107->一念永恒 S03E01',
381+
'一念永恒 S01E166->一念永恒 完结季 S01E01'
382+
].join(';'));
383+
384+
assert.equal(resolveAutoMatchMapping(parsed.rules, { title: '一念永恒', season: 1, episode: 52 }), null);
385+
assert.equal(resolveAutoMatchMapping(parsed.rules, { title: '一念永恒', season: 1, episode: 53 }).targetSeason, 2);
386+
assert.equal(resolveAutoMatchMapping(parsed.rules, { title: '一念永恒', season: 1, episode: 106 }).targetEpisode, 54);
387+
assert.equal(resolveAutoMatchMapping(parsed.rules, { title: '一念永恒', season: 1, episode: 107 }).targetSeason, 3);
388+
assert.equal(resolveAutoMatchMapping(parsed.rules, { title: '一念永恒', season: 1, episode: 165 }).targetEpisode, 59);
389+
assert.equal(resolveAutoMatchMapping(parsed.rules, { title: '一念永恒', season: 1, episode: 166 }).targetTitle, '一念永恒 完结季');
390+
});
391+
377392
await t.test('maps match input, honors qualifiers and manual season preference, then falls back to original', async () => {
378393
const originalSearch = TencentSource.prototype.search;
379394
const originalHandleAnimes = TencentSource.prototype.handleAnimes;

0 commit comments

Comments
 (0)