feat: 完善集数定位模板识别与自动推荐稳定性 (#5978)

This commit is contained in:
Album
2026-06-21 07:34:57 +08:00
committed by GitHub
parent 970f2cf1ca
commit b395d820d8
2 changed files with 524 additions and 30 deletions
+249 -1
View File
@@ -68,6 +68,39 @@ def test_locate_episode_supports_hash_prefix():
assert file_name[start - 1] == "#"
@pytest.mark.parametrize(
"file_name",
[
"Show 01《Title》.mkv",
"Show 01〈Title〉.mkv",
"Show 01Title.mkv",
],
)
def test_locate_episode_supports_east_asian_title_quotes(file_name: str):
helper = EpisodeFormatRuleHelper()
start, end = helper._locate_episode(file_name, "01")
assert file_name[start:end] == "01"
def test_auto_recommend_supports_episode_before_japanese_quote():
helper = EpisodeFormatRuleHelper()
samples = [
_make_file("[U2-Rip]バカとテストと召喚獣 01「バカとクラスと召喚戦爭」(BD 1920x1080 x264 FLACx2).mkv"),
_make_file("[U2-Rip]バカとテストと召喚獣 02「ユリとバラと保健体育」(BD 1920x1080 x264 FLACx2).mkv"),
_make_file("[U2-Rip]バカとテストと召喚獣 03「食費とデートとスタンガン」(BD 1920x1080 x264 FLACx2).mkv"),
]
state, errmsg, data = helper.recommend([], samples)
assert state is True
assert errmsg == ""
assert data["episode_format"] == "[U2-Rip]バカとテストと召喚獣 {ep}{a}」(BD 1920x1080 x264 FLACx2).mkv"
assert data["sample_count"] == 3
assert data["majority_count"] == 3
def test_auto_recommend_returns_low_confidence_for_single_sample():
helper = EpisodeFormatRuleHelper()
sample = _make_file("[Seed-Raws] Tari Tari - 01 (BD 1280x720 AVC AAC).mp4")
@@ -200,7 +233,7 @@ def test_auto_recommend_returns_false_when_parse_raises(monkeypatch):
def _raise_parse(*args, **kwargs):
raise ValueError("broken parse")
monkeypatch.setattr("app.helper.format.parse.parse", _raise_parse)
monkeypatch.setattr("app.helper.format._match_template", _raise_parse)
state, errmsg, data = helper.recommend([], samples)
@@ -350,6 +383,47 @@ def test_auto_recommend_ignores_special_sp_samples():
assert data["episode_format"] == "[Tonikaku Kawaii S2][{ep}][BDRIP][1080P][H264_FLAC].mkv"
def test_auto_recommend_ignores_promotional_special_samples():
helper = EpisodeFormatRuleHelper()
sample_names = [
"[VCB-Studio] Show [01][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [02][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [03][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [PV01][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [CM01][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [Trailer][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [Web Preview 02][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [Series Review][Ma10p_1080p][x265_flac].mkv",
"[VCB-Studio] Show [Table Game][Ma10p_1080p][x265_flac].mkv",
]
samples = [_make_file(name) for name in sample_names]
state, errmsg, data = helper.recommend([], samples)
assert state is True
assert errmsg == ""
assert data["sample_count"] == 3
assert data["majority_count"] == 3
assert data["episode_format"] == "[VCB-Studio] Show [{ep}][Ma10p_1080p][x265_flac].mkv"
def test_auto_recommend_rejects_when_valid_media_coverage_is_too_low():
helper = EpisodeFormatRuleHelper()
sample_names = [
"Show - 01.mkv",
"Show - 02.mkv",
"Show bonus.mkv",
"Show extra.mkv",
]
samples = [_make_file(name) for name in sample_names]
state, errmsg, data = helper.recommend([], samples)
assert state is False
assert errmsg == "有效正片样本覆盖率不足,建议补充集数定位规则"
assert data is None
def test_auto_recommend_uses_native_episode_as_fallback(monkeypatch):
helper = EpisodeFormatRuleHelper()
samples = [
@@ -403,6 +477,180 @@ def test_auto_recommend_rejects_when_native_episode_conflicts(monkeypatch):
assert data is None
def test_auto_recommend_degrades_native_conflict_for_title_sequence_number(monkeypatch):
helper = EpisodeFormatRuleHelper()
samples = [
_make_file("[VCB-Studio] Getsuyoubi no Tawawa 2 [01][Ma10p_1080p][x265_flac].mkv"),
_make_file("[VCB-Studio] Getsuyoubi no Tawawa 2 [02][Ma10p_1080p][x265_flac].mkv"),
]
monkeypatch.setattr(
helper,
"_extract_native_episode",
lambda item: "2",
)
state, errmsg, data = helper.recommend([], samples)
assert state is True
assert errmsg == ""
assert data["native_conflict_count"] == 0
assert data["episode_format"] == (
"[VCB-Studio] Getsuyoubi no Tawawa 2 [{ep}][Ma10p_1080p][x265_flac].mkv"
)
def test_auto_recommend_prefers_bracket_episode_over_title_sequence_native(monkeypatch):
helper = EpisodeFormatRuleHelper()
samples = [
_make_file("[VCB-Studio] Kono Subarashii Sekai ni Shukufuku wo! 3 [01][Ma10p_1080p][x265_flac_aac].mkv"),
_make_file("[VCB-Studio] Kono Subarashii Sekai ni Shukufuku wo! 3 [02][Ma10p_1080p][x265_flac_aac].mkv"),
]
monkeypatch.setattr(
"app.helper.format.anitopy.parse",
lambda _: {},
)
monkeypatch.setattr(
helper,
"_extract_native_episode",
lambda item: "3",
)
state, errmsg, data = helper.recommend([], samples)
assert state is True
assert errmsg == ""
assert data["native_conflict_count"] == 0
assert data["episode_format"] == (
"[VCB-Studio] Kono Subarashii Sekai ni Shukufuku wo! 3 [{ep}][Ma10p_1080p][x265_flac_aac].mkv"
)
def test_auto_recommend_corrects_anitopy_title_sequence_bias(monkeypatch):
helper = EpisodeFormatRuleHelper()
samples = [
_make_file(f"[VCB-Studio] Kono Subarashii Sekai ni Shukufuku wo! 3 [{index:02d}][Ma10p_1080p][x265_flac_aac].mkv")
for index in range(1, 12)
]
def _mock_parse(file_name: str):
episode_text = file_name.split("[", 2)[2].split("]", 1)[0]
if episode_text in {"01", "02"}:
return {"episode_number": episode_text}
return {"episode_number": "3"}
monkeypatch.setattr(
"app.helper.format.anitopy.parse",
_mock_parse,
)
monkeypatch.setattr(
helper,
"_extract_native_episode",
lambda item: "3",
)
state, errmsg, data = helper.recommend([], samples)
assert state is True
assert errmsg == ""
assert data["majority_count"] == 11
assert data["episode_format"] == (
"[VCB-Studio] Kono Subarashii Sekai ni Shukufuku wo! 3 [{ep}][Ma10p_1080p][x265_flac_aac].mkv"
)
def test_auto_recommend_supports_optional_finale_suffix():
helper = EpisodeFormatRuleHelper()
sample_names = [
"[SHIGURE] Rakudai Kishi no Cavalry - 01 [1080p][AVC_Hi10p_FLAC].mkv",
"[SHIGURE] Rakudai Kishi no Cavalry - 02 [1080p][AVC_Hi10p_FLAC].mkv",
"[SHIGURE] Rakudai Kishi no Cavalry - 12 Fin [1080p][AVC_Hi10p_FLAC].mkv",
]
samples = [_make_file(name) for name in sample_names]
state, errmsg, data = helper.recommend([], samples)
assert state is True
assert errmsg == ""
assert data["episode_format"] == (
"[SHIGURE] Rakudai Kishi no Cavalry - {ep} {a}[1080p][AVC_Hi10p_FLAC].mkv"
)
parser = FormatParser(eformat=data["episode_format"])
for sample_name in sample_names:
assert parser.match(sample_name) is True
def test_should_prefer_fallback_episode_when_anitopy_hits_title_sequence():
helper = EpisodeFormatRuleHelper()
assert helper._should_prefer_fallback_episode(
"[VCB-Studio] Kono Subarashii Sekai ni Shukufuku wo! 3 [04][Ma10p_1080p][x265_flac_aac].mkv",
"3",
"04",
) is True
assert helper._should_prefer_fallback_episode(
"Show - 04.mkv",
"04",
"04",
) is False
def test_should_prefer_fallback_episode_preserves_anitopy_multi_episode_list():
helper = EpisodeFormatRuleHelper()
assert helper._should_prefer_fallback_episode(
"Show 3 [01][02].mkv",
["01", "02"],
"02",
) is False
def test_extract_episode_with_native_fallback_keeps_anitopy_range_list(monkeypatch):
helper = EpisodeFormatRuleHelper()
item = _make_file("Show - 01-02 [02].mkv")
monkeypatch.setattr(
"app.helper.format.anitopy.parse",
lambda _: {"episode_number": ["01", "02"]},
)
monkeypatch.setattr(
helper,
"_extract_native_episode",
lambda _: None,
)
normalized_episode, native_episode, used_native_fallback, native_verified = (
helper._extract_episode_with_native_fallback(item)
)
assert normalized_episode == "01-02"
assert native_episode is None
assert used_native_fallback is False
assert native_verified is False
def test_should_degrade_native_conflict_only_for_preceding_title_number():
helper = EpisodeFormatRuleHelper()
assert helper._should_degrade_native_conflict(
"[VCB-Studio] Getsuyoubi no Tawawa 2 [01][Ma10p_1080p][x265_flac].mkv",
"01",
"2",
) is True
assert helper._should_degrade_native_conflict(
"Show - 01.mkv",
"01",
"02",
) is False
def test_calculate_variable_span_keeps_optional_base_suffix_span():
helper = EpisodeFormatRuleHelper()
assert helper._calculate_variable_span(" Fin ", [""]) == (0, 5)
def test_auto_recommend_marks_native_verified_samples(monkeypatch):
helper = EpisodeFormatRuleHelper()
samples = [