feat(library): remote browse status + save/materialize API (§Phase2-3)
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>
This commit is contained in:
@@ -13,14 +13,18 @@ from app.api.deps import (
|
||||
CurrentUser,
|
||||
FileStorageDep,
|
||||
MetadataServiceDep,
|
||||
RemoteLibraryServiceDep,
|
||||
StreamUser,
|
||||
TrackRepoDep,
|
||||
)
|
||||
from app.api.schemas.download import DownloadJobOut
|
||||
from app.api.schemas.pagination import PagedResponse
|
||||
from app.api.schemas.track import (
|
||||
MaterializeResponse,
|
||||
MetadataApply,
|
||||
MetadataMatch,
|
||||
MetadataMatchesOut,
|
||||
RemoteTrackSave,
|
||||
TrackOut,
|
||||
TrackUpdate,
|
||||
)
|
||||
@@ -99,6 +103,57 @@ async def list_tracks(
|
||||
return PagedResponse(items=items, total=total, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.post("/remote", status_code=201)
|
||||
async def save_remote_track(
|
||||
body: RemoteTrackSave,
|
||||
service: RemoteLibraryServiceDep,
|
||||
artist_repo: ArtistRepoDep,
|
||||
album_repo: AlbumRepoDep,
|
||||
user: CurrentUser,
|
||||
) -> TrackOut:
|
||||
"""Save a remote browse hit (§A4 discover) as a library placeholder —
|
||||
no audio is fetched yet (plan: Model C). Idempotent on ``(source,
|
||||
source_id)``: saving an already-saved hit returns the existing track."""
|
||||
track = await service.save_remote(
|
||||
source=body.source,
|
||||
source_id=body.source_id,
|
||||
title=body.title,
|
||||
artist=body.artist,
|
||||
added_by=user.id,
|
||||
)
|
||||
|
||||
artists = {a.id: a for a in await artist_repo.get_many([track.artist_id])}
|
||||
album_ids = [track.album_id] if track.album_id else []
|
||||
albums = {a.id: a for a in await album_repo.get_many(album_ids)}
|
||||
items = await _build_track_out([track], artists, albums)
|
||||
return items[0]
|
||||
|
||||
|
||||
@router.post("/{track_id}/materialize")
|
||||
async def materialize_track(
|
||||
track_id: uuid.UUID,
|
||||
service: RemoteLibraryServiceDep,
|
||||
artist_repo: ArtistRepoDep,
|
||||
album_repo: AlbumRepoDep,
|
||||
user: CurrentUser,
|
||||
) -> MaterializeResponse:
|
||||
"""Fetch a placeholder track's audio on demand (plan: Model C lazy
|
||||
materialization). Already-local tracks return ``job=None`` — nothing to
|
||||
wait for. Otherwise poll ``GET /downloads/{job.id}`` until ``done``, then
|
||||
stream as usual."""
|
||||
outcome = await service.request_materialize(track_id, requested_by=user.id)
|
||||
|
||||
artists = {a.id: a for a in await artist_repo.get_many([outcome.track.artist_id])}
|
||||
album_ids = [outcome.track.album_id] if outcome.track.album_id else []
|
||||
albums = {a.id: a for a in await album_repo.get_many(album_ids)}
|
||||
track_out = (await _build_track_out([outcome.track], artists, albums))[0]
|
||||
|
||||
return MaterializeResponse(
|
||||
track=track_out,
|
||||
job=DownloadJobOut.from_entity(outcome.job) if outcome.job is not None else None,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{track_id}")
|
||||
async def get_track(
|
||||
track_id: uuid.UUID,
|
||||
|
||||
Reference in New Issue
Block a user