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>
This commit is contained in:
commit
8c06a26ddb
25 files changed
+4316
No files matched your search
@@ -0,0 +1,177 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user