Server tracks _spRateLimitUntil and rejects retries instantly without
hitting Spotify, returning the remaining wait time. Retrying before the
window expired was resetting Spotify's timer (19s → 7s → 48s).
Frontend now shows a live countdown with a note not to retry early.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spotdl (SpotAPI) works without developer credentials. Remove the
health-check warning, credential error UI, and Client Credentials
fallback that were blocking the importer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Retrying with wait times (23s + 60s + 60s) exceeded the 120s global
timeout and showed a generic timeout error. Now returns immediately
with a RATE_LIMITED error and shows the user how long to wait.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts resolve logic into _resolveSpotify() and wraps it in
Promise.race with a 25s timer so the backend always sends a response
even if undici's AbortSignal.timeout silently fails on blocked sockets.
Also reduces frontend timeout from 90s to 30s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend fetch to /api/spotify/resolve had no timeout — if the backend
stalled (Spotify API slow or unreachable), the spinner would never stop.
Also caps the health check at 5s so loadSpotifyImporter can't hang.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend returns {error:'EDITORIAL_PLAYLIST'} on 403 playlist routes.
Frontend shows a step-by-step workaround card instead of a red error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the broken open.spotify.com/get_access_token scrape with
accounts.spotify.com/api/token (Basic auth, client_credentials grant).
Adds SPOTIFY_CLIENT_ID/SECRET to config, docker-compose, and .env.example.
Health endpoint now reports spotify.configured; frontend disables the
importer and shows a setup guide when credentials are missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- itunesSearch: use r.text() + JSON.parse so empty body (rate-limit) returns [] instead of throwing
- reduce Spotify artwork batch size 8→4 to avoid hammering iTunes and affecting global search
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _loadSpotifyTrackArtwork: rewritten to call itunesSearch() directly from browser (same as global search) instead of backend endpoint — same quality, works for all regions
- enriches track objects with collectionName, releaseDate, trackNumber, trackExplicitness so downloads get full metadata too
- 8-concurrent batch to avoid hammering iTunes
- adds onerror fallback to makeGlobalTrackItem img so broken URLs show 🎵 placeholder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- track-artwork endpoint: split artist on ,/•/ before iTunes search (same as download flow)
- track-artwork endpoint: fall back to existingArt when iTunes returns nothing
- frontend: render any artwork already in Spotify data immediately, don't wait for iTunes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>