diff --git a/backend/src/module/parser/title_parser.py b/backend/src/module/parser/title_parser.py index 49015b60..17013e88 100644 --- a/backend/src/module/parser/title_parser.py +++ b/backend/src/module/parser/title_parser.py @@ -456,7 +456,20 @@ class TitleParser: ) deterministic = parse_outcome.result if parse_outcome.engine == "classic": - deterministic = _project_classic_release(raw, deterministic) + projected = _project_classic_release(raw, deterministic) + if ( + projected is None + and deterministic is not None + and deterministic.episode is None + and deterministic.media_type is MediaType.UNKNOWN + and persistence_target(deterministic) is not None + ): + # 字幕组未标记特别篇/剧场版等字样的无集数单发资源(#1092): + # 旧版投影契约会整条拒绝,导致订阅静默漏抓。与 Preview 的 + # 准入策略对齐,按 eps_collect 整理集接纳;带集数的旧版 + # 拒绝(如小数集)不受影响。 + projected = deterministic + deterministic = projected if ( deterministic is None and parse_outcome.trace is not None diff --git a/backend/src/test/test_issue_bugs.py b/backend/src/test/test_issue_bugs.py index 7fb6565c..b5b8cece 100644 --- a/backend/src/test/test_issue_bugs.py +++ b/backend/src/test/test_issue_bugs.py @@ -555,3 +555,77 @@ class TestIssue1005SearchOfficialTitle: db = BangumiDatabase(db_session) result = await db.search_official_title("不存在的番剧") assert result is None + + +# --------------------------------------------------------------------------- +# Issue #1092: 字幕组未标记特别篇等字样时,无集数标题无法解析 +# https://github.com/EstrellaXD/Auto_Bangumi/issues/1092 +# +# LoliHouse 发布的 TV 特别篇(如 ONE PIECE HEROINES)标题中没有集数也没有 +# 特别篇/剧场版标记。Preview 引擎的准入策略按 eps_collect 整理集接纳这类 +# 带发布证据的单发资源,但 classic 引擎的旧版兼容投影会整条拒绝,导致 +# 默认配置下订阅静默漏抓。 +# --------------------------------------------------------------------------- + + +class TestIssue1092UnmarkedEpisodelessRelease: + """Issue #1092: classic admission now matches the Preview policy.""" + + REPORTED_TITLE = ( + "[LoliHouse] 海贼王:女英雄们的故事 / ONE PIECE HEROINES " + "[WebRip 1080p HEVC-10bit AAC][简繁内封字幕]" + ) + + async def test_classic_admits_reported_title_as_collect_bangumi(self): + from unittest.mock import patch + + from module.conf import settings + from module.models import Bangumi + from module.models.config import LLM + from module.parser import TitleParser + + with ( + patch.object(settings, "llm", LLM(enable=False)), + patch.object(settings.rss_parser, "engine", "classic"), + ): + result = await TitleParser.raw_parser(self.REPORTED_TITLE) + + assert isinstance(result, Bangumi) + assert result.official_title == "海贼王:女英雄们的故事" + assert result.group_name == "LoliHouse" + assert result.episode_type == "episode" + assert result.eps_collect is True + + async def test_classic_and_preview_agree_on_reported_title(self): + from unittest.mock import patch + + from module.conf import settings + from module.models import Bangumi + from module.models.config import LLM + from module.parser import TitleParser + + with ( + patch.object(settings, "llm", LLM(enable=False)), + patch.object(settings.rss_parser, "engine", "tokenizer"), + ): + preview_result = await TitleParser.raw_parser(self.REPORTED_TITLE) + + assert isinstance(preview_result, Bangumi) + assert preview_result.official_title == "海贼王:女英雄们的故事" + + async def test_classic_still_rejects_title_without_release_evidence(self): + from unittest.mock import patch + + from module.conf import settings + from module.models.config import LLM + from module.parser import TitleParser + + with ( + patch.object(settings, "llm", LLM(enable=False)), + patch.object(settings.rss_parser, "engine", "classic"), + ): + result = await TitleParser.raw_parser( + "Random Non Matching Title No Digits At All" + ) + + assert result is None diff --git a/backend/src/test/test_title_parser.py b/backend/src/test/test_title_parser.py index e944dfd9..450069ca 100644 --- a/backend/src/test/test_title_parser.py +++ b/backend/src/test/test_title_parser.py @@ -233,14 +233,8 @@ class TestConfiguredParserEngine: assert result is None - @pytest.mark.parametrize( - "raw", - ( - "[Group] Anime [1080p]", - "[Group] Anime EP07.5 [1080p]", - ), - ) - async def test_classic_preserves_legacy_compatibility_rejections(self, raw): + async def test_classic_preserves_legacy_compatibility_rejections(self): + raw = "[Group] Anime EP07.5 [1080p]" parsed = parse_classic_release_title(raw) assert parsed is not None @@ -254,6 +248,24 @@ class TestConfiguredParserEngine: assert result is None + async def test_classic_admits_episodeless_release_with_evidence(self): + """无集数且无类型标记但带发布证据的单发资源(#1092):旧版 Episode + 投影仍拒绝(冻结契约),但 classic 准入与 Preview 策略对齐后放行。""" + raw = "[Group] Anime [1080p]" + parsed = parse_classic_release_title(raw) + + assert parsed is not None + assert to_legacy_episode(parsed) is None + + with ( + patch.object(settings, "llm", LLM(enable=False)), + patch.object(settings.rss_parser, "engine", "classic"), + ): + result = await TitleParser.raw_parser(raw) + + assert isinstance(result, Bangumi) + assert result.eps_collect is True + async def test_classic_projects_admitted_result_through_legacy_episode(self): raw = "[Group] Anime - 01 [2024][1080p]" parsed = parse_classic_release_title(raw)