#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.14" # dependencies = [] # /// """Dump tracks from one or more Spotify playlists (Client Credentials flow). just spotify # uses the placeholder ids below just spotify " ..." # explicit playlists just spotify "--file lists.txt --json" just spotify " --csv" # single sockseek CSV (stdout) just spotify " --csv --out tracks.csv" Playlists can be ids, full URLs, or spotify: URIs. Works for any public playlist; no user login needed. Requires SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET in the environment (loaded from .env by just). --csv writes one combined CSV with Artist,Title,Album,Length columns (Length in seconds), the format consumed by sockseek/sldl. Each playlist is preceded by a `# ` comment line as a separator. Writes to stdout, or to a file with --out . """ import base64 import csv import json import os import re import sys import urllib.parse import urllib.request def get_token(client_id, client_secret): auth = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() data = urllib.parse.urlencode({"grant_type": "client_credentials"}).encode() req = urllib.request.Request( "https://accounts.spotify.com/api/token", data=data, headers={"Authorization": f"Basic {auth}"}, ) with urllib.request.urlopen(req) as resp: return json.load(resp)["access_token"] def api_get(url, token): req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"}) with urllib.request.urlopen(req) as resp: return json.load(resp) def parse_playlist_id(arg): m = re.search(r"playlist[/:]([A-Za-z0-9]+)", arg) return m.group(1) if m else arg.strip() def read_list_file(path): out = [] with open(path) as fh: for line in fh: line = line.split("#", 1)[0].strip() if line: out.append(line) return out def fetch_playlist(pid, token): meta = api_get( f"https://api.spotify.com/v1/playlists/{pid}" f"?fields={urllib.parse.quote('name,owner(display_name)')}", token, ) fields = "items(track(name,artists(name),album(name),duration_ms)),next" url = ( f"https://api.spotify.com/v1/playlists/{pid}/tracks" f"?limit=100&fields={urllib.parse.quote(fields)}" ) tracks = [] while url: page = api_get(url, token) for item in page.get("items", []): t = item.get("track") if not t: continue tracks.append( { "title": t["name"], "artists": [a["name"] for a in t["artists"]], "album": t["album"]["name"], "duration_ms": t["duration_ms"], } ) url = page.get("next") return {"id": pid, "name": meta.get("name"), "tracks": tracks} def write_csv(playlists, fh): """Single combined sockseek/sldl CSV, one `# ` comment per playlist.""" w = csv.writer(fh) w.writerow(["Artist", "Title", "Album", "Length"]) for pl in playlists: fh.write(f"# {pl['name']}\n") for t in pl["tracks"]: w.writerow( [ ", ".join(t["artists"]), t["title"], t["album"], round(t["duration_ms"] / 1000), ] ) def main(): argv = sys.argv[1:] as_json = "--json" in argv as_csv = "--csv" in argv argv = [a for a in argv if a not in ("--json", "--csv")] inputs = [] out_path = None i = 0 while i < len(argv): if argv[i] == "--file": inputs += read_list_file(argv[i + 1]) i += 2 elif argv[i] == "--out": out_path = argv[i + 1] i += 2 else: inputs.append(argv[i]) i += 1 if not inputs: sys.exit( "usage: spotify.py [--json|--csv] " "[--file list.txt] [--out path]" ) client_id = os.environ.get("SPOTIFY_CLIENT_ID") client_secret = os.environ.get("SPOTIFY_CLIENT_SECRET") if not (client_id and client_secret): sys.exit("set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET") token = get_token(client_id, client_secret) playlists = [fetch_playlist(parse_playlist_id(x), token) for x in inputs] if as_json: print(json.dumps(playlists, indent=2, ensure_ascii=False)) return if as_csv: if out_path: with open(out_path, "w", newline="") as fh: write_csv(playlists, fh) print(f"wrote {sum(len(p['tracks']) for p in playlists)} tracks to {out_path}") else: write_csv(playlists, sys.stdout) return for pl in playlists: print(f"\n=== {pl['name']} ({len(pl['tracks'])} tracks) ===") for n, t in enumerate(pl["tracks"], 1): print(f"{n:>3}. {', '.join(t['artists'])} - {t['title']}") if __name__ == "__main__": main()