mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 05:44:44 +08:00
fix(search): reject punctuated English word fragments (#7019)
This commit is contained in:
@@ -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}')
|
||||
|
||||
111
tests/test_torrent_title_matching.py
Normal file
111
tests/test_torrent_title_matching.py
Normal 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()
|
||||
Reference in New Issue
Block a user