591a938e71
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>
44 lines
1.2 KiB
Python
44 lines
1.2 KiB
Python
"""Radio + similarity response schemas (§6.5)."""
|
|
|
|
import uuid
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.api.schemas.artist import ArtistOut
|
|
from app.api.schemas.track import TrackOut
|
|
|
|
|
|
class RadioRequest(BaseModel):
|
|
"""Start or continue a radio. ``seed_track_id`` seeds from a track;
|
|
``from_likes`` seeds from the caller's likes. ``exclude_ids`` are already-
|
|
queued tracks to skip (the client drives the infinite feed). ``exploration``
|
|
biases familiar↔new."""
|
|
|
|
seed_track_id: uuid.UUID | None = None
|
|
from_likes: bool = False
|
|
exploration: float = Field(default=0.25, ge=0.0, le=1.0)
|
|
count: int = Field(default=20, ge=1, le=50)
|
|
exclude_ids: list[uuid.UUID] = Field(default_factory=list)
|
|
|
|
|
|
class RadioTrackOut(BaseModel):
|
|
track: TrackOut
|
|
# Short code the client localizes: ml | similar | from_likes | discover.
|
|
reason: str
|
|
|
|
|
|
class RadioResponse(BaseModel):
|
|
# Where the picks came from: "ml" or "metadata" (fallback).
|
|
source: str
|
|
tracks: list[RadioTrackOut]
|
|
|
|
|
|
class SimilarTracksOut(BaseModel):
|
|
source: str
|
|
tracks: list[TrackOut]
|
|
|
|
|
|
class SimilarArtistsOut(BaseModel):
|
|
source: str
|
|
artists: list[ArtistOut]
|