From a79e00f1e98ea6a021a54ad19fac92f2e3e504f0 Mon Sep 17 00:00:00 2001 From: ngcat Date: Wed, 26 Aug 2026 06:43:01 +0800 Subject: [PATCH] fix(subscribe): map v2 tmdbid source to media_id (#6460) --- app/application/subscription/query.py | 38 ++++++++++++++++++++++- tests/test_subscription_query_service.py | 39 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/app/application/subscription/query.py b/app/application/subscription/query.py index a6ef73950..c8ef482ec 100644 --- a/app/application/subscription/query.py +++ b/app/application/subscription/query.py @@ -7,7 +7,7 @@ from typing import Any, Optional, Protocol from app.domain.context import MediaInfo from app.domain.meta.metabase import MetaBase from app.schemas.media import resolve_media_identity -from app.schemas.types import MediaType +from app.schemas.types import MediaSource, MediaType from app.schemas.workflow import Subscribe as SubscribeView @@ -85,6 +85,14 @@ class SubscriptionQueryService: "media_id", "music_type", } + _LEGACY_ID_FIELDS: tuple[tuple[str, MediaSource], ...] = ( + ("tmdbid", MediaSource.TMDB), + ("doubanid", MediaSource.Douban), + ("bangumiid", MediaSource.Bangumi), + ("anilistid", MediaSource.AniList), + ("imdbid", MediaSource.IMDb), + ("tvdbid", MediaSource.TVDB), + ) def __init__( self, @@ -199,6 +207,30 @@ class SubscriptionQueryService: episode_group=mediainfo.episode_group, )) + @classmethod + def _has_media_identity(cls, identity: dict[str, Any]) -> bool: + """判断来源关键字是否已带成对的媒体身份。""" + media_id = identity.get("media_id") + return bool(identity.get("media_source")) and media_id not in ( + None, + "", + 0, + "0", + ) + + @classmethod + def _legacy_media_identity(cls, source_keyword: dict[str, Any]) -> dict[str, Any]: + """把 v2 订阅来源里的 tmdbid/doubanid 等补成 media_source + media_id。""" + for field, source in cls._LEGACY_ID_FIELDS: + raw = source_keyword.get(field) + if raw in (None, "", 0, "0"): + continue + return { + "media_source": source, + "media_id": str(raw), + } + return {} + def get_by_source(self, source_keyword: Optional[dict]) -> Optional[Any]: """从已解析来源关键字筛出稳定身份字段并读取订阅。""" if not source_keyword: @@ -208,6 +240,10 @@ class SubscriptionQueryService: for key, value in source_keyword.items() if key in self._SOURCE_FIELDS } + if not self._has_media_identity(identity): + identity.update(self._legacy_media_identity(source_keyword)) + if not identity.get("type") or not self._has_media_identity(identity): + return None return self._repository.get_by(**identity) def has_music(self, searchable_states: str) -> bool: diff --git a/tests/test_subscription_query_service.py b/tests/test_subscription_query_service.py index 035d46055..a8ece2b24 100644 --- a/tests/test_subscription_query_service.py +++ b/tests/test_subscription_query_service.py @@ -63,6 +63,45 @@ def test_subscription_query_service_filters_source_and_music_state() -> None: repository.list.assert_called_once_with("R,P") +def test_get_by_source_upgrades_legacy_tmdbid_identity() -> None: + """v2 下载记录只有 tmdbid 时,应补成 themoviedb + media_id 再查订阅。""" + repository = Mock() + expected = SimpleNamespace(id=22) + repository.get_by.return_value = expected + service = SubscriptionQueryService(repository) + + result = service.get_by_source({ + "id": 22, + "name": "阿滋漫画大王", + "type": MediaType.TV.value, + "season": 1, + "tmdbid": 12143, + "imdbid": "tt0339955", + "tvdbid": 79077, + }) + + assert result is expected + repository.get_by.assert_called_once_with( + type=MediaType.TV.value, + season=1, + media_source=MediaSource.TMDB, + media_id="12143", + ) + + +def test_get_by_source_skips_incomplete_legacy_identity() -> None: + """来源既无 media_id 也无旧 tmdbid 时,不得把半对身份传给仓储。""" + repository = Mock() + service = SubscriptionQueryService(repository) + + assert service.get_by_source({ + "type": MediaType.TV.value, + "season": 1, + "name": "Demo", + }) is None + repository.get_by.assert_not_called() + + def test_subscribe_chain_facade_delegates_three_query_slices() -> None: """SubscribeChain 保持三个公开方法签名并仅负责来源解析和结果转发。""" service = Mock()