import time from collections.abc import Callable from app.adapters.spotify import ( SpotifyAPIError, SpotifyClient, SpotifyTokenError, SpotifyTokenRevokedError, ) from app.dsp.backends.base import DSPBackend from app.dsp.enums import DSPId from app.dsp.exceptions import ( StreamingAPIError, TokenRefreshError, TokenRevokedError, ) from app.dsp.types import ( ArtistItem, FollowedArtistsResult, PlaylistItem, PlaylistsResult, ProfileResult, RecentlyPlayedItem, RecentlyPlayedResult, SavedAlbumItem, SavedAlbumsResult, SavedTrackItem, SavedTracksResult, TokenResult, TopArtistsResult, TopTracksResult, TrackItem, ) _TRANSIENT_STATUSES = frozenset({502, 503, 504}) _RETRY_MAX = 3 _RETRY_BACKOFF_S = 0.5 def _to_streaming_error(exc: SpotifyAPIError) -> StreamingAPIError: return StreamingAPIError( str(exc), status_code=exc.status_code, retry_after=exc.retry_after ) def _with_retry[T](fn: Callable[[], T]) -> T: for attempt in range(_RETRY_MAX): try: return fn() except SpotifyAPIError as exc: if exc.status_code not in _TRANSIENT_STATUSES or attempt == _RETRY_MAX - 1: raise time.sleep(_RETRY_BACKOFF_S * (2**attempt)) raise RuntimeError("unreachable") class SpotifyDSPBackend(DSPBackend): dsp_id = DSPId.spotify def __init__(self, client: SpotifyClient) -> None: self._client = client def close(self) -> None: self._client.close() def refresh_token(self, refresh_token: str) -> TokenResult: try: response = self._client.refresh_token(refresh_token) except SpotifyTokenRevokedError as exc: raise TokenRevokedError(str(exc)) from exc except SpotifyTokenError as exc: raise TokenRefreshError(str(exc)) from exc return TokenResult( access_token=response.access_token, refresh_token=response.refresh_token, ) def get_profile(self, access_token: str) -> ProfileResult: try: response = _with_retry( lambda: self._client.get_current_user_profile(access_token) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return ProfileResult( email=response.email, display_name=response.display_name, country=response.country, product=response.product, ) def get_top_artists(self, access_token: str) -> TopArtistsResult: try: response = _with_retry( lambda: self._client.get_current_user_top_artists(access_token) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return TopArtistsResult( items=[ ArtistItem( id=item.id, data=item.model_dump(mode="json", exclude={"id"}), ) for item in response ] ) def get_top_tracks(self, access_token: str) -> TopTracksResult: try: response = _with_retry( lambda: self._client.get_current_user_top_tracks(access_token) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return TopTracksResult( items=[ TrackItem( id=item.id, data=item.model_dump(mode="json", exclude={"id"}), ) for item in response ] ) def get_recently_played( self, access_token: str, *, after: int | None = None ) -> RecentlyPlayedResult: try: response = _with_retry( lambda: self._client.get_current_user_recently_played( access_token, after=after ) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return RecentlyPlayedResult( items=[ RecentlyPlayedItem( track_id=item.track.id, played_at=item.played_at, data=item.model_dump( mode="json", exclude={"played_at"}, ), ) for item in response ] ) def get_playlists(self, access_token: str) -> PlaylistsResult: try: response = _with_retry( lambda: self._client.get_current_user_playlists(access_token) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return PlaylistsResult( items=[ PlaylistItem( id=item.id, data=item.model_dump(mode="json", exclude={"id"}), ) for item in response ] ) def get_saved_albums(self, access_token: str) -> SavedAlbumsResult: try: response = _with_retry( lambda: self._client.get_current_user_saved_albums(access_token) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return SavedAlbumsResult( items=[ SavedAlbumItem( added_at=item.added_at, album_id=item.album.id, data=item.model_dump(mode="json", exclude={"added_at"}), ) for item in response ] ) def get_saved_tracks(self, access_token: str) -> SavedTracksResult: try: response = _with_retry( lambda: self._client.get_current_user_saved_tracks(access_token) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return SavedTracksResult( items=[ SavedTrackItem( added_at=item.added_at, track_id=item.track.id, data=item.model_dump(mode="json", exclude={"added_at"}), ) for item in response ] ) def get_followed_artists(self, access_token: str) -> FollowedArtistsResult: try: response = _with_retry( lambda: self._client.get_current_user_followed_artists(access_token) ) except SpotifyAPIError as exc: raise _to_streaming_error(exc) from exc return FollowedArtistsResult( items=[ ArtistItem( id=item.id, data=item.model_dump(mode="json", exclude={"id"}), ) for item in response ] )