mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-18 04:34:00 +08:00
feat(v3): enhance music server integration with Navidrome support
This commit is contained in:
@@ -33,6 +33,7 @@ def _build_statistic(db: Session, name: Optional[str] = None) -> schemas.Statist
|
||||
for media_statistic in media_statistics:
|
||||
ret_statistic.movie_count += media_statistic.movie_count or 0
|
||||
ret_statistic.tv_count += media_statistic.tv_count or 0
|
||||
ret_statistic.music_count += media_statistic.music_count or 0
|
||||
ret_statistic.user_count += media_statistic.user_count or 0
|
||||
if media_statistic.episode_count is not None:
|
||||
ret_statistic.episode_count += media_statistic.episode_count or 0
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from app import schemas
|
||||
from app.chain.music import MusicChain
|
||||
from app.core.music import MusicInfo
|
||||
from app.core.music import MusicAlbumInfo, MusicArtistInfo, MusicInfo
|
||||
from app.core.security import verify_token
|
||||
from app.modules.listenbrainz import (
|
||||
LISTENBRAINZ_CHART_RANGES,
|
||||
LISTENBRAINZ_FRESH_MAX_DAYS,
|
||||
LISTENBRAINZ_FRESH_SORTS,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CountParam = Annotated[int, Query(ge=1, le=100)]
|
||||
MusicRangeParam = Annotated[
|
||||
str,
|
||||
Query(pattern="^(this_week|this_month|this_year|all_time)$"),
|
||||
]
|
||||
MusicSortParam = Annotated[
|
||||
str,
|
||||
Query(pattern="^listen_count\\.(desc|asc)$"),
|
||||
PageParam = Annotated[int, Query(ge=1)]
|
||||
MusicSourceParam = Annotated[str, Query(pattern="^musicbrainz$")]
|
||||
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)})$")]
|
||||
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)$"),
|
||||
]
|
||||
|
||||
|
||||
@@ -25,6 +35,16 @@ def _serialize_music(info: MusicInfo) -> schemas.MusicInfo:
|
||||
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.get(
|
||||
"/search",
|
||||
summary="搜索音乐元数据",
|
||||
@@ -61,25 +81,122 @@ async def recognize_music(
|
||||
|
||||
@router.get(
|
||||
"/explore",
|
||||
summary="探索热门音乐",
|
||||
summary="探索音乐",
|
||||
response_model=list[schemas.MusicInfo],
|
||||
)
|
||||
async def explore_music(
|
||||
page: Annotated[int, Query(ge=1)] = 1,
|
||||
page: PageParam = 1,
|
||||
count: CountParam = 30,
|
||||
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,
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> list[schemas.MusicInfo]:
|
||||
"""按周期、热度和封面条件返回可搜索和订阅的音乐候选。"""
|
||||
results = await MusicChain().async_chart(
|
||||
range_name=range_name,
|
||||
"""按 ListenBrainz 官方热门榜单或新发行两种模式返回可订阅的音乐候选。"""
|
||||
chain = MusicChain()
|
||||
if 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,
|
||||
)
|
||||
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,
|
||||
source: MusicSourceParam = "musicbrainz",
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> schemas.MusicAlbumInfo:
|
||||
"""按专辑标准 ID 返回专辑详情、曲目列表和发行版本。"""
|
||||
info = await MusicChain().async_album(source=source, media_id=album_id)
|
||||
if not info:
|
||||
raise HTTPException(status_code=404, detail="未识别到专辑信息")
|
||||
return _serialize_album(info)
|
||||
|
||||
|
||||
@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,
|
||||
source: MusicSourceParam = "musicbrainz",
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> list[schemas.MusicInfo]:
|
||||
"""按艺术家标准 ID 分页返回其专辑、EP 和单曲。"""
|
||||
results = await MusicChain().async_artist_albums(
|
||||
source=source,
|
||||
media_id=artist_id,
|
||||
page=page,
|
||||
count=count,
|
||||
sort_by=sort_by,
|
||||
min_listen_count=min_listen_count,
|
||||
with_cover=with_cover,
|
||||
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,
|
||||
source: MusicSourceParam = "musicbrainz",
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> list[schemas.MusicArtistInfo]:
|
||||
"""按艺术家关系返回可继续浏览的关联艺术家。"""
|
||||
results = await MusicChain().async_artist_related(
|
||||
source=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,
|
||||
source: MusicSourceParam = "musicbrainz",
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> schemas.MusicArtistInfo:
|
||||
"""按艺术家标准 ID 返回艺术家详情。"""
|
||||
info = await MusicChain().async_artist(source=source, media_id=artist_id)
|
||||
if not info:
|
||||
raise HTTPException(status_code=404, detail="未识别到艺术家信息")
|
||||
return _serialize_artist(info)
|
||||
|
||||
Reference in New Issue
Block a user