Replaces rate-limited api.spotify.com calls with spotdl's SpotAPI approach, which uses Spotify's internal browser endpoints — no developer credentials needed, no api.spotify.com rate limits. TOTP anonymous token kept as automatic fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Resolves a Spotify URL to track metadata using spotdl (TzurSoffer fork).
|
|
Usage: python3 spotify_resolve.py <spotify_url>
|
|
Outputs JSON to stdout.
|
|
"""
|
|
import sys
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import subprocess
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print(json.dumps({"error": "No URL provided"}))
|
|
sys.exit(1)
|
|
|
|
url = sys.argv[1]
|
|
tmp = tempfile.mktemp(suffix=".spotdl")
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
["spotdl", "save", url, "--save-file", tmp, "--log-level", "ERROR"],
|
|
capture_output=True, text=True, timeout=60
|
|
)
|
|
if result.returncode != 0:
|
|
err = result.stderr.strip() or result.stdout.strip() or "spotdl failed"
|
|
print(json.dumps({"error": err}))
|
|
sys.exit(1)
|
|
|
|
if not os.path.exists(tmp):
|
|
print(json.dumps({"error": "spotdl produced no output file"}))
|
|
sys.exit(1)
|
|
|
|
with open(tmp, "r", encoding="utf-8") as f:
|
|
songs = json.load(f)
|
|
|
|
if not songs:
|
|
print(json.dumps({"error": "No tracks found"}))
|
|
sys.exit(1)
|
|
|
|
# Transform spotdl Song format to NovaSonic format
|
|
def fmt_track(s):
|
|
artists = s.get("artists") or []
|
|
artist_str = ", ".join(artists) if artists else s.get("artist", "")
|
|
date = s.get("date") or (str(s["year"]) if s.get("year") else "")
|
|
return {
|
|
"trackName": s.get("name", ""),
|
|
"artistName": artist_str,
|
|
"collectionName": s.get("album_name", ""),
|
|
"artworkUrl100": s.get("cover_url", ""),
|
|
"releaseDate": date,
|
|
"trackNumber": s.get("track_number"),
|
|
"trackExplicitness": "explicit" if s.get("explicit") else "notExplicit",
|
|
"durationMs": int(s["duration"] * 1000) if s.get("duration") else None,
|
|
}
|
|
|
|
tracks = [fmt_track(s) for s in songs]
|
|
first = songs[0]
|
|
artists0 = first.get("artists") or []
|
|
artist0 = ", ".join(artists0) if artists0 else first.get("artist", "")
|
|
|
|
# Determine type from URL
|
|
if "/track/" in url:
|
|
out = {
|
|
"type": "track",
|
|
"name": first.get("name", ""),
|
|
"artist": artist0,
|
|
"artworkUrl": first.get("cover_url", ""),
|
|
"tracks": tracks,
|
|
}
|
|
elif "/album/" in url:
|
|
out = {
|
|
"type": "album",
|
|
"name": first.get("album_name", ""),
|
|
"artist": first.get("album_artist", artist0),
|
|
"artworkUrl": first.get("cover_url", ""),
|
|
"tracks": tracks,
|
|
}
|
|
else:
|
|
out = {
|
|
"type": "playlist",
|
|
"name": first.get("list_name", ""),
|
|
"artist": first.get("album_artist", ""),
|
|
"artworkUrl": first.get("cover_url", ""),
|
|
"skippedLocal": 0,
|
|
"tracks": tracks,
|
|
}
|
|
|
|
print(json.dumps(out, ensure_ascii=False))
|
|
|
|
finally:
|
|
try:
|
|
os.unlink(tmp)
|
|
except Exception:
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|