158 lines
5.7 KiB
Python
158 lines
5.7 KiB
Python
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from fastapi.responses import Response
|
|
|
|
from api.v1.schemas.navidrome import (
|
|
NavidromeAlbumDetail,
|
|
NavidromeAlbumMatch,
|
|
NavidromeAlbumPage,
|
|
NavidromeAlbumSummary,
|
|
NavidromeArtistSummary,
|
|
NavidromeLibraryStats,
|
|
NavidromeSearchResponse,
|
|
)
|
|
from core.dependencies import get_navidrome_library_service, get_navidrome_repository
|
|
from core.exceptions import ExternalServiceError
|
|
from infrastructure.msgspec_fastapi import MsgSpecRoute
|
|
from repositories.navidrome_repository import NavidromeRepository
|
|
from services.navidrome_library_service import NavidromeLibraryService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(route_class=MsgSpecRoute, prefix="/navidrome", tags=["navidrome-library"])
|
|
|
|
|
|
_SORT_MAP: dict[str, str] = {
|
|
"name": "alphabeticalByName",
|
|
"date_added": "newest",
|
|
"year": "alphabeticalByName",
|
|
}
|
|
|
|
|
|
@router.get("/albums", response_model=NavidromeAlbumPage)
|
|
async def get_navidrome_albums(
|
|
limit: int = Query(default=48, ge=1, le=500, alias="limit"),
|
|
offset: int = Query(default=0, ge=0),
|
|
sort_by: str = Query(default="name"),
|
|
genre: str = Query(default=""),
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> NavidromeAlbumPage:
|
|
try:
|
|
if genre:
|
|
subsonic_type = "byGenre"
|
|
else:
|
|
subsonic_type = _SORT_MAP.get(sort_by, "alphabeticalByName")
|
|
items = await service.get_albums(type=subsonic_type, size=limit, offset=offset, genre=genre if genre else None)
|
|
stats = await service.get_stats()
|
|
total = stats.total_albums if len(items) >= limit else offset + len(items)
|
|
return NavidromeAlbumPage(items=items, total=total)
|
|
except ExternalServiceError as e:
|
|
logger.error("Navidrome service error getting albums: %s", e)
|
|
raise HTTPException(status_code=502, detail="Failed to communicate with Navidrome")
|
|
|
|
|
|
@router.get("/albums/{album_id}", response_model=NavidromeAlbumDetail)
|
|
async def get_navidrome_album_detail(
|
|
album_id: str,
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> NavidromeAlbumDetail:
|
|
result = await service.get_album_detail(album_id)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Album not found")
|
|
return result
|
|
|
|
|
|
@router.get("/artists", response_model=list[NavidromeArtistSummary])
|
|
async def get_navidrome_artists(
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> list[NavidromeArtistSummary]:
|
|
return await service.get_artists()
|
|
|
|
|
|
@router.get("/artists/{artist_id}")
|
|
async def get_navidrome_artist_detail(
|
|
artist_id: str,
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> dict:
|
|
result = await service.get_artist_detail(artist_id)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Artist not found")
|
|
return result
|
|
|
|
|
|
@router.get("/search", response_model=NavidromeSearchResponse)
|
|
async def search_navidrome(
|
|
q: str = Query(..., min_length=1),
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> NavidromeSearchResponse:
|
|
return await service.search(q)
|
|
|
|
|
|
@router.get("/recent", response_model=list[NavidromeAlbumSummary])
|
|
async def get_navidrome_recent(
|
|
limit: int = Query(default=20, ge=1, le=50),
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> list[NavidromeAlbumSummary]:
|
|
return await service.get_recent(limit=limit)
|
|
|
|
|
|
@router.get("/favorites", response_model=list[NavidromeAlbumSummary])
|
|
async def get_navidrome_favorites(
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> list[NavidromeAlbumSummary]:
|
|
result = await service.get_favorites()
|
|
return result.albums
|
|
|
|
|
|
@router.get("/genres", response_model=list[str])
|
|
async def get_navidrome_genres(
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> list[str]:
|
|
try:
|
|
return await service.get_genres()
|
|
except ExternalServiceError as e:
|
|
logger.error("Navidrome service error getting genres: %s", e)
|
|
raise HTTPException(status_code=502, detail="Failed to communicate with Navidrome")
|
|
|
|
|
|
@router.get("/stats", response_model=NavidromeLibraryStats)
|
|
async def get_navidrome_stats(
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> NavidromeLibraryStats:
|
|
return await service.get_stats()
|
|
|
|
|
|
@router.get("/cover/{cover_art_id}")
|
|
async def get_navidrome_cover(
|
|
cover_art_id: str,
|
|
size: int = Query(default=500, ge=32, le=1200),
|
|
repo: NavidromeRepository = Depends(get_navidrome_repository),
|
|
) -> Response:
|
|
try:
|
|
image_bytes, content_type = await repo.get_cover_art(cover_art_id, size)
|
|
return Response(
|
|
content=image_bytes,
|
|
media_type=content_type,
|
|
headers={"Cache-Control": "public, max-age=31536000, immutable"},
|
|
)
|
|
except ExternalServiceError as e:
|
|
logger.warning("Navidrome cover art failed for %s: %s", cover_art_id, e)
|
|
raise HTTPException(status_code=502, detail="Failed to fetch cover art")
|
|
|
|
|
|
@router.get("/album-match/{album_id}", response_model=NavidromeAlbumMatch)
|
|
async def match_navidrome_album(
|
|
album_id: str,
|
|
name: str = Query(default=""),
|
|
artist: str = Query(default=""),
|
|
service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
|
) -> NavidromeAlbumMatch:
|
|
try:
|
|
return await service.get_album_match(
|
|
album_id=album_id, album_name=name, artist_name=artist,
|
|
)
|
|
except ExternalServiceError as e:
|
|
logger.error("Failed to match Navidrome album %s: %s", album_id, e)
|
|
raise HTTPException(status_code=502, detail="Failed to match Navidrome album")
|