"""Tidal playlist source adapter. Wraps ``core.tidal_client.TidalClient``. Tidal recognizes the virtual ``tidal-favorites`` ID inside ``get_playlist`` already, so the adapter doesn't need to special-case it. """ from __future__ import annotations from typing import Any, Callable, List, Optional from core.playlists.sources.base import ( NormalizedTrack, PlaylistDetail, PlaylistMeta, PlaylistSource, SOURCE_TIDAL, ) class TidalPlaylistSource(PlaylistSource): name = SOURCE_TIDAL supports_listing = True supports_refresh = True requires_auth = True def __init__(self, client_getter: Callable[[], Any]): self._client_getter = client_getter def _client(self): return self._client_getter() def is_authenticated(self) -> bool: client = self._client() if client is None: return False return bool(client.is_authenticated()) def list_playlists(self) -> List[PlaylistMeta]: client = self._client() if client is None or not client.is_authenticated(): return [] playlists = client.get_user_playlists_metadata_only() or [] return [self._meta_from_playlist(p) for p in playlists] def get_playlist(self, playlist_id: str) -> Optional[PlaylistDetail]: client = self._client() if client is None or not client.is_authenticated(): return None playlist = client.get_playlist(playlist_id) if playlist is None: return None meta = self._meta_from_playlist(playlist) tracks_raw = getattr(playlist, "tracks", None) or [] tracks = [self._track_from_tidal(t, idx) for idx, t in enumerate(tracks_raw)] meta.track_count = len(tracks) return PlaylistDetail(meta=meta, tracks=tracks) def refresh_playlist(self, playlist_id: str) -> Optional[PlaylistDetail]: return self.get_playlist(playlist_id) # ---- projection helpers ------------------------------------------------ def _meta_from_playlist(self, playlist: Any) -> PlaylistMeta: owner_field = getattr(playlist, "owner", None) owner_name: Optional[str] = None if isinstance(owner_field, dict): owner_name = owner_field.get("name") or owner_field.get("id") elif owner_field: owner_name = str(owner_field) tracks_raw = getattr(playlist, "tracks", None) or [] return PlaylistMeta( source=self.name, source_playlist_id=str(playlist.id), name=playlist.name, owner=owner_name, description=getattr(playlist, "description", "") or None, track_count=len(tracks_raw), extra={ "public": bool(getattr(playlist, "public", True)), "external_urls": getattr(playlist, "external_urls", {}), }, ) def _track_from_tidal(self, track: Any, position: int) -> NormalizedTrack: artists = getattr(track, "artists", None) or [] # First artist only — matches the mirrored_playlist shape the # legacy refresh_mirrored handler wrote. artist_name = artists[0] if artists else "Unknown Artist" return NormalizedTrack( position=position, track_name=track.name, artist_name=artist_name, album_name=getattr(track, "album", "") or None, duration_ms=int(getattr(track, "duration_ms", 0) or 0), source_track_id=str(track.id), needs_discovery=False, extra={ "explicit": bool(getattr(track, "explicit", False)), "external_urls": getattr(track, "external_urls", {}), "popularity": getattr(track, "popularity", 0), }, )