Files
musicdownload/core/spotify_client.py
T
luzadevandClaude Opus 4.7 8c06a26ddb Initial commit: MusicDownload con UI pywebview
- Frontend HTML/CSS/JS in webui/ (stile Spotify-like, hero gradient, modal blur)
- API bridge JS<->Python in api/bridge.py
- Core invariato: downloader yt-dlp, upgrader, spotify client, config
- Build scripts macOS (WebKit) e Windows (EdgeChromium)
- .gitignore esclude credenziali (config.json, cookies.txt) e artefatti build

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 12:48:58 +02:00

178 lines
5.9 KiB
Python

"""Client API Spotify — autenticazione e fetch playlist/track/album."""
from __future__ import annotations
import re
import requests
def get_access_token(client_id: str, client_secret: str) -> str:
"""Ottiene un access token tramite Client Credentials Flow."""
resp = requests.post(
"https://accounts.spotify.com/api/token",
data={"grant_type": "client_credentials"},
auth=(client_id, client_secret),
timeout=15,
)
resp.raise_for_status()
token = resp.json().get("access_token")
if not token:
raise ValueError("Token non ricevuto da Spotify")
return token
# ---------------------------------------------------------------------------
# Riconoscimento tipo URL
# ---------------------------------------------------------------------------
def detect_url_type(url: str) -> tuple[str, str]:
"""Rileva il tipo di URL Spotify e ne estrae l'ID.
Ritorna (tipo, id) dove tipo e "track", "album" o "playlist".
Solleva ValueError se l'URL non e riconosciuto.
"""
for kind in ("track", "album", "playlist"):
match = re.search(rf"{kind}/([a-zA-Z0-9]+)", url)
if match:
return kind, match.group(1)
raise ValueError(
"URL non riconosciuto. Sono supportati: playlist, album e singoli brani."
)
# ---------------------------------------------------------------------------
# Singolo brano
# ---------------------------------------------------------------------------
def get_track_info(token: str, track_id: str) -> tuple[str, list[dict]]:
"""Ritorna (track_name, [{"name": ..., "artist": ...}]) per un singolo brano."""
resp = requests.get(
f"https://api.spotify.com/v1/tracks/{track_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
name = data.get("name", "Unknown")
artists = data.get("artists", [])
artist = artists[0].get("name", "Unknown") if artists else "Unknown"
return name, [{"name": name, "artist": artist}]
# ---------------------------------------------------------------------------
# Album
# ---------------------------------------------------------------------------
def get_album_info(token: str, album_id: str) -> tuple[str, list[dict]]:
"""Ritorna (album_name, lista tracce) per un album."""
resp = requests.get(
f"https://api.spotify.com/v1/albums/{album_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
album_name = data.get("name", "Unknown Album")
tracks = []
# Prima pagina inclusa nella risposta
tracks_data = data.get("tracks", {})
_collect_album_tracks(tracks_data, tracks)
# Paginazione se ci sono piu di 50 tracce
next_url = tracks_data.get("next")
while next_url:
resp = requests.get(
next_url,
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
resp.raise_for_status()
page = resp.json()
_collect_album_tracks(page, tracks)
next_url = page.get("next")
return album_name, tracks
def _collect_album_tracks(data: dict, out: list[dict]) -> None:
for item in data.get("items", []):
name = item.get("name", "Unknown")
artists = item.get("artists", [])
artist = artists[0].get("name", "Unknown") if artists else "Unknown"
out.append({"name": name, "artist": artist})
# ---------------------------------------------------------------------------
# Playlist
# ---------------------------------------------------------------------------
def get_playlist_info(token: str, playlist_id: str) -> tuple[str, list[dict]]:
"""Ritorna (playlist_name, lista tracce) per una playlist."""
resp = requests.get(
f"https://api.spotify.com/v1/playlists/{playlist_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
name = data.get("name")
if not name:
raise ValueError("Impossibile leggere il nome della playlist")
tracks = get_all_tracks(token, playlist_id)
return name, tracks
def get_all_tracks(token: str, playlist_id: str) -> list[dict]:
"""Scarica tutte le tracce con paginazione (100 per pagina).
Ritorna lista di {"name": ..., "artist": ...}.
"""
tracks: list[dict] = []
url: str | None = f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks?limit=100&offset=0"
while url:
resp = requests.get(
url,
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
for item in data.get("items", []):
track = item.get("track")
if not track:
continue
name = track.get("name", "Unknown")
artists = track.get("artists", [])
artist = artists[0].get("name", "Unknown") if artists else "Unknown"
tracks.append({"name": name, "artist": artist})
url = data.get("next") # None quando non ci sono altre pagine
return tracks
# ---------------------------------------------------------------------------
# Funzione unificata
# ---------------------------------------------------------------------------
def resolve_spotify_url(token: str, url: str) -> tuple[str, str, list[dict]]:
"""Risolve qualsiasi URL Spotify supportato.
Ritorna (label, nome, lista_tracce) dove label e "Brano"/"Album"/"Playlist".
"""
kind, item_id = detect_url_type(url)
if kind == "track":
name, tracks = get_track_info(token, item_id)
return "Brano", name, tracks
elif kind == "album":
name, tracks = get_album_info(token, item_id)
return "Album", name, tracks
else:
name, tracks = get_playlist_info(token, item_id)
return "Playlist", name, tracks