From 70aa16356826279e166b67ce6f6a269d5e736784 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 16 Oct 2026 12:28:03 +0800 Subject: [PATCH] fix(search): reject punctuated English word fragments (#7019) --- app/application/torrent/download.py | 22 +++--- tests/test_torrent_title_matching.py | 111 +++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 tests/test_torrent_title_matching.py diff --git a/app/application/torrent/download.py b/app/application/torrent/download.py index 0d35edee9..998ce67d7 100644 --- a/app/application/torrent/download.py +++ b/app/application/torrent/download.py @@ -244,6 +244,12 @@ def _size_filter_failure(torrent: TorrentInfo, size_range: Optional[str]) -> Opt return f"资源大小不符合条件:{size_range}" if outside else None +def _title_fragments(value: str, separators: str) -> set[str]: + """先归一化再排除英文碎片,避免 Dream! 等片段绕过单词过滤。""" + fragments = {text_tools.normalize_upper(part) for part in re.split(separators, value)} - {""} + return {fragment for fragment in fragments if not text_tools.is_english_word(fragment)} + + class TorrentHelper: """ 种子帮助类 @@ -627,6 +633,9 @@ class TorrentHelper: def match_torrent(mediainfo: MediaInfo, torrent_meta: MetaBase, torrent: TorrentInfo) -> bool: """ 检查种子是否匹配媒体信息 + + 完整名称可匹配英文标题或别名;拆分片段归一化后排除英文单词和空值, + 防止标点把普通单词变成作品身份依据。 :param mediainfo: 需要匹配的媒体信息 :param torrent_meta: 种子识别信息 :param torrent: 种子信息 @@ -647,7 +656,7 @@ class TorrentHelper: text_tools.normalize_upper(mediainfo.original_title) } - {""} # 要匹配的媒体别名、译名 - media_names = {text_tools.normalize_upper(name) for name in mediainfo.names if name} + media_names = {text_tools.normalize_upper(name) for name in mediainfo.names if name} - {""} # 识别的种子中英文名 meta_names = { text_tools.normalize_upper(torrent_meta.cn_name), @@ -696,20 +705,15 @@ class TorrentHelper: return True # 标题拆分 if torrent_meta.org_string: - # 只拆分出标题中的非英文单词进行匹配,英文单词容易误匹配(带空格的多个单词组合除外) - titles = [text_tools.normalize_upper(t) for t in re.split( - r'[\s/【】.\[\]\-]+', - torrent_meta.org_string - ) if not text_tools.is_english_word(t)] + # 完整英文名称已在上方比较,拆分后的英文单词不能单独证明作品身份。 + titles = _title_fragments(torrent_meta.org_string, r'[\s/【】.\[\]\-]+') # 在标题中判断是否存在标题、原语种标题 if media_titles.intersection(titles): logger.info(f'{mediainfo.title} 通过标题匹配到资源:{torrent.site_name} - {torrent.title}') return True # 在副标题中(非英文单词)判断是否存在标题、原语种标题、别名、译名 if torrent.description: - subtitles = {text_tools.normalize_upper(t) for t in re.split( - r'[\s/【】|]+', - torrent.description) if not text_tools.is_english_word(t)} + subtitles = _title_fragments(torrent.description, r'[\s/【】|]+') if media_titles.intersection(subtitles) or media_names.intersection(subtitles): logger.info(f'{mediainfo.title} 通过副标题匹配到资源:{torrent.site_name} - {torrent.title},' f'副标题:{torrent.description}') diff --git a/tests/test_torrent_title_matching.py b/tests/test_torrent_title_matching.py new file mode 100644 index 000000000..32c190cde --- /dev/null +++ b/tests/test_torrent_title_matching.py @@ -0,0 +1,111 @@ +"""种子标题片段匹配回归,防止带标点的英文单词被当作完整作品名。""" + +from unittest.mock import Mock + +import pytest + +from app.application.torrent.download import TorrentHelper +from app.chain.search import SearchChain +from app.chain.search import result as search_module +from app.domain.context import MediaInfo, TorrentInfo +from app.domain.metainfo import MetaInfo +from app.schemas.types import MediaSource, MediaType + + +@pytest.fixture +def target() -> MediaInfo: + """构造与 Issue #7019 相同的待播剧身份及英文别名。""" + return MediaInfo( + media_source=MediaSource.TMDB, + media_id="279923", + tmdb_id=279923, + title="醉梦", + original_title="醉梦", + names=["Dream"], + type=MediaType.TV, + year="2026", + season_years={1: "2026"}, + seasons={1: [1]}, + ) + + +@pytest.mark.parametrize("fragment", ["Dream!", "Dream!", "(Dream)", "Dream?", "Dream。", "Dream\u200b"]) +@pytest.mark.parametrize("field", ["title", "description"]) +def test_english_fragment_cannot_match_alias_after_punctuation(target, fragment, field): + """英文单词附带标点或零宽字符时仍不能成为标题片段证据。""" + if field == "title": + target.original_title = "Dream" + torrent = TorrentInfo( + title=f"BanG {fragment} YUME MITA S01 1080p WEB-DL" if field == "title" + else "BanG Dream YUME MITA S01 1080p WEB-DL", + description=f"BanG {fragment} It's MyGO!!!!! | 01-13 日语+简繁字幕" + if field == "description" else None, + category=MediaType.TV.value, + ) + meta = MetaInfo(title=torrent.title, subtitle=torrent.description) + + assert not TorrentHelper.match_torrent(target, meta, torrent) + + +@pytest.mark.parametrize("description", ["BanG Dream! It's MyGO!!!!! | 01-13 日语+简繁字幕", "【】 | !!!"]) +def test_empty_normalized_alias_is_not_matching_evidence(target, description): + """空白或纯标点别名不能与副标题里的空片段相交。""" + target.names = [" ", "!!!", "\u200b"] + torrent = TorrentInfo(title="Unrelated S01 1080p WEB-DL", description=description) + + assert not TorrentHelper.match_torrent(target, MetaInfo(torrent.title), torrent) + + +@pytest.mark.parametrize( + ("title", "description"), + [ + ("醉梦 S01 2026 1080p WEB-DL", None), + ("Dream S01 2026 1080p WEB-DL", None), + ("Dream! S01 2026 1080p WEB-DL", None), + ("Other Title S01 2026 1080p WEB-DL", "【醉梦!】 | 国语中字"), + ("Other Title S01 2026 1080p WEB-DL 【醉梦!】", None), + ], +) +def test_complete_titles_and_chinese_fragments_still_match(target, title, description): + """完整英文别名和有效中文片段继续匹配,不受英文碎片过滤影响。""" + torrent = TorrentInfo(title=title, description=description) + meta = MetaInfo(title=title, subtitle=description) + + assert TorrentHelper.match_torrent(target, meta, torrent) + + +def test_complete_multiword_alias_still_matches(target): + """英文多词别名仍按完整解析名称比较。""" + target.names = ["Drunken Dream!"] + torrent = TorrentInfo(title="Drunken Dream S01 2026 1080p WEB-DL") + + assert TorrentHelper.match_torrent(target, MetaInfo(torrent.title), torrent) + + +@pytest.mark.parametrize("year", ["", "2026 "]) +def test_subscription_search_drops_bang_dream_candidates(target, monkeypatch, year): + """搜索结果在产生可下载候选之前排除带标点的 Dream 误匹配。""" + torrents = [ + TorrentInfo( + title=f"BanG Dream YUME MITA S01 {year}1080p friDay WEB-DL AAC2.0 H.264-CHDWEB", + description="BanG Dream! YUME∞MITA | 01-13 日语+简繁字幕", + category=MediaType.TV.value, + ), + TorrentInfo( + title="醉梦 S01 2026 1080p WEB-DL", + description="醉梦 | 国语中字", + category=MediaType.TV.value, + ), + ] + media_chain = Mock() + monkeypatch.setattr(search_module, "MediaChain", Mock(return_value=media_chain)) + monkeypatch.setattr(TorrentHelper, "sort_torrents", staticmethod(lambda contexts: contexts)) + + contexts = object.__new__(SearchChain)._parse_result( + torrents=torrents, + mediainfo=target, + rule_groups=[], + ) + + assert [context.torrent_info.title for context in contexts] == [torrents[1].title] + media_chain.recognize_by_meta.assert_not_called()