diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py index ef1e67c4f..3782eade0 100644 --- a/app/api/endpoints/media.py +++ b/app/api/endpoints/media.py @@ -271,9 +271,8 @@ async def recognize( metainfo, media_source=media_source, ) - if mediainfo: - return Context(meta_info=metainfo, media_info=mediainfo).to_dict() - return _SchemaContext() + # 未匹配到媒体时也返回解析元数据,便于识别测试查看识别词应用情况和解析结果 + return Context(meta_info=metainfo, media_info=mediainfo).to_dict() @router.get( diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 905479980..46540a131 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -443,7 +443,7 @@ FastAPI 的 HTTP 异常和参数校验异常统一使用 `message`,不再返 | 方法 | 路径 | 说明 | | :--- | :--- | :--- | | GET | `/api/v1/media/search` | 按标题搜索媒体、合集、影视人物或音乐;人物结果可同时包含 MusicBrainz 艺术家,参数:`title`、`type`、`page`、`count`,可重复传入可选 `media_source`;未指定 `music_type` 时音乐搜索只返回单曲和专辑,显式 `music_type` 仍可用于专用实体选择;内置模块只处理自身支持的来源,插件模块可以处理其注册的扩展来源,旧客户端的逗号格式仅在输入边界兼容 | -| GET | `/api/v1/media/recognize` | 识别标题,参数:`title`、`subtitle`、`custom_words`,可选 `media_source`;当 `title` 为含目录的媒体文件路径时,会合并父目录中的名称、年份等信息 | +| GET | `/api/v1/media/recognize` | 识别标题,参数:`title`、`subtitle`、`custom_words`,可选 `media_source`;当 `title` 为含目录的媒体文件路径时,会合并父目录中的名称、年份等信息;未匹配到媒体时仍返回解析元数据 `meta_info`(含实际应用的识别词 `apply_words`),`media_info` 为空 | | GET | `/api/v1/media/recognize_file` | 识别文件路径,参数:`path`,可选 `media_source` | | GET | `/api/v1/media/{media_id}` | 按原生 ID 查询影视或音乐详情;必填参数:`media_source`、`type_name`,其中 `media_source` 与路径中的 `media_id` 组成统一媒体身份,`type_name` 支持电影、电视剧和音乐 | | POST | `/api/v1/media/scrape/{storage}` | 刮削媒体元数据;请求体为 `FileItem`,可选查询参数 `media_source`、`media_id`、`type_name`(电影/电视剧/音乐)和 TMDB `episode_group`。剧集组用于指定电视剧的季集顺序;音乐会按策略处理音频标签、封面和歌词。会覆盖目标位置已有的图片、NFO 和音频标签,与文件管理、手动整理一样需要管理权限 | diff --git a/scripts/generate_agent_skill_docs.py b/scripts/generate_agent_skill_docs.py index 29644ad04..8b4b62460 100644 --- a/scripts/generate_agent_skill_docs.py +++ b/scripts/generate_agent_skill_docs.py @@ -299,9 +299,11 @@ _OPERATION_NOTES: dict[str, list[str]] = { ], "media.recognize": [ "- **Agent note:** Recognition returns a context containing `media_info`, not a candidate list. " - "Check its title and media type against the user's request. If recognition is missing or " - "ambiguous, use `media.search` and compare candidates by title, type, and year. Preserve " - "the selected result's exact `media_source` + `media_id` for `search.torrents`.", + "A failed match still returns parsed `meta_info` with the applied recognition words, so treat " + "recognition as successful only when `media_info` is present. Check its title and media type " + "against the user's request. If recognition is missing or ambiguous, use `media.search` and " + "compare candidates by title, type, and year. Preserve the selected result's exact " + "`media_source` + `media_id` for `search.torrents`.", ], "search.torrents": [ "- **Agent note:** `media_id` is a **path** parameter and MUST be nested under " diff --git a/skills/moviepilot-api/api/media.md b/skills/moviepilot-api/api/media.md index daf7b9262..15457e9ed 100644 --- a/skills/moviepilot-api/api/media.md +++ b/skills/moviepilot-api/api/media.md @@ -182,7 +182,7 @@ Identity scope: `bound_user`; required role: `user`. - `path_params`: none - `query`: `custom_words` (string|null): Custom recognition or rename words applied to this media workflow.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `subtitle` (string|null): Optional subtitle text used together with title during media recognition.; `title*` (string): Media, torrent, subscription, or history title used by the operation. - `body`: none -- **Agent note:** Recognition returns a context containing `media_info`, not a candidate list. Check its title and media type against the user's request. If recognition is missing or ambiguous, use `media.search` and compare candidates by title, type, and year. Preserve the selected result's exact `media_source` + `media_id` for `search.torrents`. +- **Agent note:** Recognition returns a context containing `media_info`, not a candidate list. A failed match still returns parsed `meta_info` with the applied recognition words, so treat recognition as successful only when `media_info` is present. Check its title and media type against the user's request. If recognition is missing or ambiguous, use `media.search` and compare candidates by title, type, and year. Preserve the selected result's exact `media_source` + `media_id` for `search.torrents`. ### `media.recognize_file` `GET /api/v1/media/recognize_file`; policy effect: `safe_read`. diff --git a/tests/test_media_recognize_endpoint.py b/tests/test_media_recognize_endpoint.py index c789f94a7..7d89f22cb 100644 --- a/tests/test_media_recognize_endpoint.py +++ b/tests/test_media_recognize_endpoint.py @@ -87,3 +87,22 @@ def test_recognize_audio_file_title_uses_music_meta_without_source() -> None: metainfo = chain.async_recognize_by_meta.await_args.args[0] assert isinstance(metainfo, MetaMusic) assert metainfo.audio_format + + +def test_recognize_returns_meta_info_when_media_lookup_fails() -> None: + """未匹配到媒体时也应返回解析元数据,供识别测试展示识别词应用情况和解析结果。""" + chain = Mock() + chain.async_recognize_by_meta = AsyncMock(return_value=None) + + with patch("app.api.endpoints.media.MediaChain", return_value=chain): + result = asyncio.run( + recognize( + title="Some.Release.2024", + custom_words="Some => Other", + _=Mock(), + ) + ) + + assert result["media_info"] is None + assert result["meta_info"]["org_string"] == "Other.Release.2024" + assert result["meta_info"]["apply_words"] == ["Some => Other"]