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.
153 lines
6.0 KiB
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)
|