"""Artist endpoints.""" import uuid from fastapi import APIRouter, Query from app.api.deps import ( AlbumRepoDep, ArtistRepoDep, CurrentUser, RecommendationServiceDep, TrackRepoDep, ) from app.api.schemas.album import AlbumOut from app.api.schemas.artist import ArtistOut from app.api.schemas.pagination import PagedResponse from app.api.schemas.radio import SimilarArtistsOut from app.api.schemas.track import TrackOut from app.api.v1.albums import _build_album_out from app.api.v1.tracks import _build_track_out from app.domain.errors import NotFoundError router = APIRouter(prefix="/artists", tags=["artists"]) @router.get("") async def list_artists( artist_repo: ArtistRepoDep, _: CurrentUser, q: str | None = None, limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), ) -> PagedResponse[ArtistOut]: artists = await artist_repo.list(q=q, limit=limit, offset=offset) total = await artist_repo.count(q=q) items = [] for a in artists: album_cnt = await artist_repo.album_count(a.id) track_cnt = await artist_repo.track_count(a.id) items.append( ArtistOut( id=a.id, name=a.name, album_count=album_cnt, track_count=track_cnt, created_at=a.created_at, ) ) return PagedResponse(items=items, total=total, limit=limit, offset=offset) @router.get("/{artist_id}") async def get_artist( artist_id: uuid.UUID, artist_repo: ArtistRepoDep, _: CurrentUser, ) -> ArtistOut: artist = await artist_repo.get_by_id(artist_id) if artist is None: raise NotFoundError(f"Artist {artist_id} not found.") album_cnt = await artist_repo.album_count(artist_id) track_cnt = await artist_repo.track_count(artist_id) return ArtistOut( id=artist.id, name=artist.name, album_count=album_cnt, track_count=track_cnt, created_at=artist.created_at, ) @router.get("/{artist_id}/albums") async def get_artist_albums( artist_id: uuid.UUID, artist_repo: ArtistRepoDep, album_repo: AlbumRepoDep, _: CurrentUser, limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), ) -> PagedResponse[AlbumOut]: artist = await artist_repo.get_by_id(artist_id) if artist is None: raise NotFoundError(f"Artist {artist_id} not found.") albums = await album_repo.list(artist_id=artist_id, q=None, limit=limit, offset=offset) total = await album_repo.count(artist_id=artist_id, q=None) artists_map = {artist.id: artist} track_counts = await album_repo.track_count_many([a.id for a in albums]) items = await _build_album_out(albums, artists_map, track_counts) return PagedResponse(items=items, total=total, limit=limit, offset=offset) @router.get("/{artist_id}/tracks") async def get_artist_tracks( artist_id: uuid.UUID, artist_repo: ArtistRepoDep, track_repo: TrackRepoDep, album_repo: AlbumRepoDep, _: CurrentUser, limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), ) -> PagedResponse[TrackOut]: artist = await artist_repo.get_by_id(artist_id) if artist is None: raise NotFoundError(f"Artist {artist_id} not found.") tracks = await track_repo.list( artist_id=artist_id, album_id=None, q=None, sort_by="title", order="asc", limit=limit, offset=offset, ) total = await track_repo.count(artist_id=artist_id, album_id=None, q=None) album_ids = list({t.album_id for t in tracks if t.album_id is not None}) artists_map = {artist.id: artist} albums_map = {a.id: a for a in await album_repo.get_many(album_ids)} items = await _build_track_out(tracks, artists_map, albums_map) return PagedResponse(items=items, total=total, limit=limit, offset=offset) @router.get("/{artist_id}/similar") async def get_similar_artists( artist_id: uuid.UUID, service: RecommendationServiceDep, artist_repo: ArtistRepoDep, _: CurrentUser, limit: int = Query(20, ge=1, le=100), ) -> SimilarArtistsOut: """Artists similar to this one (ยง6.5). ML when configured, else a shared- genre metadata heuristic.""" source, artists = await service.similar_artists(artist_id, limit=limit) items = [ ArtistOut( id=a.id, name=a.name, album_count=await artist_repo.album_count(a.id), track_count=await artist_repo.track_count(a.id), created_at=a.created_at, ) for a in artists ] return SimilarArtistsOut(source=source, artists=items)