"""Subsonic search endpoints — search3 over the library repositories. Mirrors the native ``/api/v1/search/library`` fan-out (tracks/albums/artists), reshaped into the Subsonic ``searchResult3`` element. An empty query returns results so clients can use search3 to browse. """ from typing import Annotated, Any from fastapi import APIRouter, Query, Response from app.api.deps import AlbumRepoDep, ArtistRepoDep, SubsonicFormat, SubsonicUser, TrackRepoDep from app.api.rest.envelope import subsonic_response from app.api.rest.serializers import album_dict, artist_dict, song_dict router = APIRouter() @router.api_route("/search3", methods=["GET", "POST"]) @router.api_route("/search3.view", methods=["GET", "POST"]) async def search3( _user: SubsonicUser, fmt: SubsonicFormat, track_repo: TrackRepoDep, artist_repo: ArtistRepoDep, album_repo: AlbumRepoDep, query: Annotated[str, Query()] = "", artistCount: Annotated[int, Query(ge=0, le=500)] = 20, artistOffset: Annotated[int, Query(ge=0)] = 0, albumCount: Annotated[int, Query(ge=0, le=500)] = 20, albumOffset: Annotated[int, Query(ge=0)] = 0, songCount: Annotated[int, Query(ge=0, le=500)] = 20, songOffset: Annotated[int, Query(ge=0)] = 0, ) -> Response: # Subsonic sends "" (and some clients '""') to mean "everything". q: str | None = query.strip().strip('"') or None artists_out: list[dict[str, Any]] = [] if artistCount: artists = await artist_repo.list(q=q, limit=artistCount, offset=artistOffset) for a in artists: album_count = await artist_repo.album_count(a.id) artists_out.append(artist_dict(a, album_count=album_count)) albums_out: list[dict[str, Any]] = [] if albumCount: albums = await album_repo.list(artist_id=None, q=q, limit=albumCount, offset=albumOffset) album_artist_ids = list({a.artist_id for a in albums}) album_artist_map = {a.id: a for a in await artist_repo.get_many(album_artist_ids)} counts = await album_repo.track_count_many([a.id for a in albums]) albums_out = [ album_dict(a, album_artist_map.get(a.artist_id), song_count=counts.get(a.id, 0)) for a in albums ] songs_out: list[dict[str, Any]] = [] if songCount: tracks = await track_repo.list( artist_id=None, album_id=None, q=q, sort_by="title", order="asc", limit=songCount, offset=songOffset, ) song_artist_map = { a.id: a for a in await artist_repo.get_many(list({t.artist_id for t in tracks})) } song_album_map = { a.id: a for a in await album_repo.get_many( list({t.album_id for t in tracks if t.album_id is not None}) ) } songs_out = [ song_dict( t, song_artist_map.get(t.artist_id), song_album_map.get(t.album_id) if t.album_id is not None else None, ) for t in tracks ] payload = {"searchResult3": {"artist": artists_out, "album": albums_out, "song": songs_out}} return subsonic_response(payload, fmt=fmt)