mirror of https://github.com/Nezreka/SoulSync.git
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.
109 lines
4.0 KiB
109 lines
4.0 KiB
"""Deezer playlist source adapter.
|
|
|
|
Wraps ``core.deezer_client.DeezerClient.get_playlist``. Deezer's public
|
|
API needs no auth, so ``is_authenticated`` always returns True. Listing
|
|
the *user's* playlists requires OAuth — surfaced via the underlying
|
|
``is_user_authenticated`` flag — but the get-by-id flow works on any
|
|
public playlist regardless.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
from core.playlists.sources.base import (
|
|
NormalizedTrack,
|
|
PlaylistDetail,
|
|
PlaylistMeta,
|
|
PlaylistSource,
|
|
SOURCE_DEEZER,
|
|
)
|
|
|
|
|
|
class DeezerPlaylistSource(PlaylistSource):
|
|
name = SOURCE_DEEZER
|
|
supports_listing = True # user playlists need OAuth; falls back to []
|
|
supports_refresh = True
|
|
requires_auth = False
|
|
|
|
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
|
|
# Deezer's `is_authenticated` is True even with no OAuth token —
|
|
# the public API works without one. Use that as our liveness signal.
|
|
return bool(client.is_authenticated())
|
|
|
|
def list_playlists(self) -> List[PlaylistMeta]:
|
|
client = self._client()
|
|
if client is None:
|
|
return []
|
|
# User playlists need OAuth; `get_user_playlists` returns [] when
|
|
# the stub-interface variant is in use. Honor whatever the client
|
|
# actually returns.
|
|
try:
|
|
playlists = client.get_user_playlists() or []
|
|
except Exception:
|
|
return []
|
|
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:
|
|
return None
|
|
data = client.get_playlist(playlist_id)
|
|
if not data:
|
|
return None
|
|
meta = self._meta_from_dict(data)
|
|
tracks_raw = data.get("tracks") or []
|
|
tracks = [self._track_from_dict(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:
|
|
"""Project a ``DeezerClient.Playlist`` dataclass into PlaylistMeta."""
|
|
return PlaylistMeta(
|
|
source=self.name,
|
|
source_playlist_id=str(getattr(playlist, "id", "")),
|
|
name=getattr(playlist, "name", "Deezer Playlist"),
|
|
description=getattr(playlist, "description", None),
|
|
track_count=int(getattr(playlist, "total_tracks", 0) or 0),
|
|
owner=getattr(playlist, "owner", None),
|
|
)
|
|
|
|
def _meta_from_dict(self, p: Dict[str, Any]) -> PlaylistMeta:
|
|
return PlaylistMeta(
|
|
source=self.name,
|
|
source_playlist_id=str(p.get("id", "")),
|
|
name=p.get("name", "Deezer Playlist"),
|
|
description=p.get("description") or None,
|
|
owner=p.get("owner") or None,
|
|
image_url=p.get("image_url") or None,
|
|
track_count=int(p.get("track_count", 0) or 0),
|
|
)
|
|
|
|
def _track_from_dict(self, t: Dict[str, Any], position: int) -> NormalizedTrack:
|
|
artists = t.get("artists") or []
|
|
artist_name = artists[0] if artists else "Unknown Artist"
|
|
return NormalizedTrack(
|
|
position=position,
|
|
track_name=t.get("name", "Unknown Track"),
|
|
artist_name=artist_name,
|
|
album_name=t.get("album") or None,
|
|
duration_ms=int(t.get("duration_ms", 0) or 0),
|
|
source_track_id=str(t.get("id", "")),
|
|
needs_discovery=False,
|
|
extra={"track_number": t.get("track_number")},
|
|
)
|