mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 02:05:13 +08:00
fix(subscribe): 修复 RSS 媒体 ID 误识别跳过 (#6266)
This commit is contained in:
@@ -2370,26 +2370,34 @@ class SubscribeChain(ChainBase):
|
||||
)[1]:
|
||||
if torrent_mediainfo.type != mediainfo.type:
|
||||
continue
|
||||
if torrent_mediainfo.tmdb_id \
|
||||
and torrent_mediainfo.tmdb_id != mediainfo.tmdb_id:
|
||||
torrent_mediainfo = self.__reconcile_candidate_media(
|
||||
target_mediainfo=mediainfo,
|
||||
candidate_mediainfo=torrent_mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
torrent_info=torrent_info,
|
||||
context=_context,
|
||||
)
|
||||
if not torrent_mediainfo:
|
||||
continue
|
||||
if torrent_mediainfo.douban_id \
|
||||
and torrent_mediainfo.douban_id != mediainfo.douban_id:
|
||||
continue
|
||||
logger.info(
|
||||
f'{mediainfo.title_year} 通过媒体ID匹配到可选资源:{torrent_info.site_name} - {torrent_info.title}')
|
||||
match_source = _context.match_source
|
||||
if match_source == "title":
|
||||
# 标题兜底使用的是订阅目标 media_info,不能标记为候选自身识别结果。
|
||||
_context.candidate_recognized = False
|
||||
_context.media_info_is_target = True
|
||||
match_label = "标题复核"
|
||||
elif match_source == "unknown":
|
||||
_context.match_source = self.__get_media_id_match_source(torrent_mediainfo)
|
||||
_context.candidate_recognized = True
|
||||
_context.media_info_is_target = False
|
||||
match_label = "媒体ID"
|
||||
else:
|
||||
_context.candidate_recognized = True
|
||||
_context.media_info_is_target = False
|
||||
match_label = "媒体ID"
|
||||
logger.info(
|
||||
f'{mediainfo.title_year} 通过{match_label}匹配到可选资源:'
|
||||
f'{torrent_info.site_name} - {torrent_info.title}'
|
||||
)
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -4809,6 +4817,80 @@ class SubscribeChain(ChainBase):
|
||||
return "plugin"
|
||||
return "unknown"
|
||||
|
||||
@staticmethod
|
||||
def __reconcile_candidate_media(
|
||||
target_mediainfo: MediaInfo,
|
||||
candidate_mediainfo: MediaInfo,
|
||||
torrent_meta: MetaBase,
|
||||
torrent_info: TorrentInfo,
|
||||
context: Context,
|
||||
) -> Optional[MediaInfo]:
|
||||
"""
|
||||
在推断媒体 ID 冲突时,以订阅目标对候选标题做严格复核。
|
||||
|
||||
标题携带明确媒体身份或目标缺少同来源 ID 时保持严格拒绝;只有普通
|
||||
标题推断产生冲突且标题、别名、类型和年份复核通过时才回填订阅目标。
|
||||
|
||||
:param target_mediainfo: 订阅目标媒体信息
|
||||
:param candidate_mediainfo: RSS 或首页候选的识别结果
|
||||
:param torrent_meta: 候选标题解析信息
|
||||
:param torrent_info: 候选种子信息
|
||||
:param context: 当前订阅使用的候选上下文副本
|
||||
:return: 可继续匹配的媒体信息,拒绝时返回 None
|
||||
"""
|
||||
identity_pairs = (
|
||||
("TMDB", candidate_mediainfo.tmdb_id, target_mediainfo.tmdb_id),
|
||||
("豆瓣", candidate_mediainfo.douban_id, target_mediainfo.douban_id),
|
||||
)
|
||||
conflicts = [
|
||||
(source, str(candidate_id), str(target_id) if target_id is not None else None)
|
||||
for source, candidate_id, target_id in identity_pairs
|
||||
if candidate_id is not None
|
||||
and str(candidate_id) != (str(target_id) if target_id is not None else None)
|
||||
]
|
||||
if not conflicts:
|
||||
return candidate_mediainfo
|
||||
|
||||
conflict_text = "、".join(
|
||||
f"{source} {candidate_id} != {target_id if target_id is not None else '缺失'}"
|
||||
for source, candidate_id, target_id in conflicts
|
||||
)
|
||||
if any(target_id is None for _, _, target_id in conflicts):
|
||||
logger.debug(
|
||||
f'{torrent_info.site_name} - {torrent_info.title} 候选媒体ID与订阅目标不可比较:'
|
||||
f'{conflict_text}'
|
||||
)
|
||||
return None
|
||||
|
||||
explicit_source, explicit_id = resolve_media_identity(media=torrent_meta)
|
||||
if explicit_source and explicit_id:
|
||||
logger.debug(
|
||||
f'{torrent_info.site_name} - {torrent_info.title} 标题含明确媒体ID '
|
||||
f'{explicit_source}:{explicit_id},保持严格校验:{conflict_text}'
|
||||
)
|
||||
return None
|
||||
|
||||
if not TorrentHelper.match_torrent(
|
||||
mediainfo=target_mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
torrent=torrent_info,
|
||||
):
|
||||
logger.debug(
|
||||
f'{torrent_info.site_name} - {torrent_info.title} 候选媒体ID冲突且标题复核失败:'
|
||||
f'{conflict_text}'
|
||||
)
|
||||
return None
|
||||
|
||||
context.media_info = target_mediainfo
|
||||
context.match_source = "title"
|
||||
context.candidate_recognized = False
|
||||
context.media_info_is_target = True
|
||||
logger.debug(
|
||||
f'{target_mediainfo.title_year} 候选媒体ID冲突({conflict_text}),'
|
||||
f'经标题或别名复核匹配到订阅目标:{torrent_info.site_name} - {torrent_info.title}'
|
||||
)
|
||||
return target_mediainfo
|
||||
|
||||
@staticmethod
|
||||
def get_states_for_search(state: str) -> str:
|
||||
"""
|
||||
|
||||
184
tests/test_subscribe_media_id_conflict.py
Normal file
184
tests/test_subscribe_media_id_conflict.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.core.context import Context, MediaInfo, TorrentInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
def _target_media(tmdb_id=106449, douban_id=None) -> MediaInfo:
|
||||
"""构造订阅目标媒体。"""
|
||||
return MediaInfo(
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def _candidate_media(tmdb_id=285479, douban_id=None) -> MediaInfo:
|
||||
"""构造由 RSS 标题推断出的候选媒体。"""
|
||||
return MediaInfo(
|
||||
title="凡人修仙传",
|
||||
type=MediaType.TV,
|
||||
year="2020",
|
||||
tmdb_id=tmdb_id,
|
||||
douban_id=douban_id,
|
||||
)
|
||||
|
||||
|
||||
def _torrent_meta(*, en_name="A Record Of A Mortals Journey To Immortality", tmdbid=None):
|
||||
"""构造不触发外部识别的标题解析结果。"""
|
||||
return SimpleNamespace(
|
||||
tmdbid=tmdbid,
|
||||
doubanid=None,
|
||||
bangumiid=None,
|
||||
anilistid=None,
|
||||
media_source="themoviedb" if tmdbid else None,
|
||||
media_id=str(tmdbid) if tmdbid else None,
|
||||
cn_name="",
|
||||
en_name=en_name,
|
||||
type=MediaType.TV,
|
||||
year=None,
|
||||
org_string=f"{en_name} S01E186 2160p WEB-DL",
|
||||
)
|
||||
|
||||
|
||||
def _context(meta, candidate, torrent) -> Context:
|
||||
"""构造 RSS 候选上下文。"""
|
||||
return Context(
|
||||
meta_info=meta,
|
||||
media_info=candidate,
|
||||
torrent_info=torrent,
|
||||
resource_source="rss",
|
||||
match_source="tmdbid",
|
||||
candidate_recognized=True,
|
||||
media_info_is_target=False,
|
||||
)
|
||||
|
||||
|
||||
def _reconcile(target, candidate, meta, torrent, context):
|
||||
"""调用订阅链的候选媒体冲突复核。"""
|
||||
return SubscribeChain._SubscribeChain__reconcile_candidate_media(
|
||||
target_mediainfo=target,
|
||||
candidate_mediainfo=candidate,
|
||||
torrent_meta=meta,
|
||||
torrent_info=torrent,
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
def test_inferred_tmdb_conflict_falls_back_to_strict_alias_match():
|
||||
"""标题推断到重复 TMDB 条目时应允许订阅目标别名复核。"""
|
||||
target = _target_media()
|
||||
candidate = _candidate_media()
|
||||
meta = _torrent_meta()
|
||||
torrent = TorrentInfo(
|
||||
title="A Record Of A Mortals Journey To Immortality S01E186 2160p WEB-DL",
|
||||
site_name="测试站点",
|
||||
category=MediaType.TV.value,
|
||||
)
|
||||
context = _context(meta, candidate, torrent)
|
||||
|
||||
result = _reconcile(target, candidate, meta, torrent, context)
|
||||
|
||||
assert result is target
|
||||
assert context.media_info is target
|
||||
assert context.match_source == "title"
|
||||
assert context.candidate_recognized is False
|
||||
assert context.media_info_is_target is True
|
||||
|
||||
|
||||
def test_inferred_tmdb_conflict_rejects_nonmatching_title():
|
||||
"""ID 冲突且标题和别名不匹配时应继续拒绝候选。"""
|
||||
target = _target_media()
|
||||
candidate = _candidate_media()
|
||||
meta = _torrent_meta(en_name="Different Series")
|
||||
torrent = TorrentInfo(
|
||||
title="Different Series S01E186 2160p WEB-DL",
|
||||
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"
|
||||
assert context.candidate_recognized is True
|
||||
assert context.media_info_is_target is False
|
||||
|
||||
|
||||
def test_inferred_douban_conflict_falls_back_to_strict_alias_match():
|
||||
"""豆瓣 ID 推断冲突时应使用同一套严格标题复核。"""
|
||||
target = _target_media(tmdb_id=None, douban_id="30170816")
|
||||
candidate = _candidate_media(tmdb_id=None, douban_id="36612345")
|
||||
meta = _torrent_meta()
|
||||
torrent = TorrentInfo(
|
||||
title="A Record Of A Mortals Journey To Immortality S01E186 2160p WEB-DL",
|
||||
site_name="测试站点",
|
||||
category=MediaType.TV.value,
|
||||
)
|
||||
context = _context(meta, candidate, torrent)
|
||||
|
||||
result = _reconcile(target, candidate, meta, torrent, context)
|
||||
|
||||
assert result is target
|
||||
assert context.media_info is target
|
||||
assert context.match_source == "title"
|
||||
|
||||
|
||||
def test_explicit_tmdb_identity_keeps_strict_conflict_rejection():
|
||||
"""标题显式携带媒体 ID 时不得被标题别名覆盖。"""
|
||||
target = _target_media()
|
||||
candidate = _candidate_media()
|
||||
meta = _torrent_meta(tmdbid=285479)
|
||||
torrent = TorrentInfo(
|
||||
title="A Record Of A Mortals Journey To Immortality S01E186 {tmdbid=285479}",
|
||||
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_conflict_rejects_target_without_same_source_identity():
|
||||
"""订阅目标缺少候选同来源 ID 时不得用标题跨来源放行。"""
|
||||
target = _target_media(tmdb_id=None)
|
||||
candidate = _candidate_media()
|
||||
meta = _torrent_meta()
|
||||
torrent = TorrentInfo(
|
||||
title="A Record Of A Mortals Journey To Immortality S01E186",
|
||||
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
|
||||
|
||||
|
||||
def test_matching_media_id_preserves_candidate_identity():
|
||||
"""媒体 ID 原本一致时应保持候选识别上下文。"""
|
||||
target = _target_media()
|
||||
candidate = _candidate_media(tmdb_id=106449)
|
||||
meta = _torrent_meta()
|
||||
torrent = TorrentInfo(
|
||||
title="A Record Of A Mortals Journey To Immortality S01E186",
|
||||
site_name="测试站点",
|
||||
category=MediaType.TV.value,
|
||||
)
|
||||
context = _context(meta, candidate, torrent)
|
||||
|
||||
result = _reconcile(target, candidate, meta, torrent, context)
|
||||
|
||||
assert result is candidate
|
||||
assert context.media_info is candidate
|
||||
assert context.match_source == "tmdbid"
|
||||
assert context.candidate_recognized is True
|
||||
assert context.media_info_is_target is False
|
||||
Reference in New Issue
Block a user