mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 18:24:42 +08:00
289 lines
9.8 KiB
Python
289 lines
9.8 KiB
Python
from typing import Annotated, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
from app import schemas
|
|
from app.chain.media import MediaChain
|
|
from app.chain.music import MusicChain
|
|
from app.schemas.types import MediaType
|
|
from app.core.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
|
|
from app.core.security import verify_token
|
|
from app.db.models.user import User
|
|
from app.db.user_oper import get_current_active_superuser_async
|
|
from app.modules.listenbrainz import (
|
|
LISTENBRAINZ_CHART_RANGES,
|
|
LISTENBRAINZ_FRESH_MAX_DAYS,
|
|
LISTENBRAINZ_FRESH_SORTS,
|
|
)
|
|
from app.modules.musicbrainz.music_cache import MusicBrainzCache
|
|
|
|
router = APIRouter()
|
|
|
|
CountParam = Annotated[int, Query(ge=1, le=100)]
|
|
PageParam = Annotated[int, Query(ge=1)]
|
|
MusicSourceParam = Annotated[
|
|
str,
|
|
Query(pattern="^(musicbrainz|theaudiodb|doubanmusic)$"),
|
|
]
|
|
MusicExploreSourceParam = Annotated[
|
|
str,
|
|
Query(pattern="^(musicbrainz|doubanmusic)$"),
|
|
]
|
|
MusicModeParam = Annotated[str, Query(pattern="^(chart|fresh)$")]
|
|
MusicEntityParam = Annotated[str, Query(pattern="^(recording|album)$")]
|
|
MusicRangeParam = Annotated[str, Query(pattern=f"^({'|'.join(LISTENBRAINZ_CHART_RANGES)})$")]
|
|
MusicSortParam = Annotated[str, Query(pattern="^listen_count\\.(desc|asc)$")]
|
|
MusicFreshSortParam = Annotated[str, Query(pattern=f"^({'|'.join(LISTENBRAINZ_FRESH_SORTS)})$")]
|
|
DoubanMusicSortParam = Annotated[str, Query(pattern="^(U|S|R|O)$")]
|
|
MusicDaysParam = Annotated[int, Query(ge=1, le=LISTENBRAINZ_FRESH_MAX_DAYS)]
|
|
# MusicBrainz 浏览接口支持的专辑类型筛选
|
|
MusicAlbumTypeParam = Annotated[
|
|
Optional[str],
|
|
Query(pattern="^(album|single|ep|broadcast|other|compilation|soundtrack|live|remix)$"),
|
|
]
|
|
|
|
|
|
def _serialize_music(info: MusicInfo) -> schemas.MusicInfo:
|
|
"""将内部音乐信息转换为 REST 响应模型。"""
|
|
return schemas.MusicInfo(**info.to_dict())
|
|
|
|
|
|
def _serialize_album(info: MusicAlbumInfo) -> schemas.MusicAlbumInfo:
|
|
"""将内部专辑信息转换为 REST 响应模型。"""
|
|
return schemas.MusicAlbumInfo(**info.to_dict())
|
|
|
|
|
|
def _serialize_artist(info: MusicArtistInfo) -> schemas.MusicArtistInfo:
|
|
"""将内部艺术家信息转换为 REST 响应模型。"""
|
|
return schemas.MusicArtistInfo(**info.to_dict())
|
|
|
|
|
|
@router.post(
|
|
"/recognize",
|
|
summary="识别音乐元数据详情",
|
|
response_model=schemas.MusicInfo,
|
|
)
|
|
async def recognize_music(
|
|
request: schemas.MusicRecognizeRequest,
|
|
_: schemas.TokenPayload = Depends(verify_token),
|
|
) -> schemas.MusicInfo:
|
|
"""根据音乐元数据来源和媒体 ID 获取标准详情,与影视识别共用统一入口。"""
|
|
recognize_kwargs = {
|
|
"media_source": request.media_source,
|
|
"media_id": request.media_id,
|
|
"mtype": MediaType.MUSIC,
|
|
}
|
|
if request.music_type is not None:
|
|
recognize_kwargs["music_type"] = request.music_type
|
|
info = await MediaChain().async_recognize_media(
|
|
**recognize_kwargs,
|
|
)
|
|
if not info:
|
|
raise HTTPException(status_code=404, detail="未识别到音乐信息")
|
|
return _serialize_music(info)
|
|
|
|
|
|
@router.get(
|
|
"/cache", summary="查询音乐识别缓存", response_model=schemas.Response
|
|
)
|
|
async def music_recognition_cache(
|
|
_: User = Depends(get_current_active_superuser_async),
|
|
) -> schemas.Response:
|
|
"""查询可管理的 MusicBrainz 识别缓存。"""
|
|
cache_items = MusicBrainzCache().list_items()
|
|
recognized_count = sum(1 for item in cache_items if item["media_id"])
|
|
return schemas.Response(
|
|
success=True,
|
|
data={
|
|
"count": len(cache_items),
|
|
"recognized": recognized_count,
|
|
"unrecognized": len(cache_items) - recognized_count,
|
|
"data": cache_items,
|
|
},
|
|
)
|
|
|
|
|
|
@router.delete(
|
|
"/cache/{cache_key:path}",
|
|
summary="删除指定音乐识别缓存",
|
|
response_model=schemas.Response,
|
|
)
|
|
async def delete_music_recognition_cache(
|
|
cache_key: str,
|
|
_: User = Depends(get_current_active_superuser_async),
|
|
) -> schemas.Response:
|
|
"""按缓存键删除单条 MusicBrainz 识别缓存。"""
|
|
deleted_item = MusicBrainzCache().delete(cache_key)
|
|
if not deleted_item:
|
|
return schemas.Response(success=False, message="音乐识别缓存不存在")
|
|
return schemas.Response(success=True, message="音乐识别缓存删除成功")
|
|
|
|
|
|
@router.delete(
|
|
"/cache", summary="清空音乐识别缓存", response_model=schemas.Response
|
|
)
|
|
async def clear_music_recognition_cache(
|
|
_: User = Depends(get_current_active_superuser_async),
|
|
) -> schemas.Response:
|
|
"""清空全部 MusicBrainz 识别缓存。"""
|
|
MusicBrainzCache().clear()
|
|
return schemas.Response(success=True, message="音乐识别缓存清理完成")
|
|
|
|
|
|
@router.get(
|
|
"/explore",
|
|
summary="探索音乐",
|
|
response_model=list[schemas.MusicInfo],
|
|
)
|
|
async def explore_music(
|
|
page: PageParam = 1,
|
|
count: CountParam = 30,
|
|
media_source: MusicExploreSourceParam = "musicbrainz",
|
|
mode: MusicModeParam = "chart",
|
|
entity: MusicEntityParam = "recording",
|
|
range_name: MusicRangeParam = "this_month",
|
|
sort_by: MusicSortParam = "listen_count.desc",
|
|
sort: MusicFreshSortParam = "release_date",
|
|
days: MusicDaysParam = 14,
|
|
past: bool = True,
|
|
future: bool = True,
|
|
min_listen_count: Annotated[int, Query(ge=0)] = 0,
|
|
with_cover: bool = False,
|
|
tags: str = "",
|
|
douban_sort: DoubanMusicSortParam = "U",
|
|
_: schemas.TokenPayload = Depends(verify_token),
|
|
) -> list[schemas.MusicInfo]:
|
|
"""MusicBrainz 返回榜单或新发行,豆瓣音乐固定按官方标签分类浏览。"""
|
|
chain = MusicChain()
|
|
if media_source != "musicbrainz":
|
|
results = await chain.async_discover(
|
|
media_source=media_source,
|
|
page=page,
|
|
count=count,
|
|
entity=entity,
|
|
mode="tag",
|
|
tags=tags,
|
|
sort=douban_sort,
|
|
)
|
|
elif mode == "fresh":
|
|
results = await chain.async_fresh_releases(
|
|
days=days,
|
|
sort=sort,
|
|
past=past,
|
|
future=future,
|
|
page=page,
|
|
count=count,
|
|
with_cover=with_cover,
|
|
)
|
|
else:
|
|
results = await chain.async_chart(
|
|
range_name=range_name,
|
|
page=page,
|
|
count=count,
|
|
sort_by=sort_by,
|
|
min_listen_count=min_listen_count,
|
|
with_cover=with_cover,
|
|
entity=entity,
|
|
)
|
|
if media_source != "musicbrainz" and with_cover:
|
|
results = [info for info in results if info.cover_url or info.poster_path]
|
|
return [_serialize_music(info) for info in results]
|
|
|
|
|
|
@router.get(
|
|
"/album/{album_id}",
|
|
summary="查询音乐专辑详情",
|
|
response_model=schemas.MusicAlbumInfo,
|
|
)
|
|
async def music_album(
|
|
album_id: str,
|
|
media_source: MusicSourceParam = "musicbrainz",
|
|
_: schemas.TokenPayload = Depends(verify_token),
|
|
) -> schemas.MusicAlbumInfo:
|
|
"""按专辑标准 ID 返回专辑详情、曲目列表和发行版本。"""
|
|
info = await MusicChain().async_album(media_source=media_source, media_id=album_id)
|
|
if not info:
|
|
raise HTTPException(status_code=404, detail="未识别到专辑信息")
|
|
return _serialize_album(info)
|
|
|
|
|
|
@router.get(
|
|
"/album/{album_id}/related",
|
|
summary="查询关联音乐专辑",
|
|
response_model=list[schemas.MusicInfo],
|
|
)
|
|
async def music_album_related(
|
|
album_id: str,
|
|
count: CountParam = 24,
|
|
media_source: MusicSourceParam = "musicbrainz",
|
|
_: schemas.TokenPayload = Depends(verify_token),
|
|
) -> list[schemas.MusicInfo]:
|
|
"""按来源和专辑 ID 返回可继续浏览的关联专辑。"""
|
|
results = await MusicChain().async_album_related(
|
|
media_source=media_source,
|
|
media_id=album_id,
|
|
count=count,
|
|
)
|
|
return [_serialize_music(info) for info in results]
|
|
|
|
|
|
@router.get(
|
|
"/artist/{artist_id}/albums",
|
|
summary="查询艺术家的专辑列表",
|
|
response_model=list[schemas.MusicInfo],
|
|
)
|
|
async def music_artist_albums(
|
|
artist_id: str,
|
|
page: PageParam = 1,
|
|
count: CountParam = 30,
|
|
album_type: MusicAlbumTypeParam = None,
|
|
media_source: MusicSourceParam = "musicbrainz",
|
|
_: schemas.TokenPayload = Depends(verify_token),
|
|
) -> list[schemas.MusicInfo]:
|
|
"""按艺术家标准 ID 分页返回其专辑、EP 和单曲。"""
|
|
results = await MusicChain().async_artist_albums(
|
|
media_source=media_source,
|
|
media_id=artist_id,
|
|
page=page,
|
|
count=count,
|
|
album_type=album_type,
|
|
)
|
|
return [_serialize_music(info) for info in results]
|
|
|
|
|
|
@router.get(
|
|
"/artist/{artist_id}/related",
|
|
summary="查询关联艺术家",
|
|
response_model=list[schemas.MusicArtistInfo],
|
|
)
|
|
async def music_artist_related(
|
|
artist_id: str,
|
|
count: CountParam = 24,
|
|
media_source: MusicSourceParam = "musicbrainz",
|
|
_: schemas.TokenPayload = Depends(verify_token),
|
|
) -> list[schemas.MusicArtistInfo]:
|
|
"""按艺术家关系返回可继续浏览的关联艺术家。"""
|
|
results = await MusicChain().async_artist_related(
|
|
media_source=media_source,
|
|
media_id=artist_id,
|
|
count=count,
|
|
)
|
|
return [_serialize_artist(info) for info in results]
|
|
|
|
|
|
@router.get(
|
|
"/artist/{artist_id}",
|
|
summary="查询音乐艺术家详情",
|
|
response_model=schemas.MusicArtistInfo,
|
|
)
|
|
async def music_artist(
|
|
artist_id: str,
|
|
media_source: MusicSourceParam = "musicbrainz",
|
|
_: schemas.TokenPayload = Depends(verify_token),
|
|
) -> schemas.MusicArtistInfo:
|
|
"""按艺术家标准 ID 返回艺术家详情。"""
|
|
info = await MusicChain().async_artist(media_source=media_source, media_id=artist_id)
|
|
if not info:
|
|
raise HTTPException(status_code=404, detail="未识别到艺术家信息")
|
|
return _serialize_artist(info)
|