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.
138 lines
5.2 KiB
138 lines
5.2 KiB
"""Spotify playlist source adapter.
|
|
|
|
Wraps ``core.spotify_client.SpotifyClient`` — the authenticated Spotify
|
|
client used everywhere else. Adapter projects Spotify's ``Playlist`` /
|
|
``Track`` dataclasses into ``PlaylistMeta`` / ``NormalizedTrack``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
from core.playlists.sources.base import (
|
|
NormalizedTrack,
|
|
PlaylistDetail,
|
|
PlaylistMeta,
|
|
PlaylistSource,
|
|
SOURCE_SPOTIFY,
|
|
)
|
|
|
|
|
|
class SpotifyPlaylistSource(PlaylistSource):
|
|
name = SOURCE_SPOTIFY
|
|
supports_listing = True
|
|
supports_refresh = True
|
|
requires_auth = True
|
|
|
|
def __init__(self, client_getter: Callable[[], Any]):
|
|
"""``client_getter`` returns the live ``SpotifyClient`` singleton.
|
|
|
|
We accept a getter (not the client itself) so the adapter can be
|
|
constructed at import time, before ``web_server.py`` has wired
|
|
up the singleton."""
|
|
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
|
|
# ``is_spotify_authenticated`` is the Spotify-specific check;
|
|
# ``is_authenticated`` on SpotifyClient is a metadata-aware
|
|
# superset that returns True even when only the iTunes
|
|
# fallback is available. The adapter needs the strict check
|
|
# because it calls Spotify-only endpoints (get_user_playlists,
|
|
# get_playlist_by_id).
|
|
check = getattr(client, "is_spotify_authenticated", None) or client.is_authenticated
|
|
return bool(check())
|
|
|
|
def list_playlists(self) -> List[PlaylistMeta]:
|
|
client = self._client()
|
|
if client is None or not self.is_authenticated():
|
|
return []
|
|
playlists = client.get_user_playlists_metadata_only()
|
|
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 self.is_authenticated():
|
|
return None
|
|
playlist = client.get_playlist_by_id(playlist_id)
|
|
if playlist is None:
|
|
return None
|
|
meta = self._meta_from_playlist(playlist)
|
|
tracks = [self._track_from_spotify(t, idx) for idx, t in enumerate(playlist.tracks)]
|
|
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:
|
|
return PlaylistMeta(
|
|
source=self.name,
|
|
source_playlist_id=str(playlist.id),
|
|
name=playlist.name,
|
|
owner=playlist.owner,
|
|
description=playlist.description,
|
|
track_count=int(getattr(playlist, "total_tracks", 0) or 0),
|
|
extra={
|
|
"public": bool(getattr(playlist, "public", False)),
|
|
"collaborative": bool(getattr(playlist, "collaborative", False)),
|
|
},
|
|
)
|
|
|
|
def _track_from_spotify(self, track: Any, position: int) -> NormalizedTrack:
|
|
artists = getattr(track, "artists", None) or []
|
|
artist_name = artists[0] if artists else "Unknown Artist"
|
|
track_id = str(track.id) if getattr(track, "id", None) else ""
|
|
track_name = track.name or ""
|
|
album_name = getattr(track, "album", "") or ""
|
|
duration_ms = int(getattr(track, "duration_ms", 0) or 0)
|
|
image_url = getattr(track, "image_url", None)
|
|
|
|
# Spotify's authenticated API IS canonical metadata — populate
|
|
# the discovered/matched_data block so to_mirror_track_dict emits
|
|
# the same extra_data shape downstream consumers (sync, wishlist)
|
|
# already expect from this path.
|
|
extra: Dict[str, Any] = {
|
|
"popularity": getattr(track, "popularity", 0),
|
|
"external_urls": getattr(track, "external_urls", None),
|
|
"preview_url": getattr(track, "preview_url", None),
|
|
}
|
|
if track_id:
|
|
album_obj: Dict[str, Any] = {"name": album_name}
|
|
if image_url:
|
|
album_obj["images"] = [{
|
|
"url": image_url,
|
|
"height": 600,
|
|
"width": 600,
|
|
}]
|
|
extra["discovered"] = True
|
|
extra["provider"] = "spotify"
|
|
extra["confidence"] = 1.0
|
|
extra["matched_data"] = {
|
|
"id": track_id,
|
|
"name": track_name,
|
|
"artists": [{"name": str(a)} for a in artists],
|
|
"album": album_obj,
|
|
"duration_ms": duration_ms,
|
|
"image_url": image_url,
|
|
}
|
|
|
|
return NormalizedTrack(
|
|
position=position,
|
|
track_name=track_name,
|
|
artist_name=str(artist_name),
|
|
album_name=album_name or None,
|
|
duration_ms=duration_ms,
|
|
source_track_id=track_id,
|
|
image_url=image_url,
|
|
needs_discovery=False,
|
|
extra=extra,
|
|
)
|