Files
musicseerr/backend/api/v1/routes/library.py
T
2026-04-03 15:53:00 +01:00

161 lines
5.6 KiB
Python

import asyncio
import logging
from fastapi import APIRouter, Depends, HTTPException
from api.v1.schemas.library import (
LibraryResponse,
LibraryArtistsResponse,
LibraryAlbumsResponse,
PaginatedLibraryAlbumsResponse,
PaginatedLibraryArtistsResponse,
RecentlyAddedResponse,
LibraryStatsResponse,
AlbumRemoveResponse,
AlbumRemovePreviewResponse,
SyncLibraryResponse,
LibraryMbidsResponse,
LibraryGroupedResponse,
TrackResolveRequest,
TrackResolveResponse,
)
from core.dependencies import get_library_service
from core.exceptions import ExternalServiceError
from infrastructure.msgspec_fastapi import MsgSpecRoute, MsgSpecBody
from services.library_service import LibraryService
logger = logging.getLogger(__name__)
router = APIRouter(route_class=MsgSpecRoute, prefix="/library", tags=["library"])
@router.get("/", response_model=LibraryResponse)
async def get_library(
library_service: LibraryService = Depends(get_library_service)
):
library = await library_service.get_library()
return LibraryResponse(library=library)
@router.get("/artists", response_model=PaginatedLibraryArtistsResponse)
async def get_library_artists(
limit: int = 50,
offset: int = 0,
sort_by: str = "name",
sort_order: str = "asc",
q: str | None = None,
library_service: LibraryService = Depends(get_library_service)
):
limit = max(1, min(limit, 100))
offset = max(0, offset)
allowed_sort = {"name", "album_count", "date_added"}
if sort_by not in allowed_sort:
sort_by = "name"
if sort_order not in ("asc", "desc"):
sort_order = "asc"
artists, total = await library_service.get_artists_paginated(
limit=limit, offset=offset, sort_by=sort_by, sort_order=sort_order, search=q,
)
return PaginatedLibraryArtistsResponse(artists=artists, total=total, offset=offset, limit=limit)
@router.get("/albums", response_model=PaginatedLibraryAlbumsResponse)
async def get_library_albums(
limit: int = 50,
offset: int = 0,
sort_by: str = "date_added",
sort_order: str = "desc",
q: str | None = None,
library_service: LibraryService = Depends(get_library_service)
):
limit = max(1, min(limit, 100))
offset = max(0, offset)
allowed_sort = {"date_added", "artist", "title", "year"}
if sort_by not in allowed_sort:
sort_by = "date_added"
if sort_order not in ("asc", "desc"):
sort_order = "desc"
albums, total = await library_service.get_albums_paginated(
limit=limit, offset=offset, sort_by=sort_by, sort_order=sort_order, search=q,
)
return PaginatedLibraryAlbumsResponse(albums=albums, total=total, offset=offset, limit=limit)
@router.get("/recently-added", response_model=RecentlyAddedResponse)
async def get_recently_added(
limit: int = 20,
library_service: LibraryService = Depends(get_library_service)
):
albums = await library_service.get_recently_added(limit=limit)
return RecentlyAddedResponse(albums=albums, artists=[])
@router.post("/sync", response_model=SyncLibraryResponse)
async def sync_library(
library_service: LibraryService = Depends(get_library_service)
):
try:
return await library_service.sync_library(is_manual=True)
except ExternalServiceError as e:
logger.error(f"Couldn't sync the library: {e}")
if "cooldown" in str(e).lower():
raise HTTPException(status_code=429, detail="Sync is on cooldown, please wait")
raise HTTPException(status_code=503, detail="External service unavailable")
@router.get("/stats", response_model=LibraryStatsResponse)
async def get_library_stats(
library_service: LibraryService = Depends(get_library_service)
):
return await library_service.get_stats()
@router.get("/mbids", response_model=LibraryMbidsResponse)
async def get_library_mbids(
library_service: LibraryService = Depends(get_library_service)
):
mbids, requested = await asyncio.gather(
library_service.get_library_mbids(),
library_service.get_requested_mbids(),
)
return LibraryMbidsResponse(mbids=mbids, requested_mbids=requested)
@router.get("/grouped", response_model=LibraryGroupedResponse)
async def get_library_grouped(
library_service: LibraryService = Depends(get_library_service)
):
grouped = await library_service.get_library_grouped()
return LibraryGroupedResponse(library=grouped)
@router.get("/album/{album_mbid}/removal-preview", response_model=AlbumRemovePreviewResponse)
async def get_album_removal_preview(
album_mbid: str,
library_service: LibraryService = Depends(get_library_service)
):
try:
return await library_service.get_album_removal_preview(album_mbid)
except ExternalServiceError as e:
logger.error(f"Failed to get album removal preview: {e}")
raise HTTPException(status_code=500, detail="Failed to load removal preview")
@router.delete("/album/{album_mbid}", response_model=AlbumRemoveResponse)
async def remove_album(
album_mbid: str,
delete_files: bool = False,
library_service: LibraryService = Depends(get_library_service)
):
try:
return await library_service.remove_album(album_mbid, delete_files=delete_files)
except ExternalServiceError as e:
logger.error(f"Couldn't remove album {album_mbid}: {e}")
raise HTTPException(status_code=500, detail="Couldn't remove this album")
@router.post("/resolve-tracks", response_model=TrackResolveResponse)
async def resolve_tracks(
body: TrackResolveRequest = MsgSpecBody(TrackResolveRequest),
library_service: LibraryService = Depends(get_library_service),
):
return await library_service.resolve_tracks_batch(body.items)