You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/core/playlists/sources/tidal.py

104 lines
3.7 KiB

"""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),
},
)