fix(subscribe): preserve source identity when adding subscriptions

This commit is contained in:
jxxghp
2026-07-25 08:51:30 +08:00
parent ff9ba79b60
commit 8d099b9581
3 changed files with 175 additions and 2 deletions

View File

@@ -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:

View File

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

View File

@@ -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"