from collections.abc import Callable from fansifter_common.utils.functional import lazy_proxy from app.adapters.db import db from app.adapters.redis import redis_client from app.adapters.spotify import SpotifyClient from app.config import settings from app.dsp.models import DSPClient from .backends.base import DSPBackend from .backends.faker import FakerDSPBackend from .backends.spotify import SpotifyDSPBackend from .exceptions import DSPNotRegisteredError from .guard import RateLimitGuard from .stats import InMemoryStatsBackend, RedisStatsBackend, RequestStats, StatsBackend from .types import ( FollowedArtistsResult, PlaylistsResult, ProfileResult, RecentlyPlayedResult, SavedAlbumsResult, SavedTracksResult, TokenResult, TopArtistsResult, TopTracksResult, ) # --------------------------------------------------------------------------- # Gateway # --------------------------------------------------------------------------- class DSPGateway: def __init__(self) -> None: self._backends: dict[str, DSPBackend] = {} self._guards: dict[str, RateLimitGuard] = {} def register( self, client_name: str, backend: DSPBackend, guard: RateLimitGuard | None = None, ) -> None: self._backends[client_name] = backend if guard is not None: self._guards[client_name] = guard def is_configured(self, client_name: str) -> bool: return client_name in self._backends def get(self, client_name: str) -> DSPBackend: try: return self._backends[client_name] except KeyError: raise DSPNotRegisteredError(client_name) from None def _call[T](self, client_name: str, fn: Callable[[], T]) -> T: guard = self._guards.get(client_name) if guard is not None: return guard.execute(fn) return fn() # -- API ----------------------------------------------------------------- def refresh_token(self, client_name: str, refresh_token: str) -> TokenResult: return self._call( client_name, lambda: self.get(client_name).refresh_token(refresh_token) ) def get_profile(self, client_name: str, access_token: str) -> ProfileResult: return self._call( client_name, lambda: self.get(client_name).get_profile(access_token) ) def get_top_artists(self, client_name: str, access_token: str) -> TopArtistsResult: return self._call( client_name, lambda: self.get(client_name).get_top_artists(access_token) ) def get_top_tracks(self, client_name: str, access_token: str) -> TopTracksResult: return self._call( client_name, lambda: self.get(client_name).get_top_tracks(access_token) ) def get_recently_played( self, client_name: str, access_token: str, *, after: int | None = None ) -> RecentlyPlayedResult: return self._call( client_name, lambda: self.get(client_name).get_recently_played( access_token, after=after ), ) def get_playlists(self, client_name: str, access_token: str) -> PlaylistsResult: return self._call( client_name, lambda: self.get(client_name).get_playlists(access_token) ) def get_saved_albums( self, client_name: str, access_token: str ) -> SavedAlbumsResult: return self._call( client_name, lambda: self.get(client_name).get_saved_albums(access_token) ) def get_saved_tracks( self, client_name: str, access_token: str ) -> SavedTracksResult: return self._call( client_name, lambda: self.get(client_name).get_saved_tracks(access_token) ) def get_followed_artists( self, client_name: str, access_token: str ) -> FollowedArtistsResult: return self._call( client_name, lambda: self.get(client_name).get_followed_artists(access_token), ) def stats(self, client_name: str) -> RequestStats | None: guard = self._guards.get(client_name) return guard.stats if guard is not None else None def warm_up(self) -> None: """Force lazy-proxy initialization without performing any API calls.""" def close(self) -> None: for backend in self._backends.values(): backend.close() def __enter__(self) -> DSPGateway: return self def __exit__(self, *_: object) -> None: self.close() # --------------------------------------------------------------------------- # Factory helpers # --------------------------------------------------------------------------- @db.autocommit def _get_nominal_rps(client_name: str) -> int | None: client = DSPClient.query.where(DSPClient.name == client_name).one_or_none() return client.nominal_rps if client is not None else None def _register_backend( gateway: DSPGateway, *, client_name: str, backend: DSPBackend, use_faker: bool, ) -> None: gateway.register( client_name, backend=( FakerDSPBackend( dsp_id=backend.dsp_id, nominal_rps=_get_nominal_rps(client_name), ) if use_faker else backend ), guard=_create_guard(client_name), ) def _create_guard(client_name: str) -> RateLimitGuard: stats: StatsBackend = ( RedisStatsBackend(redis_client, f"resonance:streaming:{client_name}") if settings.dsp_rate_limit_stats_backend == "redis" else InMemoryStatsBackend() ) return RateLimitGuard(stats_backend=stats) # --------------------------------------------------------------------------- # Registration # --------------------------------------------------------------------------- def get_dsp_gateway() -> DSPGateway: gateway = DSPGateway() _register_backend( gateway, client_name="spotify_songwhip", backend=SpotifyDSPBackend( client=SpotifyClient( client_id=settings.spotify_songwhip_client_id, client_secret=settings.spotify_songwhip_client_secret, ) ), use_faker=settings.spotify_songwhip_use_faker, ) _register_backend( gateway, client_name="spotify_smf", backend=SpotifyDSPBackend( client=SpotifyClient( client_id=settings.spotify_smf_client_id, client_secret=settings.spotify_smf_client_secret, ) ), use_faker=settings.spotify_smf_use_faker, ) return gateway dsp_gateway = lazy_proxy(get_dsp_gateway)