perf(web-player): serve YouTube artwork through a caching proxy

The browser used to pull every cover straight from Google, so a search grid
plus infinite scroll fired dozens of cross-origin image requests and started
coming back as 429. Covers now go through /api/youtube/thumbnail, which
resizes the upstream URL, caches the bytes in memory, collapses duplicate
in-flight fetches, caps the concurrency against Google and backs off when it
answers 429. Responses are immutable for a month and revalidate with an ETag,
and serving them same-origin also lets the accent extractor read the pixels.
This commit is contained in:
2026-07-20 17:36:41 +02:00
parent dd3150e3d4
commit 2691c40da8
6 changed files with 262 additions and 3 deletions
+27
View File
@@ -51,3 +51,30 @@ export const apiConfig = {
return (env.API_BASE_URL ?? '').replace(/\/+$/, '');
}
};
export const thumbnailConfig = {
get maxAgeSeconds() {
return positiveInt(env.THUMBNAIL_CACHE_MAX_AGE_SECONDS, 30 * DAY_SECONDS);
},
get failureCacheSeconds() {
return positiveInt(env.THUMBNAIL_FAILURE_CACHE_SECONDS, 300);
},
get memoryCacheBytes() {
return positiveInt(env.THUMBNAIL_CACHE_BYTES, 128 * 1024 * 1024);
},
get maxConcurrentFetches() {
return positiveInt(env.THUMBNAIL_MAX_CONCURRENT_FETCHES, 4);
},
get fetchTimeoutMs() {
return positiveInt(env.THUMBNAIL_FETCH_TIMEOUT_MS, 8000);
},
get cooldownSeconds() {
return positiveInt(env.THUMBNAIL_COOLDOWN_SECONDS, 30);
},
get maxCooldownSeconds() {
return positiveInt(env.THUMBNAIL_MAX_COOLDOWN_SECONDS, 300);
},
get maxSize() {
return positiveInt(env.THUMBNAIL_MAX_SIZE, 720);
}
};