feat(reco): radio + similar with metadata fallback (§6.5)
POST /radio + /radio/next (stateless infinite feed: seed track / from-likes,
exploration mix, client-passed exclude_ids) and GET /tracks|artists/{id}/similar,
replacing the stubs. Recommender port abstracts the (future) ML service —
NullRecommender is wired now so RecommendationService always uses its metadata
heuristics (genre/artist similarity, random exploration filler), never a hard ML
dependency. Adds TrackRepository.list_similar/sample_playable + Artist.list_similar,
reason codes for the client, RemoteRecommender skeleton (TODO: ML contract).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-2
@@ -1,7 +1,7 @@
|
||||
"""Track endpoints."""
|
||||
|
||||
import uuid
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
@@ -14,6 +14,7 @@ from app.api.deps import (
|
||||
FileStorageDep,
|
||||
LyricsServiceDep,
|
||||
MetadataServiceDep,
|
||||
RecommendationServiceDep,
|
||||
RemoteLibraryServiceDep,
|
||||
StreamUser,
|
||||
TrackRepoDep,
|
||||
@@ -21,6 +22,7 @@ from app.api.deps import (
|
||||
from app.api.schemas.download import DownloadJobOut
|
||||
from app.api.schemas.lyrics import LyricsOut
|
||||
from app.api.schemas.pagination import PagedResponse
|
||||
from app.api.schemas.radio import SimilarTracksOut
|
||||
from app.api.schemas.track import (
|
||||
MaterializeResponse,
|
||||
MetadataApply,
|
||||
@@ -221,7 +223,23 @@ async def delete_track(
|
||||
|
||||
|
||||
@router.get("/{track_id}/similar")
|
||||
async def get_similar_tracks(track_id: uuid.UUID, _: CurrentUser) -> Any: ...
|
||||
async def get_similar_tracks(
|
||||
track_id: uuid.UUID,
|
||||
service: RecommendationServiceDep,
|
||||
artist_repo: ArtistRepoDep,
|
||||
album_repo: AlbumRepoDep,
|
||||
_: CurrentUser,
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 20,
|
||||
) -> SimilarTracksOut:
|
||||
"""Tracks similar to this one (§6.5). Uses ML when configured, else a
|
||||
genre/artist metadata heuristic."""
|
||||
source, tracks = await service.similar_tracks(track_id, limit=limit)
|
||||
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 = {a.id: a for a in await artist_repo.get_many(artist_ids)}
|
||||
albums = {a.id: a for a in await album_repo.get_many(album_ids)}
|
||||
outs = await _build_track_out(tracks, artists, albums)
|
||||
return SimilarTracksOut(source=source, tracks=outs)
|
||||
|
||||
|
||||
@router.post("/{track_id}/optimize", status_code=202)
|
||||
|
||||
Reference in New Issue
Block a user