fix(api): 修复音乐详情响应校验

This commit is contained in:
jxxghp
2026-09-05 16:42:15 +08:00
parent 0ed64d9380
commit 7251400237
5 changed files with 58 additions and 6 deletions
+3 -2
View File
@@ -25,6 +25,7 @@ from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.schemas.category import CategoryConfig as _SchemaCategoryConfig
from app.schemas.category import MediaCategoryMap as _SchemaMediaCategoryMap
from app.schemas.context import MediaDetailResult as _SchemaMediaDetailResult
from app.schemas.context import MediaEpisodeGroup as _SchemaMediaEpisodeGroup
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.context import MediaSearchResults as _SchemaMediaSearchResults
@@ -624,7 +625,7 @@ async def seasons(
return []
@router.get("/{media_id}", summary="查询媒体详情", response_model=_SchemaMediaInfo)
@router.get("/{media_id}", summary="查询媒体详情", response_model=_SchemaMediaDetailResult)
async def detail(
media_id: str,
media_source: MediaSource,
@@ -632,7 +633,7 @@ async def detail(
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
根据媒体来源和原生 ID 查询媒体信息,type_name: 电影/电视剧
根据媒体来源和原生 ID 查询影视或音乐信息
"""
mtype = MediaType(type_name)
normalized_source, normalized_media_id = resolve_media_identity(
+12 -3
View File
@@ -539,8 +539,8 @@ class MediaPerson(BaseModel):
relation: Optional[str] = None
def _media_search_result_kind(value: Any) -> str:
"""按稳定字段区分音乐、人物与影视/合集搜索结果。"""
def _media_result_kind(value: Any) -> str:
"""按稳定字段区分音乐、人物与影视/合集结果。"""
if isinstance(value, BaseModel):
value = value.model_dump()
if isinstance(value, dict):
@@ -551,13 +551,22 @@ def _media_search_result_kind(value: Any) -> str:
return "media"
MediaDetailResult = Annotated[
Union[
Annotated[MusicInfo, Tag("music")],
Annotated[MediaInfo, Tag("media")],
],
Discriminator(_media_result_kind),
]
MediaSearchResult = Annotated[
Union[
Annotated[MusicInfo, Tag("music")],
Annotated[MediaPerson, Tag("person")],
Annotated[MediaInfo, Tag("media")],
],
Discriminator(_media_search_result_kind),
Discriminator(_media_result_kind),
]
+1
View File
@@ -242,6 +242,7 @@ SCHEMA_EXPORTS = {
'MediaCompany': ('app.schemas.context', 'MediaCompany'),
'MediaCountry': ('app.schemas.context', 'MediaCountry'),
'MediaCredit': ('app.schemas.context', 'MediaCredit'),
'MediaDetailResult': ('app.schemas.context', 'MediaDetailResult'),
'MediaEpisode': ('app.schemas.context', 'MediaEpisode'),
'MediaEpisodeGroup': ('app.schemas.context', 'MediaEpisodeGroup'),
'MediaEpisodeGroupNetwork': ('app.schemas.context', 'MediaEpisodeGroupNetwork'),
+1 -1
View File
@@ -274,7 +274,7 @@ FastAPI 的 HTTP 异常和参数校验异常统一使用 `message`,不再返
| GET | `/api/v1/media/search` | 按标题搜索媒体、合集、人物或音乐,参数:`title``type``page``count`,可重复传入可选 `media_source`;内置模块只处理自身支持的来源,插件模块可以处理其注册的扩展来源,旧客户端的逗号格式仅在输入边界兼容 |
| GET | `/api/v1/media/recognize` | 识别标题,参数:`title``subtitle``custom_words`,可选 `media_source`;当 `title` 为含目录的媒体文件路径时,会合并父目录中的名称、年份等信息 |
| GET | `/api/v1/media/recognize_file` | 识别文件路径,参数:`path`,可选 `media_source` |
| GET | `/api/v1/media/{media_id}` | 按原生 ID 查询媒体详情;必填参数:`media_source``type_name`,其中 `media_source` 与路径中的 `media_id` 组成统一媒体身份 |
| 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`(电影/电视剧/音乐)。音乐会按策略处理音频标签、封面和歌词 |
| POST | `/api/v1/transfer/manual/target-path` | 按源文件与目录配置匹配手动整理目标路径;请求体为 `ManualTransferItem`,该接口不执行媒体识别 |
| POST | `/api/v1/transfer/manual/history` | 查询文件、批量文件或目录命中的成功整理历史摘要,用于进入手动整理界面时显示重新整理状态 |
+41
View File
@@ -1,11 +1,15 @@
from unittest.mock import AsyncMock, Mock
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from app import schemas
from app.api.endpoints import media as media_endpoint
from app.api.endpoints import mediaserver as mediaserver_endpoint
from app.api.response import ResponseAPIRouter
from app.domain.context import MediaInfo as CoreMediaInfo
from app.domain.context import MusicInfo as CoreMusicInfo
from app.schemas.types import MediaSource, MediaType
@@ -121,6 +125,43 @@ async def test_douban_media_response_filters_unknown_season_years() -> None:
assert response.json()["data"][0]["season_years"] == {"2": "2025"}
@pytest.mark.asyncio
async def test_media_detail_response_accepts_music_numeric_year(monkeypatch) -> None:
"""通用详情接口应按音乐模型序列化 MusicBrainz 的整数年份。"""
media_id = "b79e2ffd-7e44-4dbf-91f9-167c05d1fc91"
music = CoreMusicInfo(
media_source=MediaSource.MusicBrainz,
media_id=media_id,
title="示例单曲",
artists=["示例歌手"],
album="示例专辑",
year=2004,
)
media_chain = Mock()
media_chain.async_recognize_media = AsyncMock(return_value=music)
media_chain.async_obtain_images = AsyncMock(return_value=None)
monkeypatch.setattr(media_endpoint, "MediaChain", Mock(return_value=media_chain))
app = FastAPI()
app.dependency_overrides[media_endpoint.verify_token] = lambda: None
app.include_router(media_endpoint.router, prefix="/api/v1/media")
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as client:
response = await client.get(
f"/api/v1/media/{media_id}",
params={"media_source": "musicbrainz", "type_name": "音乐"},
)
assert response.status_code == 200
result = response.json()["data"]
assert result["type"] == MediaType.MUSIC.value
assert result["music_type"] == "recording"
assert result["artists"] == ["示例歌手"]
assert result["year"] == 2004
@pytest.mark.asyncio
async def test_media_response_accepts_legacy_source_key() -> None:
"""媒体身份重构前缓存的旧格式条目(source + media_id)应被归一化并正常响应。"""