From 37ea8ba14d61af4859d8d9b20b6a5b8a149f1d63 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 15 Sep 2026 11:20:55 +0800 Subject: [PATCH] fix(subscribe): support cross-source video status lookup --- app/api/endpoints/subscribe.py | 22 ++++++- app/application/subscription/query.py | 25 ++++++++ tests/test_subscribe_endpoint.py | 82 +++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index dd83297a4..886ea57bf 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -408,17 +408,37 @@ async def subscribe_media_identity( media_source: MediaSource, season: Optional[int] = None, title: Optional[str] = None, + year: Optional[str] = None, + mtype: Optional[MediaType] = None, music_type: Optional[str] = None, query: SubscriptionQueryService = Depends(get_subscription_query_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ - 根据媒体来源和原生 ID 查询订阅。 + 根据媒体身份查询订阅,视频身份未命中时按类型、标题和年份回退。 """ + metadata = MetaInfo(title) if title else None + normalized_title = metadata.name if metadata else None + if season is None and metadata: + season = metadata.begin_season subscribes = await query.list_by_media_identity(media_source, media_id, music_type) if season is not None: subscribes = [subscribe for subscribe in subscribes if subscribe.season == season] result = select_accessible_subscribe(subscribes, current_user) + if ( + not result + and media_source != MediaSource.TMDB + and mtype in (MediaType.MOVIE, MediaType.TV) + and normalized_title + and year + ): + subscribes = await query.list_by_video_metadata( + title=normalized_title, + year=year, + media_type=mtype, + season=season, + ) + result = select_accessible_subscribe(subscribes, current_user) return result if result else _SchemaSubscribe() diff --git a/app/application/subscription/query.py b/app/application/subscription/query.py index 6e29a3717..b940fa1fa 100644 --- a/app/application/subscription/query.py +++ b/app/application/subscription/query.py @@ -123,6 +123,31 @@ class SubscriptionQueryService: if self._matches_music_type(record, music_type) ] + async def list_by_video_metadata( + self, + *, + title: str, + year: str, + media_type: MediaType, + season: Optional[int] = None, + ) -> list[SubscribeView]: + """按影视类型、规范标题、年份和可选季号读取跨来源订阅。""" + if self._async_repository is None: + raise RuntimeError("异步订阅查询端口未注册") + if media_type not in (MediaType.MOVIE, MediaType.TV): + return [] + records = await self._async_repository.async_list_by_title( + title=title, + season=season, + ) + expected_year = str(year).strip() + return [ + self._to_public_view(record) + for record in records + if record.type == media_type.value + and str(record.year or "").strip() == expected_year + ] + async def list_history( self, mtype: str, diff --git a/tests/test_subscribe_endpoint.py b/tests/test_subscribe_endpoint.py index 9f1f3c69f..26b1fee91 100644 --- a/tests/test_subscribe_endpoint.py +++ b/tests/test_subscribe_endpoint.py @@ -598,6 +598,88 @@ class TestSubscribeEndpoint: assert result.id is None repository.async_list_by_title.assert_not_awaited() + def test_subscribe_media_identity_falls_back_to_video_metadata(self): + """豆瓣身份未命中时,电影应按类型、标题和年份匹配已有订阅。""" + from app.api.endpoints.subscribe import subscribe_media_identity + + other_year = _EndpointSubscribe( + id=31, + username="alice", + name="跨来源电影", + year="2024", + type=MediaType.MOVIE.value, + media_source=MediaSource.TMDB, + media_id="tmdb-2024", + ) + matched = _EndpointSubscribe( + id=32, + username="alice", + name="跨来源电影", + year="2025", + type=MediaType.MOVIE.value, + media_source=MediaSource.TMDB, + media_id="tmdb-2025", + ) + inaccessible_exact = _EndpointSubscribe( + id=30, + username="bob", + name="跨来源电影", + year="2025", + type=MediaType.MOVIE.value, + media_source=MediaSource.Douban, + media_id="douban-2025", + ) + repository = _SubscriptionRepositoryFake(inaccessible_exact, other_year, matched) + + result = asyncio.run( + subscribe_media_identity( + media_id="douban-2025", + media_source=MediaSource.Douban, + title="跨来源电影", + year="2025", + mtype=MediaType.MOVIE, + query=_subscription_query(repository), + current_user=_EndpointUser(name="alice", is_superuser=False), + ) + ) + + assert result.id == 32 + repository.async_list_by_title.assert_awaited_once_with( + title="跨来源电影", + season=None, + ) + + def test_subscribe_media_identity_keeps_tmdb_lookup_strict(self): + """TMDB 身份可用时不得退化为同名年份查询。""" + from app.api.endpoints.subscribe import subscribe_media_identity + + repository = _SubscriptionRepositoryFake( + _EndpointSubscribe( + id=33, + username="alice", + name="同名电影", + year="2025", + type=MediaType.MOVIE.value, + media_source=MediaSource.Douban, + media_id="douban-same-title", + ) + ) + + result = asyncio.run( + subscribe_media_identity( + media_id="missing-tmdb", + media_source=MediaSource.TMDB, + title="同名电影", + year="2025", + mtype=MediaType.MOVIE, + query=_subscription_query(repository), + current_user=_EndpointUser(name="alice", is_superuser=False), + ) + ) + + assert result.id is None + repository.async_list_by_title.assert_not_awaited() + def test_delete_subscribe_by_media_identity_deletes_owner_candidate(self): """ 按媒体删除端点应把媒体身份和当前用户交给应用命令。