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,6 +20,7 @@ from app.application.auth_service import AuthService
|
||||
from app.application.download_service import DownloadService
|
||||
from app.application.lyrics_service import LyricsService
|
||||
from app.application.metadata_service import MetadataEnrichmentService
|
||||
from app.application.recommendation_service import RecommendationService
|
||||
from app.application.remote_library_service import RemoteLibraryService
|
||||
from app.application.streaming_service import StreamingService
|
||||
from app.application.subsonic_auth_service import SubsonicAuthService
|
||||
@@ -51,6 +52,7 @@ from app.infrastructure.metadata.acoustid import AcoustIdHttpClient
|
||||
from app.infrastructure.metadata.fingerprint import FpcalcFingerprinter
|
||||
from app.infrastructure.metadata.lrclib import LrclibHttpClient
|
||||
from app.infrastructure.metadata.tags import MutagenTagReader
|
||||
from app.infrastructure.ml.recommender import NullRecommender
|
||||
from app.infrastructure.sources.registry import SourceRegistry, build_source_registry
|
||||
from app.infrastructure.storage.provider import get_file_storage
|
||||
from app.workers.queue import enqueue_download, enqueue_enrich, enqueue_materialize
|
||||
@@ -188,6 +190,18 @@ def get_transcode_service(session: SessionDep) -> TranscodeService:
|
||||
)
|
||||
|
||||
|
||||
def get_recommendation_service(session: SessionDep) -> RecommendationService:
|
||||
"""Radio + similarity (§6.5). ML is optional and no service/contract exists
|
||||
yet, so we wire ``NullRecommender`` — the service then uses its metadata
|
||||
fallback. Swap in ``RemoteRecommender(ml_service_url)`` once ML lands."""
|
||||
return RecommendationService(
|
||||
recommender=NullRecommender(),
|
||||
tracks=SqlAlchemyTrackRepository(session),
|
||||
artists=SqlAlchemyArtistRepository(session),
|
||||
likes=SqlAlchemyLikeRepository(session),
|
||||
)
|
||||
|
||||
|
||||
def get_lyrics_service(session: SessionDep) -> LyricsService:
|
||||
"""Wires the LRCLIB lyrics provider + cache repo (plan §6.7). LRCLIB is
|
||||
keyless, so this is always available; failures degrade to ``not_found``."""
|
||||
@@ -226,6 +240,9 @@ StreamingServiceDep = Annotated[StreamingService, Depends(get_streaming_service)
|
||||
MetadataServiceDep = Annotated[MetadataEnrichmentService, Depends(get_metadata_service)]
|
||||
LyricsServiceDep = Annotated[LyricsService, Depends(get_lyrics_service)]
|
||||
TranscodeServiceDep = Annotated[TranscodeService, Depends(get_transcode_service)]
|
||||
RecommendationServiceDep = Annotated[
|
||||
RecommendationService, Depends(get_recommendation_service)
|
||||
]
|
||||
DownloadServiceDep = Annotated[DownloadService, Depends(get_download_service)]
|
||||
RemoteLibraryServiceDep = Annotated[RemoteLibraryService, Depends(get_remote_library_service)]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user