feat: add recommendation service adapters

This commit is contained in:
Justin Visser 2026-08-10 12:05:59 +02:00
parent e970bdf542
commit 751391e6a2
9 changed files with 676 additions and 26 deletions

View file

@ -12,15 +12,22 @@ from app.adapters.spotify.errors import (
SpotifyUnavailableError,
)
from app.adapters.spotify.mapping import (
CreatedPlaylist,
CurrentUser,
parse_created_playlist,
parse_current_user,
parse_saved_track_page,
parse_search_tracks,
parse_top_artists,
parse_track_page,
)
from app.adapters.spotify.session import SpotifySession
from app.config import Settings
from app.domain.models import Track
from app.domain.models import CreatedPlaylist, Track
from app.observability.timing import increment_spotify_calls
from app.ports.protocols import TimeRange
SPOTIFY_SEARCH_LIMIT = 10
SPOTIFY_PAGE_LIMIT = 50
class SpotifyClient:
@ -39,6 +46,8 @@ class SpotifyClient:
async def search_tracks(self, query: str, limit: int = 10) -> list[Track]:
"""Search Spotify tracks and return only valid mapped results."""
if not 1 <= limit <= SPOTIFY_SEARCH_LIMIT:
raise ValueError("Spotify search limit must be between 1 and 10")
response = await self._request(
"GET",
"/search",
@ -46,6 +55,40 @@ class SpotifyClient:
)
return parse_search_tracks(response.json())
async def fetch_top_artists(self, time_range: TimeRange, limit: int) -> list[str]:
"""Fetch the user's top artists for a supported time range."""
response = await self._request(
"GET",
"/me/top/artists",
params={"time_range": time_range, "limit": limit},
)
return parse_top_artists(response.json())
async def fetch_top_tracks(self, time_range: TimeRange, limit: int) -> list[Track]:
"""Fetch the user's top tracks for a supported time range."""
response = await self._request(
"GET",
"/me/top/tracks",
params={"time_range": time_range, "limit": limit},
)
return parse_track_page(response.json())
async def fetch_saved_tracks(self, limit: int) -> list[Track]:
"""Fetch a bounded saved-track sample across Spotify pages."""
tracks: list[Track] = []
while len(tracks) < limit:
page_limit = min(SPOTIFY_PAGE_LIMIT, limit - len(tracks))
response = await self._request(
"GET",
"/me/tracks",
params={"limit": page_limit, "offset": len(tracks)},
)
page = parse_saved_track_page(response.json())
tracks.extend(page)
if len(page) < page_limit:
break
return tracks
async def fetch_current_user(self) -> CurrentUser:
"""Fetch the authenticated Spotify user's stable identity."""
response = await self._request("GET", "/me")
@ -110,6 +153,7 @@ class SpotifyClient:
params: dict[str, str | int] | None,
json: dict[str, object] | None,
) -> httpx2.Response:
increment_spotify_calls()
return await self.http.request(
method,
f"{self.settings.spotify_api_base_url.rstrip('/')}{path}",