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/api/playlists.py

153 lines
6.0 KiB

"""
Playlist endpoints — list and inspect playlists from Spotify/Tidal.
"""
from flask import request, current_app
from .auth import require_api_key
from .helpers import api_success, api_error
def register_routes(bp):
@bp.route("/playlists", methods=["GET"])
@require_api_key
def list_playlists():
"""List user playlists from Spotify or Tidal.
Query: ?source=spotify|tidal (default: spotify)
"""
source = request.args.get("source", "spotify")
ctx = current_app.soulsync
try:
if source == "spotify":
spotify = ctx.get("spotify_client")
if not spotify or not spotify.is_authenticated():
return api_error("NOT_AUTHENTICATED", "Spotify not authenticated.", 401)
playlists = spotify.get_user_playlists_metadata_only()
return api_success({
"playlists": [
{
"id": p.id,
"name": p.name,
"owner": p.owner,
"track_count": p.total_tracks,
"image_url": getattr(p, "image_url", None),
}
for p in playlists
],
"source": "spotify",
})
elif source == "tidal":
tidal = ctx.get("tidal_client")
if not tidal:
return api_error("NOT_AVAILABLE", "Tidal client not configured.", 503)
playlists = tidal.get_user_playlists_metadata_only()
return api_success({
"playlists": [
{
"id": p.get("id") or p.get("uuid"),
"name": p.get("title") or p.get("name"),
"track_count": p.get("numberOfTracks", 0),
"image_url": p.get("image"),
}
for p in (playlists or [])
],
"source": "tidal",
})
return api_error("BAD_REQUEST", "source must be 'spotify' or 'tidal'.", 400)
except Exception as e:
return api_error("PLAYLIST_ERROR", str(e), 500)
@bp.route("/playlists/<playlist_id>", methods=["GET"])
@require_api_key
def get_playlist(playlist_id):
"""Get playlist details with tracks.
Query: ?source=spotify (default: spotify)
"""
source = request.args.get("source", "spotify")
ctx = current_app.soulsync
try:
if source == "spotify":
spotify = ctx.get("spotify_client")
if not spotify or not spotify.is_authenticated():
return api_error("NOT_AUTHENTICATED", "Spotify not authenticated.", 401)
playlist = spotify.get_playlist_by_id(playlist_id)
if not playlist:
return api_error("NOT_FOUND", "Playlist not found.", 404)
tracks = []
for item in playlist.get("tracks", {}).get("items", []):
t = item.get("track")
if not t:
continue
tracks.append({
"id": t.get("id"),
"name": t.get("name"),
"artists": [a.get("name") for a in t.get("artists", [])],
"album": t.get("album", {}).get("name"),
"duration_ms": t.get("duration_ms"),
"image_url": (t.get("album", {}).get("images", [{}])[0].get("url")
if t.get("album", {}).get("images") else None),
})
return api_success({
"playlist": {
"id": playlist.get("id"),
"name": playlist.get("name"),
"owner": playlist.get("owner", {}).get("display_name"),
"total_tracks": playlist.get("tracks", {}).get("total", len(tracks)),
"tracks": tracks,
},
"source": "spotify",
})
return api_error("BAD_REQUEST", "source must be 'spotify'.", 400)
except Exception as e:
return api_error("PLAYLIST_ERROR", str(e), 500)
@bp.route("/playlists/<playlist_id>/sync", methods=["POST"])
@require_api_key
def sync_playlist(playlist_id):
"""Trigger playlist sync/download.
This delegates to the internal sync endpoint by forwarding the request.
Body: {"playlist_name": "...", "tracks": [...]}
"""
body = request.get_json(silent=True) or {}
playlist_name = body.get("playlist_name")
tracks = body.get("tracks")
if not playlist_name or not tracks:
return api_error("BAD_REQUEST", "Missing 'playlist_name' or 'tracks' in body.", 400)
try:
from web_server import sync_states
if playlist_id in sync_states and sync_states[playlist_id].get("phase") not in ("complete", "error", None):
return api_error("CONFLICT", "Sync already in progress for this playlist.", 409)
except ImportError:
pass
try:
# Forward to the internal sync endpoint
import requests as http_requests
internal_url = f"http://127.0.0.1:8008/api/sync/start"
resp = http_requests.post(internal_url, json={
"playlist_id": playlist_id,
"playlist_name": playlist_name,
"tracks": tracks,
}, timeout=10)
data = resp.json()
if data.get("success"):
return api_success({"message": "Playlist sync started.", "playlist_id": playlist_id})
return api_error("SYNC_FAILED", data.get("error", "Sync failed to start."), 500)
except Exception as e:
return api_error("PLAYLIST_ERROR", str(e), 500)