fix(subscribe): support cross-source video status lookup

This commit is contained in:
jxxghp
2026-09-15 11:20:55 +08:00
parent 99831650bb
commit 37ea8ba14d
3 changed files with 128 additions and 1 deletions

View File

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

View File

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

View File

@@ -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):
"""
按媒体删除端点应把媒体身份和当前用户交给应用命令。