diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py index 587a11851..028054b2c 100644 --- a/app/api/endpoints/media.py +++ b/app/api/endpoints/media.py @@ -16,7 +16,7 @@ from app.db.user_oper import get_current_active_user, get_current_active_superus from app.schemas import MediaType, MediaRecognizeConvertEventData from app.schemas.category import CategoryConfig from app.schemas.types import ChainEventType -from app.utils.media import parse_media_key +from app.utils.media import MEDIA_SOURCE_ID_FIELDS, parse_media_key router = APIRouter() MediaSource = str @@ -414,8 +414,10 @@ async def detail( mediaid=source_media_id, mtype=mtype, ) - else: - # 广播事件解析媒体信息 + if not mediainfo and ( + not media_source or media_source not in MEDIA_SOURCE_ID_FIELDS + ): + # 旧探索插件可能只提供列表或转换事件,原生 ID 直识别失败后需保留原有兼容链路。 event_data = MediaRecognizeConvertEventData( mediaid=mediaid, convert_type=settings.RECOGNIZE_SOURCE ) @@ -432,7 +434,7 @@ async def detail( mediaid=str(new_id), mtype=mtype, ) - elif title: + if not mediainfo and title: # 使用名称识别兜底 meta = MetaInfo(title) if year: diff --git a/tests/test_search_media_sources.py b/tests/test_search_media_sources.py index 4db81050c..97aa632ac 100644 --- a/tests/test_search_media_sources.py +++ b/tests/test_search_media_sources.py @@ -119,6 +119,132 @@ def test_media_detail_forwards_custom_plugin_source(monkeypatch) -> None: assert captured["mediaid"] == "custom-1" +def test_media_detail_falls_back_to_title_for_legacy_discover_source( + monkeypatch, +) -> None: + """仅提供探索列表的旧插件应在原生 ID 识别失败后按标题年份兜底。""" + media = MediaInfo( + source="themoviedb", + type=MediaType.TV, + title="旧版剧集", + tmdb_id=12345, + ) + media_chain = Mock() + media_chain.async_recognize_media = AsyncMock(return_value=None) + media_chain.async_recognize_by_meta = AsyncMock(return_value=media) + media_chain.async_obtain_images = AsyncMock(return_value=None) + monkeypatch.setattr(media_endpoint, "MediaChain", Mock(return_value=media_chain)) + monkeypatch.setattr( + media_endpoint.eventmanager, + "async_send_event", + AsyncMock(return_value=None), + ) + + result = asyncio.run( + media_endpoint.detail( + mediaid="tvdb:81189", + type_name=MediaType.TV.value, + title="旧版剧集", + year="2026", + _=None, + ) + ) + + assert result["tmdb_id"] == 12345 + media_chain.async_recognize_media.assert_awaited_once_with( + source="tvdb", + mediaid="81189", + mtype=MediaType.TV, + ) + fallback_meta = media_chain.async_recognize_by_meta.await_args.args[0] + assert fallback_meta.name == "旧版剧集" + assert fallback_meta.year == "2026" + assert fallback_meta.type == MediaType.TV + + +def test_media_detail_uses_convert_event_for_legacy_discover_source( + monkeypatch, +) -> None: + """旧探索插件提供 ID 转换事件时应优先转换到系统识别源。""" + media = MediaInfo( + source="themoviedb", + type=MediaType.MOVIE, + title="转换电影", + tmdb_id=54321, + ) + event_data = media_endpoint.MediaRecognizeConvertEventData( + mediaid="legacy:42", + convert_type="themoviedb", + ) + event_data.media_dict["id"] = 54321 + media_chain = Mock() + media_chain.async_recognize_media = AsyncMock(side_effect=[None, media]) + media_chain.async_recognize_by_meta = AsyncMock( + side_effect=AssertionError("转换成功后不应再按标题识别") + ) + media_chain.async_obtain_images = AsyncMock(return_value=None) + monkeypatch.setattr(media_endpoint, "MediaChain", Mock(return_value=media_chain)) + monkeypatch.setattr( + media_endpoint.eventmanager, + "async_send_event", + AsyncMock(return_value=Mock(event_data=event_data)), + ) + + result = asyncio.run( + media_endpoint.detail( + mediaid="legacy:42", + type_name=MediaType.MOVIE.value, + title="转换电影", + year="2026", + _=None, + ) + ) + + assert result["tmdb_id"] == 54321 + assert media_chain.async_recognize_media.await_count == 2 + assert media_chain.async_recognize_media.await_args_list[0].kwargs == { + "source": "legacy", + "mediaid": "42", + "mtype": MediaType.MOVIE, + } + assert media_chain.async_recognize_media.await_args_list[1].kwargs == { + "source": "themoviedb", + "mediaid": "54321", + "mtype": MediaType.MOVIE, + } + media_chain.async_recognize_by_meta.assert_not_awaited() + + +def test_media_detail_does_not_fallback_for_builtin_source(monkeypatch) -> None: + """内置来源的明确 ID 查询失败时不应被标题识别替换身份。""" + media_chain = Mock() + media_chain.async_recognize_media = AsyncMock(return_value=None) + media_chain.async_recognize_by_meta = AsyncMock( + side_effect=AssertionError("不应按标题切换识别源") + ) + convert_event = AsyncMock(return_value=None) + monkeypatch.setattr(media_endpoint, "MediaChain", Mock(return_value=media_chain)) + monkeypatch.setattr( + media_endpoint.eventmanager, + "async_send_event", + convert_event, + ) + + result = asyncio.run( + media_endpoint.detail( + mediaid="tmdb:999999", + type_name=MediaType.MOVIE.value, + title="错误兜底电影", + year="2026", + _=None, + ) + ) + + assert isinstance(result, media_endpoint.schemas.MediaInfo) + media_chain.async_recognize_by_meta.assert_not_awaited() + convert_event.assert_not_awaited() + + def test_media_seasons_builds_anilist_season_response(monkeypatch) -> None: """AniList 详情应能通过统一季信息接口返回剧集季。""" captured = {}