128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
import logging
|
|
import time
|
|
from fastapi import APIRouter, Query, Path, BackgroundTasks, Depends, Request
|
|
from core.exceptions import ClientDisconnectedError
|
|
from api.v1.schemas.search import (
|
|
SearchResponse,
|
|
SearchBucketResponse,
|
|
EnrichmentResponse,
|
|
EnrichmentBatchRequest,
|
|
SuggestResponse,
|
|
)
|
|
from core.dependencies import get_search_service, get_coverart_repository, get_search_enrichment_service
|
|
from infrastructure.degradation import try_get_degradation_context
|
|
from infrastructure.msgspec_fastapi import MsgSpecBody, MsgSpecRoute
|
|
|
|
import msgspec.structs
|
|
from services.search_service import SearchService
|
|
from services.search_enrichment_service import SearchEnrichmentService
|
|
from repositories.coverart_repository import CoverArtRepository
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(route_class=MsgSpecRoute, prefix="/search", tags=["search"])
|
|
|
|
|
|
@router.get("", response_model=SearchResponse)
|
|
async def search(
|
|
request: Request,
|
|
background_tasks: BackgroundTasks,
|
|
q: str = Query(..., min_length=1, description="Search term"),
|
|
limit_per_bucket: int | None = Query(
|
|
None, ge=1, le=100,
|
|
description="Max items per bucket (deprecated, use limit_artists/limit_albums)"
|
|
),
|
|
limit_artists: int = Query(10, ge=0, le=100, description="Max artists to return"),
|
|
limit_albums: int = Query(10, ge=0, le=100, description="Max albums to return"),
|
|
buckets: str | None = Query(
|
|
None, description="Comma-separated subset: artists,albums"
|
|
),
|
|
search_service: SearchService = Depends(get_search_service),
|
|
coverart_repo: CoverArtRepository = Depends(get_coverart_repository)
|
|
):
|
|
if await request.is_disconnected():
|
|
raise ClientDisconnectedError("Client disconnected")
|
|
|
|
buckets_list = [b.strip().lower() for b in buckets.split(",")] if buckets else None
|
|
|
|
final_limit_artists = limit_per_bucket if limit_per_bucket else limit_artists
|
|
final_limit_albums = limit_per_bucket if limit_per_bucket else limit_albums
|
|
|
|
result = await search_service.search(
|
|
query=q,
|
|
limit_artists=final_limit_artists,
|
|
limit_albums=final_limit_albums,
|
|
buckets=buckets_list
|
|
)
|
|
|
|
ctx = try_get_degradation_context()
|
|
if ctx is not None and ctx.has_degradation():
|
|
result = msgspec.structs.replace(result, service_status=ctx.degraded_summary())
|
|
|
|
album_ids = search_service.schedule_cover_prefetch(result.albums)
|
|
if album_ids:
|
|
background_tasks.add_task(
|
|
coverart_repo.batch_prefetch_covers,
|
|
album_ids,
|
|
"250"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/suggest", response_model=SuggestResponse)
|
|
async def suggest(
|
|
q: str = Query(..., min_length=2, description="Search query"),
|
|
limit: int = Query(5, ge=1, le=10, description="Max results"),
|
|
search_service: SearchService = Depends(get_search_service),
|
|
) -> SuggestResponse:
|
|
stripped = q.strip()
|
|
if len(stripped) < 2:
|
|
return SuggestResponse()
|
|
start = time.monotonic()
|
|
result = await search_service.suggest(query=stripped, limit=limit)
|
|
elapsed_ms = (time.monotonic() - start) * 1000
|
|
logger.debug("Suggest query_len=%d results=%d time_ms=%.1f", len(stripped), len(result.results), elapsed_ms)
|
|
return result
|
|
|
|
|
|
@router.get("/{bucket}", response_model=SearchBucketResponse)
|
|
async def search_bucket(
|
|
bucket: str = Path(..., pattern="^(artists|albums)$"),
|
|
q: str = Query(..., min_length=1, description="Search term"),
|
|
limit: int = Query(50, ge=1, le=100, description="Page size"),
|
|
offset: int = Query(0, ge=0, description="Pagination offset"),
|
|
search_service: SearchService = Depends(get_search_service)
|
|
):
|
|
results, top_result = await search_service.search_bucket(
|
|
bucket=bucket,
|
|
query=q,
|
|
limit=limit,
|
|
offset=offset
|
|
)
|
|
return SearchBucketResponse(bucket=bucket, limit=limit, offset=offset, results=results, top_result=top_result)
|
|
|
|
|
|
@router.get("/enrich/batch", response_model=EnrichmentResponse)
|
|
async def enrich_search_results(
|
|
artist_mbids: str = Query("", description="Comma-separated artist MBIDs"),
|
|
album_mbids: str = Query("", description="Comma-separated album MBIDs"),
|
|
enrichment_service: SearchEnrichmentService = Depends(get_search_enrichment_service)
|
|
):
|
|
artist_list = [m.strip() for m in artist_mbids.split(",") if m.strip()]
|
|
album_list = [m.strip() for m in album_mbids.split(",") if m.strip()]
|
|
|
|
return await enrichment_service.enrich(
|
|
artist_mbids=artist_list,
|
|
album_mbids=album_list,
|
|
)
|
|
|
|
|
|
@router.post("/enrich/batch", response_model=EnrichmentResponse)
|
|
async def enrich_search_results_post(
|
|
body: EnrichmentBatchRequest = MsgSpecBody(EnrichmentBatchRequest),
|
|
enrichment_service: SearchEnrichmentService = Depends(get_search_enrichment_service),
|
|
):
|
|
return await enrichment_service.enrich_batch(body)
|
|
|