"""ApplicationService — mirrors ApplicationInstanceManager.cs + BaseModule.cs logic. Resolution order for get_by_country_code(code): 1. "global" → first app where global_push_application=True 2. "other" → first app where fallback_application=True and workout_market=False 3. Numeric → app by integer id 4. String → first app whose spotify_region_code matches (case-insensitive) All applications are cached in memory for TTL_SECONDS to avoid repeated DB hits. """ import time from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from playlist_sync.models.application import Application TTL_SECONDS = 300 # 5-minute in-memory cache class ApplicationService: _cache: list[Application] = [] _cache_ts: float = 0.0 def __init__(self, session: AsyncSession): self._session = session # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ async def _load_all(self) -> list[Application]: """Return all applications, refreshing in-memory cache when stale.""" now = time.monotonic() if ( ApplicationService._cache and (now - ApplicationService._cache_ts) < TTL_SECONDS ): return ApplicationService._cache result = await self._session.execute(select(Application)) apps: list[Application] = list(result.scalars().all()) ApplicationService._cache = apps ApplicationService._cache_ts = now return apps # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ async def get_all(self) -> list[Application]: return await self._load_all() async def get_by_id(self, app_id: int) -> Optional[Application]: apps = await self._load_all() return next((a for a in apps if a.id == app_id), None) async def get_fallback(self) -> Optional[Application]: """Return the fallback (non-workout) application. Mirrors GetFallbackApplication(). """ apps = await self._load_all() return next( (a for a in apps if a.fallback_application and not a.workout_market), None ) async def get_global_push(self) -> Optional[Application]: """Return the global-push application — mirrors GetGlobalPushApplication().""" apps = await self._load_all() return next((a for a in apps if a.global_push_application), None) async def get_by_country_code(self, country_code: str) -> Optional[Application]: """Resolve a URL country_code parameter to an Application. Mirrors the Before-hook in BaseModule.cs used by PlaylistSyncModule. Precedence: 1. "global" → GlobalPushApplication 2. "other" → FallbackApplication (non-workout) 3. Numeric → lookup by integer id 4. String → first app whose spotify_region_code matches (case-insensitive) """ if not country_code: return None lower = country_code.strip().lower() if lower == "global": return await self.get_global_push() if lower == "other": return await self.get_fallback() apps = await self._load_all() # Numeric id lookup if lower.lstrip("-").isdigit(): try: app_id = int(country_code) return next((a for a in apps if a.id == app_id), None) except ValueError: pass # SpotifyRegionCode match (case-insensitive) return next( ( a for a in apps if a.spotify_region_code and a.spotify_region_code.lower() == lower ), None, ) @staticmethod def invalidate_cache() -> None: """Force next call to re-query the database.""" ApplicationService._cache = [] ApplicationService._cache_ts = 0.0