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,
|
// 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
|
// 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.
|
// 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 LEASE_SECONDS = 20; // renewed every tick; another container takes over 20 s after we stop
|
||||||
const RECENT_MS = 60_000;
|
const RECENT_MS = 60_000;
|
||||||
const CATCH_UP_MS = 10_000;
|
const CATCH_UP_MS = 10_000;
|
||||||
const HOLDER = `app-${crypto.randomUUID().slice(0, 8)}`;
|
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 timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
@@ -35,6 +45,8 @@ let last: PlayerSample | null = null;
|
|||||||
let idleTicks = 0;
|
let idleTicks = 0;
|
||||||
let nextRecentAt = 0;
|
let nextRecentAt = 0;
|
||||||
let catchUps = 0;
|
let catchUps = 0;
|
||||||
|
let recentBlockedUntil = 0;
|
||||||
|
let recentError: string | null = null;
|
||||||
let cache = newCache();
|
let cache = newCache();
|
||||||
|
|
||||||
export const spotifyCredentials = () => ({
|
export const spotifyCredentials = () => ({
|
||||||
@@ -86,7 +98,8 @@ export async function tick(): Promise<number> {
|
|||||||
}
|
}
|
||||||
last = sample;
|
last = sample;
|
||||||
idleTicks = sample.isPlaying ? 0 : idleTicks + 1;
|
idleTicks = sample.isPlaying ? 0 : idleTicks + 1;
|
||||||
if (Date.now() >= nextRecentAt) {
|
if (Date.now() >= nextRecentAt && Date.now() >= recentBlockedUntil) {
|
||||||
|
try {
|
||||||
const added = await pollRecent(client, access, cache);
|
const added = await pollRecent(client, access, cache);
|
||||||
// Spotify can take a little while to log a finished track: retry twice, 20 s apart.
|
// Spotify can take a little while to log a finished track: retry twice, 20 s apart.
|
||||||
if (!added && catchUps > 0) {
|
if (!added && catchUps > 0) {
|
||||||
@@ -96,12 +109,23 @@ export async function tick(): Promise<number> {
|
|||||||
catchUps = 0;
|
catchUps = 0;
|
||||||
nextRecentAt = Date.now() + RECENT_MS;
|
nextRecentAt = Date.now() + RECENT_MS;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!rateLimited(e)) throw e;
|
||||||
|
const wait = retryMs(e, 60);
|
||||||
|
recentBlockedUntil = Date.now() + wait;
|
||||||
|
catchUps = 0;
|
||||||
|
recentError = `${e.message} (play log paused until ${until(wait)})`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (Date.now() < recentBlockedUntil) error = recentError;
|
||||||
return nextDelay(sample.isPlaying, idleTicks);
|
return nextDelay(sample.isPlaying, idleTicks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = await handlePollError(client, e).catch(() => String(e));
|
error = await handlePollError(client, e).catch(() => String(e));
|
||||||
if (e instanceof SpotifyError && e.status === 429)
|
if (rateLimited(e)) {
|
||||||
return Math.min(600, Math.max(5, e.retryAfter ?? 30)) * 1000;
|
const wait = retryMs(e, 30);
|
||||||
|
error = `${error} (paused until ${until(wait)})`;
|
||||||
|
return wait;
|
||||||
|
}
|
||||||
return 30_000;
|
return 30_000;
|
||||||
} finally {
|
} finally {
|
||||||
log(error ?? '');
|
log(error ?? '');
|
||||||
|
|||||||
Reference in New Issue
Block a user