discovery-by-llm/backend/app/adapters/spotify/mapping.py
Justin Visser e171dd6d80
Some checks are pending
ci / backend (push) Waiting to run
ci / frontend (push) Waiting to run
feat: add Spotify client and auth routes
2026-08-10 10:53:14 +02:00

132 lines
4.1 KiB
Python

"""Map Spotify response data into application-owned models."""
from collections.abc import Mapping
from dataclasses import dataclass
from typing import cast
from app.domain.models import Track
@dataclass(frozen=True)
class CurrentUser:
"""The stable identity fields returned for the current Spotify user."""
account_id: str
display_name: str
@dataclass(frozen=True)
class CreatedPlaylist:
"""The application-owned result of creating a Spotify playlist."""
id: str
url: str
def parse_search_tracks(payload: object) -> list[Track]:
"""Map valid Spotify search items and discard malformed entries."""
root = _as_mapping(payload)
tracks = _as_mapping(root.get("tracks")) if root is not None else None
items = tracks.get("items") if tracks is not None else None
if not isinstance(items, list):
return []
parsed_tracks: list[Track] = []
for item in items:
parsed_track = _parse_track(item)
if parsed_track is not None:
parsed_tracks.append(parsed_track)
return parsed_tracks
def parse_current_user(payload: object) -> CurrentUser:
"""Map a Spotify current-user response into stable identity fields."""
root = _as_mapping(payload)
account_id = _required_string(root, "account_id")
display_name = _required_string(root, "display_name")
if account_id is None or display_name is None:
raise ValueError("Spotify returned an invalid current-user response")
return CurrentUser(account_id=account_id, display_name=display_name)
def parse_created_playlist(payload: object) -> CreatedPlaylist:
"""Map a Spotify playlist creation response."""
root = _as_mapping(payload)
playlist_id = _required_string(root, "id")
external_urls = _as_mapping(root.get("external_urls")) if root is not None else None
url = _required_string(external_urls, "spotify")
if playlist_id is None or url is None:
raise ValueError("Spotify returned an invalid playlist response")
return CreatedPlaylist(id=playlist_id, url=url)
def _parse_track(payload: object) -> Track | None:
item = _as_mapping(payload)
track_id = _required_string(item, "id")
uri = _required_string(item, "uri")
title = _required_string(item, "name")
album = _as_mapping(item.get("album")) if item is not None else None
album_name = _required_string(album, "name")
artists = _parse_artists(item.get("artists")) if item is not None else None
if (
item is None
or track_id is None
or uri is None
or title is None
or album_name is None
or artists is None
):
return None
return Track(
id=track_id,
uri=uri,
title=title,
artists=artists,
album_name=album_name,
album_art_url=_parse_album_art_url(album),
external_url=_parse_external_url(item),
)
def _parse_artists(payload: object) -> tuple[str, ...] | None:
if not isinstance(payload, list) or not payload:
return None
names: list[str] = []
for artist_payload in payload:
name = _required_string(_as_mapping(artist_payload), "name")
if name is None:
return None
names.append(name)
return tuple(names)
def _parse_album_art_url(album: Mapping[str, object] | None) -> str | None:
images = album.get("images") if album is not None else None
if not isinstance(images, list) or not images:
return None
return _optional_string(_as_mapping(images[0]), "url")
def _parse_external_url(item: Mapping[str, object]) -> str | None:
return _optional_string(_as_mapping(item.get("external_urls")), "spotify")
def _as_mapping(payload: object) -> Mapping[str, object] | None:
if not isinstance(payload, Mapping):
return None
return cast(Mapping[str, object], payload)
def _required_string(payload: Mapping[str, object] | None, key: str) -> str | None:
value = _optional_string(payload, key)
return value if value else None
def _optional_string(payload: Mapping[str, object] | None, key: str) -> str | None:
if payload is None:
return None
value = payload.get(key)
return value if isinstance(value, str) else None