e45e578f54
Search results now report whether a hit is already saved (in_library,
track_id, availability). New RemoteLibraryService backs POST
/tracks/remote (idempotent placeholder save) and POST
/tracks/{id}/materialize (on-demand fetch via a new materialize_track
arq task, reusing in-flight jobs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
"""Search endpoints: global and library-scoped."""
|
|
|
|
from fastapi import APIRouter, Query
|
|
|
|
from app.api.deps import AlbumRepoDep, ArtistRepoDep, CurrentUser, SourceRegistryDep, TrackRepoDep
|
|
from app.api.schemas.album import AlbumOut
|
|
from app.api.schemas.artist import ArtistOut
|
|
from app.api.schemas.external_search import ExternalSearchResponse, ExternalSearchResultOut
|
|
from app.api.schemas.search import LibrarySearchResponse
|
|
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
|
|
|
|
router = APIRouter(prefix="/search", tags=["search"])
|
|
|
|
|
|
@router.get("")
|
|
async def search(
|
|
_: CurrentUser,
|
|
registry: SourceRegistryDep,
|
|
track_repo: TrackRepoDep,
|
|
q: str = Query(min_length=1),
|
|
limit: int = Query(20, ge=1, le=50),
|
|
) -> ExternalSearchResponse:
|
|
"""Search every available fetch source and merge the hits (§A4 discover).
|
|
|
|
A source that is down contributes nothing rather than failing the whole
|
|
request (graceful degradation); only available sources are reported as
|
|
searched. Each hit is checked against the library by ``(source,
|
|
source_id)`` so the UI can show "Saved"/"Play" instead of "Save to
|
|
library" without a separate round-trip (remote browse, plan: Model C)."""
|
|
results: list[ExternalSearchResultOut] = []
|
|
searched: list[str] = []
|
|
for backend in registry.searchables():
|
|
if not backend.is_available():
|
|
continue
|
|
searched.append(backend.name)
|
|
hits = await backend.search(q, limit=limit)
|
|
for h in hits:
|
|
existing = await track_repo.get_by_source(h.source, h.source_id)
|
|
results.append(ExternalSearchResultOut.from_entity(h, existing=existing))
|
|
return ExternalSearchResponse(results=results, searched_sources=searched)
|
|
|
|
|
|
@router.get("/library")
|
|
async def search_library(
|
|
track_repo: TrackRepoDep,
|
|
artist_repo: ArtistRepoDep,
|
|
album_repo: AlbumRepoDep,
|
|
_: CurrentUser,
|
|
q: str = Query(min_length=1),
|
|
types: str = Query(default="tracks,albums,artists"),
|
|
limit: int = Query(20, ge=1, le=100),
|
|
) -> LibrarySearchResponse:
|
|
requested = {t.strip() for t in types.split(",")}
|
|
|
|
tracks_out: list[TrackOut] = []
|
|
albums_out: list[AlbumOut] = []
|
|
artists_out: list[ArtistOut] = []
|
|
|
|
if "tracks" in requested:
|
|
tracks = await track_repo.list(
|
|
artist_id=None,
|
|
album_id=None,
|
|
q=q,
|
|
sort_by="title",
|
|
order="asc",
|
|
limit=limit,
|
|
offset=0,
|
|
)
|
|
if tracks:
|
|
artist_ids = list({t.artist_id for t in tracks})
|
|
album_ids = list({t.album_id for t in tracks if t.album_id is not None})
|
|
artists_map = {a.id: a for a in await artist_repo.get_many(artist_ids)}
|
|
albums_map = {a.id: a for a in await album_repo.get_many(album_ids)}
|
|
tracks_out = await _build_track_out(tracks, artists_map, albums_map)
|
|
|
|
if "albums" in requested:
|
|
albums = await album_repo.list(artist_id=None, q=q, limit=limit, offset=0)
|
|
if albums:
|
|
artist_ids = list({a.artist_id for a in albums})
|
|
artists_map = {a.id: a for a in await artist_repo.get_many(artist_ids)}
|
|
track_counts = await album_repo.track_count_many([a.id for a in albums])
|
|
albums_out = await _build_album_out(albums, artists_map, track_counts)
|
|
|
|
if "artists" in requested:
|
|
raw_artists = await artist_repo.list(q=q, limit=limit, offset=0)
|
|
for a in raw_artists:
|
|
album_cnt = await artist_repo.album_count(a.id)
|
|
track_cnt = await artist_repo.track_count(a.id)
|
|
artists_out.append(
|
|
ArtistOut(
|
|
id=a.id,
|
|
name=a.name,
|
|
album_count=album_cnt,
|
|
track_count=track_cnt,
|
|
created_at=a.created_at,
|
|
)
|
|
)
|
|
|
|
return LibrarySearchResponse(tracks=tracks_out, albums=albums_out, artists=artists_out)
|