fix(search): disambiguate yearless same-title resources

This commit is contained in:
jxxghp
2026-08-22 11:35:01 +08:00
parent 5d98f397cb
commit 2d982f08fa
6 changed files with 435 additions and 12 deletions
+24
View File
@@ -5,6 +5,7 @@ from unittest.mock import patch
import pytest
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo, MetaInfoPath, find_metainfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
@@ -160,6 +161,29 @@ def test_torrent_title_match_ignores_question_mark_variants():
)
def test_torrent_title_match_rejects_season_absent_from_target_series():
"""无年份同名剧的资源季超出目标剧季范围时应拒绝。"""
mediainfo = MediaInfo(
title="家族计划",
original_title="가족계획",
names=["Family Matters"],
type=MediaType.TV,
year="2024",
number_of_seasons=1,
seasons={1: list(range(1, 7))},
season_years={1: "2024"},
)
torrent_meta = MetaInfo("Family Matters S02 1080p WEBRip DD2.0 x264-TrollHD")
torrent = SimpleNamespace(
site_name="测试站点",
title=torrent_meta.org_string,
category=MediaType.TV.value,
description=None,
)
assert not TorrentHelper.match_torrent(mediainfo, torrent_meta, torrent)
def test_python_metainfo_fallback_preserves_xxx_movie_title():
"""Python 兜底解析不应删除合法 xXx 片名。"""
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
@@ -0,0 +1,104 @@
from unittest.mock import Mock
from app.chain import search as search_module
from app.chain.search import SearchChain
from app.domain.context import MediaInfo, TorrentInfo
from app.schemas.types import MediaSource, MediaType
def test_exact_search_rejects_no_year_alias_recognized_as_different_work(monkeypatch):
"""精确搜索中的无年份别名候选识别为不同作品时应拒绝。"""
target = MediaInfo(
media_source=MediaSource.TMDB,
media_id="236356",
tmdb_id=236356,
title="家族计划",
original_title="가족계획",
names=["Family Matters"],
type=MediaType.TV,
year="2024",
original_language="ko",
season_years={1: "2024"},
)
candidate = MediaInfo(
media_source=MediaSource.TMDB,
media_id="30161",
tmdb_id=30161,
title="Family Matters",
original_title="Family Matters",
type=MediaType.TV,
year="2008",
original_language="en",
)
torrent = TorrentInfo(
title="Family Matters S01 1080p WEBRip DD2.0 x264-TrollHD",
site_name="测试站点",
category=MediaType.TV.value,
)
media_chain = Mock()
media_chain.recognize_by_meta.return_value = candidate
monkeypatch.setattr(search_module, "MediaChain", Mock(return_value=media_chain))
monkeypatch.setattr(
search_module.TorrentHelper,
"sort_torrents",
staticmethod(lambda contexts: contexts),
)
chain = object.__new__(SearchChain)
contexts = chain._SearchChain__parse_result(
torrents=[torrent],
mediainfo=target,
rule_groups=[],
)
assert contexts == []
media_chain.recognize_by_meta.assert_called_once()
def test_exact_search_reuses_disambiguation_for_same_parsed_title(monkeypatch):
"""同一解析标题的多条资源应复用一次候选识别,避免重复外部查询。"""
target = MediaInfo(
media_source=MediaSource.TMDB,
media_id="236356",
tmdb_id=236356,
title="家族计划",
original_title="가족계획",
names=["Family Matters"],
type=MediaType.TV,
year="2024",
season_years={1: "2024"},
)
candidate = MediaInfo(
media_source=MediaSource.TMDB,
media_id="236356",
tmdb_id=236356,
title="家族计划",
type=MediaType.TV,
year="2024",
)
torrents = [
TorrentInfo(
title=f"Family Matters S01 1080p WEB-DL {group}",
site_name=f"测试站点{index}",
category=MediaType.TV.value,
)
for index, group in enumerate(("GROUP-A", "GROUP-B"), start=1)
]
media_chain = Mock()
media_chain.recognize_by_meta.return_value = candidate
monkeypatch.setattr(search_module, "MediaChain", Mock(return_value=media_chain))
monkeypatch.setattr(
search_module.TorrentHelper,
"sort_torrents",
staticmethod(lambda contexts: contexts),
)
chain = object.__new__(SearchChain)
contexts = chain._SearchChain__parse_result(
torrents=torrents,
mediainfo=target,
rule_groups=[],
)
assert len(contexts) == 2
media_chain.recognize_by_meta.assert_called_once()
+82 -11
View File
@@ -5,36 +5,44 @@ from app.domain.context import Context, MediaInfo, TorrentInfo
from app.schemas.types import MediaSource, MediaType
def _target_media(tmdb_id=106449, douban_id=None) -> MediaInfo:
def _target_media(tmdb_id=106449, douban_id=None, **kwargs) -> MediaInfo:
"""构造订阅目标媒体。"""
media_source = MediaSource.TMDB if tmdb_id is not None else MediaSource.Douban
media_id = str(tmdb_id) if tmdb_id is not None else douban_id
defaults = {
"title": "凡人修仙传",
"original_title": "凡人修仙传",
"names": ["A Record Of A Mortals Journey To Immortality"],
"type": MediaType.TV,
"year": "2020",
"season_years": {1: "2020"},
}
defaults.update(kwargs)
return MediaInfo(
media_source=media_source if media_id is not None else None,
media_id=media_id,
title="凡人修仙传",
original_title="凡人修仙传",
names=["A Record Of A Mortals Journey To Immortality"],
type=MediaType.TV,
year="2020",
season_years={1: "2020"},
tmdb_id=tmdb_id,
douban_id=douban_id,
**defaults,
)
def _candidate_media(tmdb_id=285479, douban_id=None) -> MediaInfo:
def _candidate_media(tmdb_id=285479, douban_id=None, **kwargs) -> MediaInfo:
"""构造由 RSS 标题推断出的候选媒体。"""
media_source = MediaSource.TMDB if tmdb_id is not None else MediaSource.Douban
media_id = str(tmdb_id) if tmdb_id is not None else douban_id
defaults = {
"title": "凡人修仙传",
"type": MediaType.TV,
"year": "2020",
}
defaults.update(kwargs)
return MediaInfo(
media_source=media_source if media_id is not None else None,
media_id=media_id,
title="凡人修仙传",
type=MediaType.TV,
year="2020",
tmdb_id=tmdb_id,
douban_id=douban_id,
**defaults,
)
@@ -96,6 +104,69 @@ def test_inferred_tmdb_conflict_falls_back_to_strict_alias_match():
assert context.media_info_is_target is True
def test_no_year_alias_rejects_candidate_with_different_first_air_year():
"""无年份别名命中另一个首播年份的同名作品时应拒绝。"""
target = _target_media(
tmdb_id=236356,
title="家族计划",
original_title="가족계획",
names=["Family Matters"],
year="2024",
original_language="ko",
season_years={1: "2024"},
)
candidate = _candidate_media(
tmdb_id=30161,
title="Family Matters",
original_title="Family Matters",
year="2008",
original_language="en",
)
meta = _torrent_meta(en_name="Family Matters")
torrent = TorrentInfo(
title="Family Matters S01 1080p WEBRip DD2.0 x264-TrollHD",
site_name="测试站点",
category=MediaType.TV.value,
)
context = _context(meta, candidate, torrent)
assert _reconcile(target, candidate, meta, torrent, context) is None
assert context.media_info is candidate
assert context.match_source == "tmdbid"
def test_explicit_target_year_can_override_wrong_same_name_candidate():
"""资源明确携带目标年份时应允许纠正同名候选的错误识别。"""
target = _target_media(
tmdb_id=236356,
title="家族计划",
original_title="가족계획",
names=["Family Matters"],
year="2024",
original_language="ko",
season_years={1: "2024"},
)
candidate = _candidate_media(
tmdb_id=30161,
title="Family Matters",
original_title="Family Matters",
year="2008",
original_language="en",
)
meta = _torrent_meta(en_name="Family Matters")
meta.year = "2024"
torrent = TorrentInfo(
title="Family Matters 2024 S01 1080p WEB-DL",
site_name="测试站点",
category=MediaType.TV.value,
)
context = _context(meta, candidate, torrent)
assert _reconcile(target, candidate, meta, torrent, context) is target
assert context.media_info is target
assert context.match_source == "title"
def test_inferred_tmdb_conflict_rejects_nonmatching_title():
"""ID 冲突且标题和别名不匹配时应继续拒绝候选。"""
target = _target_media()