From 9b197e119a81103415e9b75258537fcc6251ae82 Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 11 May 2026 14:55:41 +0200 Subject: [PATCH] fix: strip ?si= from Spotify URL, improve spotdl error logging spotdl doesn't handle tracking params in URLs. Also separate stdout/stderr in Python script and always try parsing stdout before checking exit code. Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 22 ++++++++++++++-------- backend/spotify_resolve.py | 26 ++++++++++++++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 36d41f4..f63c2e2 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -379,16 +379,22 @@ app.get('/api/health', (req, res) => { async function _resolveSpotifyViaSpotdl(url) { const { execFile } = require('child_process'); const scriptPath = path.join(__dirname, 'spotify_resolve.py'); + // Strip ?si= and other tracking params — spotdl doesn't handle them + const cleanUrl = url.split('?')[0]; return new Promise((resolve, reject) => { - execFile('python3', [scriptPath, url], { timeout: 90000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => { - if (err) return reject(new Error(`spotdl: ${stderr?.trim() || err.message}`)); - try { - const data = JSON.parse(stdout.trim()); - if (data.error) return reject(new Error(data.error)); - resolve(data); - } catch { - reject(new Error(`spotdl: ungültige Ausgabe — ${stdout.slice(0, 200)}`)); + execFile('python3', [scriptPath, cleanUrl], { timeout: 90000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => { + if (stderr?.trim()) console.log('[spotdl-py]', stderr.trim().slice(0, 500)); + // Always try to parse stdout first — script outputs JSON even on error + const out = stdout?.trim(); + if (out) { + try { + const data = JSON.parse(out); + if (data.error) return reject(new Error(`spotdl: ${data.error}`)); + return resolve(data); + } catch { /* not JSON, fall through */ } } + if (err) return reject(new Error(`spotdl exited ${err.code}: ${stderr?.trim() || out || err.message}`)); + reject(new Error(`spotdl: keine Ausgabe`)); }); }); } diff --git a/backend/spotify_resolve.py b/backend/spotify_resolve.py index fab495c..238b645 100644 --- a/backend/spotify_resolve.py +++ b/backend/spotify_resolve.py @@ -2,7 +2,7 @@ """ Resolves a Spotify URL to track metadata using spotdl (TzurSoffer fork). Usage: python3 spotify_resolve.py -Outputs JSON to stdout. +Outputs JSON to stdout. Errors go to stderr for clean separation. """ import sys import json @@ -10,36 +10,47 @@ import os import tempfile import subprocess +def eprint(*args): + print(*args, file=sys.stderr) + def main(): if len(sys.argv) < 2: - print(json.dumps({"error": "No URL provided"})) + eprint("No URL provided") sys.exit(1) url = sys.argv[1] tmp = tempfile.mktemp(suffix=".spotdl") try: + eprint(f"[spotdl] running: spotdl save {url}") result = subprocess.run( ["spotdl", "save", url, "--save-file", tmp, "--log-level", "ERROR"], capture_output=True, text=True, timeout=60 ) + eprint(f"[spotdl] exit={result.returncode}") + if result.stdout.strip(): + eprint(f"[spotdl] stdout: {result.stdout.strip()[:300]}") + if result.stderr.strip(): + eprint(f"[spotdl] stderr: {result.stderr.strip()[:300]}") + if result.returncode != 0: - err = result.stderr.strip() or result.stdout.strip() or "spotdl failed" - print(json.dumps({"error": err})) + err = result.stderr.strip() or result.stdout.strip() or "spotdl exited non-zero" + eprint(f"[spotdl] FAILED: {err}") sys.exit(1) if not os.path.exists(tmp): - print(json.dumps({"error": "spotdl produced no output file"})) + eprint("[spotdl] no output file produced") 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"})) + eprint("[spotdl] empty track list") sys.exit(1) - # Transform spotdl Song format to NovaSonic format + eprint(f"[spotdl] got {len(songs)} tracks") + def fmt_track(s): artists = s.get("artists") or [] artist_str = ", ".join(artists) if artists else s.get("artist", "") @@ -60,7 +71,6 @@ def main(): 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",