music poller: a 429 on recently-played pauses only the play log for the full Retry-After; now playing keeps polling every 5 s; player 429s honour Retry-After up to 24 h
All checks were successful
Deploy production / deploy (push) Successful in 1m8s
All checks were successful
Deploy production / deploy (push) Successful in 1m8s
This commit is contained in:
@@ -23,10 +23,20 @@ import { getLiveFromDb } from './stats';
|
||||
// off to 30 s when paused or idle, and honouring 429 Retry-After. Changes (new track,
|
||||
// play/pause, seek) are pushed to browsers via ./live. Recently played (the play log) runs
|
||||
// every minute, plus ~10 s after a track change so a finished track lands quickly.
|
||||
// Spotify rate-limits endpoints separately (recently-played can be blocked for hours while
|
||||
// currently-playing still answers), so a 429 on the play log only pauses the play log, for
|
||||
// the full Retry-After, and never slows the now-playing card down.
|
||||
const LEASE_SECONDS = 20; // renewed every tick; another container takes over 20 s after we stop
|
||||
const RECENT_MS = 60_000;
|
||||
const CATCH_UP_MS = 10_000;
|
||||
const HOLDER = `app-${crypto.randomUUID().slice(0, 8)}`;
|
||||
const MAX_RETRY_S = 24 * 3600;
|
||||
const retryMs = (e: SpotifyError, fallback: number) =>
|
||||
Math.min(MAX_RETRY_S, Math.max(5, e.retryAfter ?? fallback)) * 1000;
|
||||
const rateLimited = (e: unknown): e is SpotifyError =>
|
||||
e instanceof SpotifyError && e.status === 429;
|
||||
const until = (ms: number) =>
|
||||
new Date(Date.now() + ms).toISOString().slice(0, 16).replace('T', ' ') + ' UTC';
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let stopped = false;
|
||||
@@ -35,6 +45,8 @@ let last: PlayerSample | null = null;
|
||||
let idleTicks = 0;
|
||||
let nextRecentAt = 0;
|
||||
let catchUps = 0;
|
||||
let recentBlockedUntil = 0;
|
||||
let recentError: string | null = null;
|
||||
let cache = newCache();
|
||||
|
||||
export const spotifyCredentials = () => ({
|
||||
@@ -86,22 +98,34 @@ export async function tick(): Promise<number> {
|
||||
}
|
||||
last = sample;
|
||||
idleTicks = sample.isPlaying ? 0 : idleTicks + 1;
|
||||
if (Date.now() >= nextRecentAt) {
|
||||
const added = await pollRecent(client, access, cache);
|
||||
// Spotify can take a little while to log a finished track: retry twice, 20 s apart.
|
||||
if (!added && catchUps > 0) {
|
||||
catchUps--;
|
||||
nextRecentAt = Date.now() + 20_000;
|
||||
} else {
|
||||
if (Date.now() >= nextRecentAt && Date.now() >= recentBlockedUntil) {
|
||||
try {
|
||||
const added = await pollRecent(client, access, cache);
|
||||
// Spotify can take a little while to log a finished track: retry twice, 20 s apart.
|
||||
if (!added && catchUps > 0) {
|
||||
catchUps--;
|
||||
nextRecentAt = Date.now() + 20_000;
|
||||
} else {
|
||||
catchUps = 0;
|
||||
nextRecentAt = Date.now() + RECENT_MS;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!rateLimited(e)) throw e;
|
||||
const wait = retryMs(e, 60);
|
||||
recentBlockedUntil = Date.now() + wait;
|
||||
catchUps = 0;
|
||||
nextRecentAt = Date.now() + RECENT_MS;
|
||||
recentError = `${e.message} (play log paused until ${until(wait)})`;
|
||||
}
|
||||
}
|
||||
if (Date.now() < recentBlockedUntil) error = recentError;
|
||||
return nextDelay(sample.isPlaying, idleTicks);
|
||||
} catch (e) {
|
||||
error = await handlePollError(client, e).catch(() => String(e));
|
||||
if (e instanceof SpotifyError && e.status === 429)
|
||||
return Math.min(600, Math.max(5, e.retryAfter ?? 30)) * 1000;
|
||||
if (rateLimited(e)) {
|
||||
const wait = retryMs(e, 30);
|
||||
error = `${error} (paused until ${until(wait)})`;
|
||||
return wait;
|
||||
}
|
||||
return 30_000;
|
||||
} finally {
|
||||
log(error ?? '');
|
||||
|
||||
Reference in New Issue
Block a user