mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 02:05:13 +08:00
fix(subscribe): preserve source identity when adding subscriptions
This commit is contained in:
@@ -366,6 +366,9 @@ async def seasons(
|
||||
)
|
||||
if mediainfo:
|
||||
return _build_media_seasons(mediainfo, season)
|
||||
# 明确来源的查询不能按标题切换到默认识别源,避免辅助 TMDB 信息替换主身份。
|
||||
if media_source and source_media_id:
|
||||
return []
|
||||
if title:
|
||||
meta = MetaInfo(title)
|
||||
if year:
|
||||
|
||||
@@ -904,6 +904,16 @@ class SubscribeChain(ChainBase):
|
||||
metainfo.begin_season = season
|
||||
if not media_source and not media_id and mediaid:
|
||||
media_source, media_id = parse_media_key(mediaid)
|
||||
resolved_source, resolved_media_id = resolve_media_identity(
|
||||
source=media_source,
|
||||
media_id=media_id,
|
||||
tmdbid=tmdbid,
|
||||
doubanid=doubanid,
|
||||
bangumiid=bangumiid,
|
||||
anilistid=anilistid,
|
||||
)
|
||||
if resolved_source and resolved_media_id:
|
||||
media_source, media_id = resolved_source, resolved_media_id
|
||||
if any((media_id, tmdbid, doubanid, bangumiid, anilistid)):
|
||||
mediainfo = self.recognize_media(
|
||||
meta=metainfo,
|
||||
@@ -926,10 +936,11 @@ class SubscribeChain(ChainBase):
|
||||
if season is None:
|
||||
season = meta.begin_season
|
||||
|
||||
# 使用名称识别兜底
|
||||
# 明确来源时只允许在同一来源内按名称兜底,不能切换主识别源。
|
||||
if not mediainfo:
|
||||
mediainfo = MediaChain().recognize_by_meta(
|
||||
metainfo,
|
||||
source=media_source,
|
||||
episode_group=episode_group,
|
||||
obtain_images=False,
|
||||
)
|
||||
@@ -1099,6 +1110,16 @@ class SubscribeChain(ChainBase):
|
||||
metainfo.begin_season = season
|
||||
if not media_source and not media_id and mediaid:
|
||||
media_source, media_id = parse_media_key(mediaid)
|
||||
resolved_source, resolved_media_id = resolve_media_identity(
|
||||
source=media_source,
|
||||
media_id=media_id,
|
||||
tmdbid=tmdbid,
|
||||
doubanid=doubanid,
|
||||
bangumiid=bangumiid,
|
||||
anilistid=anilistid,
|
||||
)
|
||||
if resolved_source and resolved_media_id:
|
||||
media_source, media_id = resolved_source, resolved_media_id
|
||||
if any((media_id, tmdbid, doubanid, bangumiid, anilistid)):
|
||||
mediainfo = await self.async_recognize_media(
|
||||
meta=metainfo,
|
||||
@@ -1121,10 +1142,11 @@ class SubscribeChain(ChainBase):
|
||||
if season is None:
|
||||
season = meta.begin_season
|
||||
|
||||
# 使用名称识别兜底
|
||||
# 明确来源时只允许在同一来源内按名称兜底,不能切换主识别源。
|
||||
if not mediainfo:
|
||||
mediainfo = await MediaChain().async_recognize_by_meta(
|
||||
metainfo,
|
||||
source=media_source,
|
||||
episode_group=episode_group,
|
||||
obtain_images=False,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.endpoints import media as media_endpoint
|
||||
from app.api.endpoints import search as search_endpoint
|
||||
from app.chain import subscribe as subscribe_module
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.core.context import MediaInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
@@ -150,3 +155,146 @@ def test_media_seasons_builds_anilist_season_response(monkeypatch) -> None:
|
||||
assert result[0].poster_path == media.poster_path
|
||||
assert captured["source"] == "anilist"
|
||||
assert captured["mediaid"] == "154587"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mediaid", "media_kwargs", "episode_count"),
|
||||
[
|
||||
(
|
||||
"douban:db-7301",
|
||||
{
|
||||
"douban_info": {
|
||||
"episodes_count": 12,
|
||||
"id": "db-7301",
|
||||
"subtype": "tv",
|
||||
"title": "豆瓣剧集",
|
||||
}
|
||||
},
|
||||
12,
|
||||
),
|
||||
(
|
||||
"bangumi:7302",
|
||||
{
|
||||
"bangumi_info": {
|
||||
"id": 7302,
|
||||
"name_cn": "Bangumi 剧集",
|
||||
"platform": "TV",
|
||||
"total_episodes": 13,
|
||||
}
|
||||
},
|
||||
13,
|
||||
),
|
||||
(
|
||||
"anilist:7303",
|
||||
{
|
||||
"anilist_info": {
|
||||
"episodes": 14,
|
||||
"format": "TV",
|
||||
"id": 7303,
|
||||
"title": {"native": "AniList 剧集"},
|
||||
}
|
||||
},
|
||||
14,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_media_seasons_uses_source_episode_count_and_defaults_to_first_season(
|
||||
monkeypatch, mediaid: str, media_kwargs: dict, episode_count: int,
|
||||
) -> None:
|
||||
"""非 TMDB 来源应使用自身总集数构造第 1 季,不依赖 TMDB。"""
|
||||
media = MediaInfo(**media_kwargs)
|
||||
media_chain = Mock()
|
||||
media_chain.async_recognize_media = AsyncMock(return_value=media)
|
||||
monkeypatch.setattr(media_endpoint, "MediaChain", Mock(return_value=media_chain))
|
||||
|
||||
result = asyncio.run(
|
||||
media_endpoint.seasons(mediaid=mediaid, season=None, _=None)
|
||||
)
|
||||
|
||||
assert media.season is None
|
||||
assert media.seasons[1] == list(range(1, episode_count + 1))
|
||||
assert len(result) == 1
|
||||
assert result[0].season_number == 1
|
||||
assert result[0].episode_count == episode_count
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mediaid",
|
||||
["douban:db-7401", "bangumi:7402", "anilist:7403"],
|
||||
)
|
||||
def test_media_seasons_does_not_fallback_to_default_source_for_explicit_identity(
|
||||
monkeypatch, mediaid: str,
|
||||
) -> None:
|
||||
"""明确来源查询失败时应直接返回空列表,不能按标题切换到默认源。"""
|
||||
media_chain = Mock()
|
||||
media_chain.async_recognize_media = AsyncMock(return_value=None)
|
||||
media_chain.async_recognize_by_meta = AsyncMock(
|
||||
side_effect=AssertionError("不应按标题切换识别源")
|
||||
)
|
||||
monkeypatch.setattr(media_endpoint, "MediaChain", Mock(return_value=media_chain))
|
||||
|
||||
result = asyncio.run(
|
||||
media_endpoint.seasons(
|
||||
mediaid=mediaid,
|
||||
title="来源查询失败剧集",
|
||||
year="2026",
|
||||
_=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert result == []
|
||||
media_chain.async_recognize_media.assert_awaited_once()
|
||||
media_chain.async_recognize_by_meta.assert_not_awaited()
|
||||
|
||||
|
||||
def test_subscribe_add_keeps_inferred_anilist_source_during_title_fallback() -> None:
|
||||
"""同步新增订阅按兼容 ID 推导来源后,标题兜底仍应限定 AniList。"""
|
||||
media_chain = Mock()
|
||||
media_chain.recognize_by_meta.return_value = None
|
||||
chain = object.__new__(SubscribeChain)
|
||||
|
||||
with patch.object(SubscribeChain, "recognize_media", return_value=None) as recognize, \
|
||||
patch.object(subscribe_module, "MediaChain", return_value=media_chain):
|
||||
sid, message = chain.add(
|
||||
title="AniList 同步订阅",
|
||||
year="2026",
|
||||
mtype=MediaType.TV,
|
||||
anilistid=154587,
|
||||
tmdbid=209867,
|
||||
media_source="anilist",
|
||||
)
|
||||
|
||||
assert sid is None
|
||||
assert message == "未识别到媒体信息"
|
||||
assert recognize.call_args.kwargs["source"] == "anilist"
|
||||
assert recognize.call_args.kwargs["mediaid"] == "154587"
|
||||
assert media_chain.recognize_by_meta.call_args.kwargs["source"] == "anilist"
|
||||
|
||||
|
||||
def test_subscribe_async_add_keeps_inferred_anilist_source_during_title_fallback() -> None:
|
||||
"""异步新增订阅按兼容 ID 推导来源后,标题兜底仍应限定 AniList。"""
|
||||
media_chain = Mock()
|
||||
media_chain.async_recognize_by_meta = AsyncMock(return_value=None)
|
||||
chain = object.__new__(SubscribeChain)
|
||||
|
||||
with patch.object(
|
||||
SubscribeChain, "async_recognize_media", new=AsyncMock(return_value=None)
|
||||
) as recognize, patch.object(
|
||||
subscribe_module, "MediaChain", return_value=media_chain
|
||||
):
|
||||
sid, message = asyncio.run(
|
||||
chain.async_add(
|
||||
title="AniList 异步订阅",
|
||||
year="2026",
|
||||
mtype=MediaType.TV,
|
||||
anilistid=154587,
|
||||
tmdbid=209867,
|
||||
media_source="anilist",
|
||||
)
|
||||
)
|
||||
|
||||
assert sid is None
|
||||
assert message == "未识别到媒体信息"
|
||||
assert recognize.await_args.kwargs["source"] == "anilist"
|
||||
assert recognize.await_args.kwargs["mediaid"] == "154587"
|
||||
assert media_chain.async_recognize_by_meta.await_args.kwargs["source"] == "anilist"
|
||||
|
||||
Reference in New Issue
Block a user