feat(sources): YouTube Music search + download pipeline (§1C/§1E)
Pluggable fetch source: ytmusicapi search + yt-dlp download (cookies-file guard), DownloadJob entity/repo + DownloadService, download_task worker with exponential-backoff retries, and wired /search, /sources/{source}/search, and /downloads endpoints. Adds youtube_enabled/cookies config, yt-dlp+ytmusicapi deps, and the download_jobs.track_id migration. Snapshot also bundles in-progress storage/tracks/acoustid edits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-4
@@ -1,12 +1,11 @@
|
||||
"""Search endpoints: global and library-scoped."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.api.deps import AlbumRepoDep, ArtistRepoDep, CurrentUser, TrackRepoDep
|
||||
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
|
||||
@@ -16,7 +15,26 @@ router = APIRouter(prefix="/search", tags=["search"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def search(_: CurrentUser) -> Any: ...
|
||||
async def search(
|
||||
_: CurrentUser,
|
||||
registry: SourceRegistryDep,
|
||||
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."""
|
||||
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)
|
||||
results.extend(ExternalSearchResultOut.from_entity(h) for h in hits)
|
||||
return ExternalSearchResponse(results=results, searched_sources=searched)
|
||||
|
||||
|
||||
@router.get("/library")
|
||||
|
||||
Reference in New Issue
Block a user