diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 917a67c..f437f2f 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -251,25 +251,16 @@ async function fetchSpotifyToken() { async function _spApi(path, token) { const url = path.startsWith('http') ? path : `https://api.spotify.com/v1${path}`; - for (let attempt = 0; attempt < 4; attempt++) { - const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } }); - if (r.status === 429) { - const retryAfter = parseInt(r.headers.get('Retry-After') || '5', 10); - // Long Retry-After = token is banned — invalidate cache and fail immediately - if (retryAfter > 60) { - _spTokenCache = null; - _spTokenExpiry = 0; - throw new Error(`Spotify-Token für ${Math.round(retryAfter / 3600)}h gesperrt (Rate Limit). Token-Cache geleert — nächster Versuch holt neuen Token.`); - } - const wait = (retryAfter + 1) * 1000; - console.log(`[spotify] 429 waiting ${retryAfter + 1}s`); - await new Promise(res => setTimeout(res, wait)); - continue; - } - if (!r.ok) throw new Error(`Spotify API Fehler ${r.status}: ${path}`); - return r.json(); + const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } }); + if (r.status === 429) { + const retryAfter = parseInt(r.headers.get('Retry-After') || '10', 10); + _spTokenCache = null; + _spTokenExpiry = 0; + console.log(`[spotify] 429 Retry-After=${retryAfter}s — failing fast`); + throw new Error(`RATE_LIMITED:${retryAfter}`); } - throw new Error('Spotify API rate-limited — bitte kurz warten und erneut versuchen'); + if (!r.ok) throw new Error(`Spotify API Fehler ${r.status}: ${path}`); + return r.json(); } async function _spAllPlaylistTracks(id, token) { @@ -423,6 +414,10 @@ app.post('/api/spotify/resolve', async (req, res) => { if (e.message.includes('403') && e.message.includes('/playlists/')) { return res.status(403).json({ error: 'EDITORIAL_PLAYLIST' }); } + if (e.message.startsWith('RATE_LIMITED:')) { + const secs = parseInt(e.message.split(':')[1], 10) || 30; + return res.status(429).json({ error: 'RATE_LIMITED', retryAfter: secs }); + } res.status(500).json({ error: e.message }); } }); diff --git a/src/spotify.js b/src/spotify.js index 6e7e8d6..68ee4ac 100644 --- a/src/spotify.js +++ b/src/spotify.js @@ -68,6 +68,7 @@ export async function resolveSpotifyLink() { if (!res.ok) { const err = await res.json().catch(() => ({})); if (err.error === 'EDITORIAL_PLAYLIST') throw new Error('EDITORIAL_PLAYLIST'); + if (err.error === 'RATE_LIMITED') throw new Error(`RATE_LIMITED:${err.retryAfter || 30}`); throw new Error(err.error || err.message || `HTTP ${res.status}`); } const data = await res.json(); @@ -79,6 +80,17 @@ export async function resolveSpotifyLink() { inner.innerHTML = `
Zeitüberschreitung — der Server hat nicht rechtzeitig geantwortet. Läuft das Backend?
`; return; } + if (e.message.startsWith('RATE_LIMITED:')) { + const secs = parseInt(e.message.split(':')[1], 10) || 30; + const mins = secs >= 60 ? `${Math.ceil(secs / 60)} Min.` : `${secs}s`; + inner.innerHTML = `
+
⏳ Spotify Rate Limit
+
+ Spotify hat zu viele Anfragen erhalten. Bitte in ca. ${mins} erneut versuchen. +
+
`; + return; + } if (e.message === 'EDITORIAL_PLAYLIST') { inner.innerHTML = `
💡 Editorial Playlist erkannt