mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 22:04:57 +08:00
fix(subscribe): resolve cross-source video status without premiere year
This commit is contained in:
@@ -415,7 +415,7 @@ async def subscribe_media_identity(
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
根据媒体身份查询订阅,视频身份未命中时按类型、标题和年份回退。
|
||||
根据媒体身份查询订阅,影视身份未命中时按类型、标题和可用年份跨源回退。
|
||||
"""
|
||||
metadata = MetaInfo(title) if title else None
|
||||
normalized_title = metadata.name if metadata else None
|
||||
@@ -427,10 +427,8 @@ async def subscribe_media_identity(
|
||||
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,
|
||||
|
||||
@@ -127,11 +127,11 @@ class SubscriptionQueryService:
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
year: str,
|
||||
year: Optional[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):
|
||||
@@ -140,13 +140,14 @@ class SubscriptionQueryService:
|
||||
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
|
||||
expected_year = str(year or "").strip()
|
||||
matching_type = [record for record in records if record.type == media_type.value]
|
||||
exact_year = [
|
||||
record for record in matching_type
|
||||
if expected_year and str(record.year or "").strip() == expected_year
|
||||
]
|
||||
unknown_year = [record for record in matching_type if not str(record.year or "").strip()]
|
||||
return [self._to_public_view(record) for record in exact_year + unknown_year]
|
||||
|
||||
async def list_history(
|
||||
self,
|
||||
|
||||
@@ -604,6 +604,13 @@ TMDB 缓存查询响应的 `data` 包含 `count`、`recognized`、`unrecognized`
|
||||
|
||||
取消是幂等的状态请求,不会撤销已经提交到下载器的任务;批次详情中的状态和任务结果才是最终事实。
|
||||
|
||||
### 跨来源订阅回显
|
||||
|
||||
`GET /api/v1/subscribe/media/{media_id}` 先按 `media_source`、`media_id` 和可选季号查询当前用户可访问的订阅。
|
||||
影视身份未命中且提供 `mtype`、`title` 时,会按类型、规范标题和可选季号跨来源查找;
|
||||
有年份时优先精确年份,其次匹配订阅年份为空的未定档媒体;没有年份时只匹配年份也为空的订阅。
|
||||
音乐订阅始终只按媒体身份查询。
|
||||
|
||||
### 单条订阅搜索周期
|
||||
|
||||
`POST /api/v1/subscribe/` 和 `PUT /api/v1/subscribe/` 支持 `search_interval`:
|
||||
|
||||
@@ -56,7 +56,7 @@ Purpose: Read local library and transfer-file coverage for one accessible subscr
|
||||
|
||||
### `subscription.find`
|
||||
`GET /api/v1/subscribe/media/{media_id}`; policy effect: `safe_read`.
|
||||
Purpose: Find one accessible subscription by canonical media identity and optional season.
|
||||
Purpose: Find one accessible subscription by canonical media identity and optional season. For movies and TV, if exact identity misses and mtype/title are supplied, match across sources by type, normalized title, optional season, and year. An exact year ranks first; a subscription with no year may match when the card has a year, while a card with no year only matches a subscription with no year. Music uses exact identity only.
|
||||
- `path_params`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search.
|
||||
- `query`: `media_source*` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `mtype` (MediaType|null): MoviePilot media type or subscription-history category required by the operation.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `title` (string|null): Media, torrent, subscription, or history title used by the operation.; `year` (string|null): Release or premiere year used to disambiguate the media title.
|
||||
- `body`: none
|
||||
|
||||
@@ -669,8 +669,8 @@ class TestSubscribeEndpoint:
|
||||
season=None,
|
||||
)
|
||||
|
||||
def test_subscribe_media_identity_keeps_tmdb_lookup_strict(self):
|
||||
"""TMDB 身份可用时不得退化为同名年份查询。"""
|
||||
def test_subscribe_media_identity_falls_back_from_tmdb(self):
|
||||
"""TMDB 身份未命中时也应找到其它来源的同名同年订阅。"""
|
||||
from app.api.endpoints.subscribe import subscribe_media_identity
|
||||
|
||||
repository = _SubscriptionRepositoryFake(
|
||||
@@ -697,8 +697,112 @@ class TestSubscribeEndpoint:
|
||||
)
|
||||
)
|
||||
|
||||
assert result.id == 33
|
||||
repository.async_list_by_title.assert_awaited_once_with(title="同名电影", season=None)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("card_source", "subscription_source", "card_year"),
|
||||
[
|
||||
(MediaSource("iqiyidiscover"), MediaSource.TMDB, "2026"),
|
||||
(MediaSource.TMDB, MediaSource("iqiyidiscover"), None),
|
||||
(MediaSource.TMDB, MediaSource("iqiyidiscover"), ""),
|
||||
],
|
||||
)
|
||||
def test_subscribe_media_identity_matches_unknown_subscription_year(
|
||||
self, card_source, subscription_source, card_year,
|
||||
):
|
||||
"""跨源卡片的年份可有可无,但订阅无年份时仍能回显。"""
|
||||
from app.api.endpoints.subscribe import subscribe_media_identity
|
||||
|
||||
repository = _SubscriptionRepositoryFake(
|
||||
_EndpointSubscribe(
|
||||
id=34,
|
||||
username="alice",
|
||||
name="未定档剧",
|
||||
year=None,
|
||||
type=MediaType.TV.value,
|
||||
media_source=subscription_source,
|
||||
media_id="existing-id",
|
||||
season=1,
|
||||
)
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
subscribe_media_identity(
|
||||
media_id="card-id",
|
||||
media_source=card_source,
|
||||
title="未定档剧 第一季",
|
||||
year=card_year,
|
||||
mtype=MediaType.TV,
|
||||
query=_subscription_query(repository),
|
||||
current_user=_EndpointUser(name="alice", is_superuser=False),
|
||||
)
|
||||
)
|
||||
|
||||
assert result.id == 34
|
||||
repository.async_list_by_title.assert_awaited_once_with(title="未定档剧", season=1)
|
||||
|
||||
@pytest.mark.parametrize("card_year", [None, "", "2025"])
|
||||
def test_subscribe_media_identity_rejects_conflicting_known_year(
|
||||
self, card_year,
|
||||
):
|
||||
"""无年份卡片不能串到已定档剧,已知年份也不能匹配冲突年份。"""
|
||||
from app.api.endpoints.subscribe import subscribe_media_identity
|
||||
|
||||
repository = _SubscriptionRepositoryFake(
|
||||
_EndpointSubscribe(
|
||||
id=35,
|
||||
username="alice",
|
||||
name="同名剧",
|
||||
year="2024",
|
||||
type=MediaType.TV.value,
|
||||
media_source=MediaSource.Douban,
|
||||
media_id="existing-id",
|
||||
)
|
||||
)
|
||||
result = asyncio.run(
|
||||
subscribe_media_identity(
|
||||
media_id="card-id",
|
||||
media_source=MediaSource.TMDB,
|
||||
title="同名剧",
|
||||
year=card_year,
|
||||
mtype=MediaType.TV,
|
||||
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_subscribe_media_identity_prefers_matching_year_over_unknown_year(self):
|
||||
"""同名订阅同时含已知和未知年份时,优先返回确切年份。"""
|
||||
from app.api.endpoints.subscribe import subscribe_media_identity
|
||||
|
||||
repository = _SubscriptionRepositoryFake(
|
||||
_EndpointSubscribe(
|
||||
id=36, username="alice", name="同名剧", year=None,
|
||||
type=MediaType.TV.value, media_source=MediaSource.Douban,
|
||||
media_id="undated",
|
||||
),
|
||||
_EndpointSubscribe(
|
||||
id=37, username="alice", name="同名剧", year="2026",
|
||||
type=MediaType.TV.value, media_source=MediaSource.Douban,
|
||||
media_id="dated",
|
||||
),
|
||||
)
|
||||
result = asyncio.run(
|
||||
subscribe_media_identity(
|
||||
media_id="card-id",
|
||||
media_source=MediaSource.TMDB,
|
||||
title="同名剧",
|
||||
year="2026",
|
||||
mtype=MediaType.TV,
|
||||
query=_subscription_query(repository),
|
||||
current_user=_EndpointUser(name="alice", is_superuser=False),
|
||||
)
|
||||
)
|
||||
|
||||
assert result.id == 37
|
||||
|
||||
def test_delete_subscribe_by_media_identity_deletes_owner_candidate(self):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user