fix(search): reject punctuated English word fragments (#7019)

This commit is contained in:
jxxghp
2026-10-16 12:28:03 +08:00
parent 383fb82b89
commit 70aa163568
2 changed files with 124 additions and 9 deletions

View File

@@ -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}')

View File

@@ -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()