fix: enforce Spotify rate limit cooldown server- and client-side
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>
This commit is contained in:
1 parent
5265aa1f33
commit
d4c41a9f92
2 files changed
+34
-11
No files matched your search
@@ -124,13 +124,21 @@ async function _resolveSpotifyViaSpotdl(url) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _resolveSpotify(type, id, originalUrl) {
|
// Tracks when the Spotify rate limit window expires so retries never reach Spotify.
|
||||||
return await _resolveSpotifyViaSpotdl(originalUrl);
|
let _spRateLimitUntil = 0;
|
||||||
}
|
|
||||||
|
|
||||||
app.post('/api/spotify/resolve', async (req, res) => {
|
app.post('/api/spotify/resolve', async (req, res) => {
|
||||||
const { url } = req.body;
|
const { url } = req.body;
|
||||||
if (!url) return res.status(400).json({ error: 'url required' });
|
if (!url) return res.status(400).json({ error: 'url required' });
|
||||||
|
|
||||||
|
// Block retries until the cooldown the Spotify told us has actually elapsed.
|
||||||
|
const now = Date.now();
|
||||||
|
if (_spRateLimitUntil > now) {
|
||||||
|
const secs = Math.ceil((_spRateLimitUntil - now) / 1000);
|
||||||
|
console.log(`[spotify/resolve] cooldown active — ${secs}s remaining`);
|
||||||
|
return res.status(429).json({ error: 'RATE_LIMITED', retryAfter: secs });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { type, id } = parseSpotifyUrl(url);
|
const { type, id } = parseSpotifyUrl(url);
|
||||||
console.log(`[spotify/resolve] ${type} ${id}`);
|
console.log(`[spotify/resolve] ${type} ${id}`);
|
||||||
@@ -138,7 +146,7 @@ app.post('/api/spotify/resolve', async (req, res) => {
|
|||||||
const timeout = new Promise((_, reject) =>
|
const timeout = new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('Zeitüberschreitung — Spotify API antwortet nicht (>120s)')), 120000)
|
setTimeout(() => reject(new Error('Zeitüberschreitung — Spotify API antwortet nicht (>120s)')), 120000)
|
||||||
);
|
);
|
||||||
const data = await Promise.race([_resolveSpotify(type, id, url), timeout]);
|
const data = await Promise.race([_resolveSpotifyViaSpotdl(url), timeout]);
|
||||||
res.json(data);
|
res.json(data);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('[spotify/resolve]', e.message);
|
console.error('[spotify/resolve]', e.message);
|
||||||
@@ -147,6 +155,7 @@ app.post('/api/spotify/resolve', async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (e.message.startsWith('RATE_LIMITED:')) {
|
if (e.message.startsWith('RATE_LIMITED:')) {
|
||||||
const secs = parseInt(e.message.split(':')[1], 10) || 30;
|
const secs = parseInt(e.message.split(':')[1], 10) || 30;
|
||||||
|
_spRateLimitUntil = Date.now() + secs * 1000;
|
||||||
return res.status(429).json({ error: 'RATE_LIMITED', retryAfter: secs });
|
return res.status(429).json({ error: 'RATE_LIMITED', retryAfter: secs });
|
||||||
}
|
}
|
||||||
res.status(500).json({ error: e.message });
|
res.status(500).json({ error: e.message });
|
||||||
|
|||||||
+21
-7
@@ -63,13 +63,7 @@ export async function resolveSpotifyLink() {
|
|||||||
}
|
}
|
||||||
if (e.message.startsWith('RATE_LIMITED:')) {
|
if (e.message.startsWith('RATE_LIMITED:')) {
|
||||||
const secs = parseInt(e.message.split(':')[1], 10) || 30;
|
const secs = parseInt(e.message.split(':')[1], 10) || 30;
|
||||||
const mins = secs >= 60 ? `${Math.ceil(secs / 60)} Min.` : `${secs}s`;
|
_startRateLimitCountdown(inner, secs);
|
||||||
inner.innerHTML = `<div style="color:var(--text);padding:16px;background:var(--surface);border-radius:10px;border-left:3px solid var(--warning,#f59e0b)">
|
|
||||||
<div style="font-weight:600;margin-bottom:6px">⏳ Spotify Rate Limit</div>
|
|
||||||
<div style="font-size:13px;line-height:1.5;color:var(--text2)">
|
|
||||||
Spotify hat zu viele Anfragen erhalten. Bitte in ca. <strong>${mins}</strong> erneut versuchen.
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.message === 'EDITORIAL_PLAYLIST') {
|
if (e.message === 'EDITORIAL_PLAYLIST') {
|
||||||
@@ -232,6 +226,26 @@ async function _loadSpotifyTrackArtwork(tracks, listEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _startRateLimitCountdown(inner, totalSecs) {
|
||||||
|
const endMs = Date.now() + totalSecs * 1000;
|
||||||
|
const render = (remaining) => {
|
||||||
|
const s = Math.max(0, Math.ceil(remaining / 1000));
|
||||||
|
const label = s >= 60 ? `${Math.ceil(s / 60)} Min.` : `${s}s`;
|
||||||
|
inner.innerHTML = `<div style="color:var(--text);padding:16px;background:var(--surface);border-radius:10px;border-left:3px solid var(--warning,#f59e0b)">
|
||||||
|
<div style="font-weight:600;margin-bottom:6px">⏳ Spotify Rate Limit</div>
|
||||||
|
<div style="font-size:13px;line-height:1.5;color:var(--text2)">
|
||||||
|
Bitte warte noch <strong id="sp-rl-countdown">${label}</strong> — nicht erneut versuchen, das setzt den Timer zurück.
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
render(totalSecs * 1000);
|
||||||
|
const iv = setInterval(() => {
|
||||||
|
const remaining = endMs - Date.now();
|
||||||
|
if (remaining <= 0) { clearInterval(iv); render(0); return; }
|
||||||
|
render(remaining);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
function _setArt(el, url) {
|
function _setArt(el, url) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
|
|||||||
Reference in new issue
Block a user