fix: proxy Funkwhale cover images server-side to fix presigned URL expiry
Deploy Indiekit Server / deploy (push) Successful in 1m23s

Presigned Wasabi S3 cover URLs expire after ~1h. Since Funkwhale/Last.fm
data is now fetched client-side (Alpine.js), the old build-time image cache
no longer runs. Fix: getCoverUrl() now returns /funkwhale/api/cover-proxy?url=...
and a new public route proxies the image server-side on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sven
2026-04-15 10:55:09 +02:00
parent da74eb8625
commit f758fdcdf1
@@ -691,6 +691,93 @@ const patchSpecs = [
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-lastfm/lib/stats.js",
],
},
{
name: "funkwhale-cover-url-proxy",
marker: "proxy wrapper: routes presigned S3 URLs through server-side cover proxy",
oldSnippet: ` // Check track cover
if (track.cover?.urls) {
return track.cover.urls[sizeKey] || track.cover.urls.original || null;
}
// Check album cover
if (track.album?.cover?.urls) {
return track.album.cover.urls[sizeKey] || track.album.cover.urls.original || null;
}
// Check artist cover
const artist = track.artist_credit?.[0]?.artist;
if (artist?.cover?.urls) {
return artist.cover.urls[sizeKey] || artist.cover.urls.original || null;
}
return null;`,
newSnippet: ` // proxy wrapper: routes presigned S3 URLs through server-side cover proxy
const proxyCover = (url) =>
url ? \`/funkwhale/api/cover-proxy?url=\${encodeURIComponent(url)}\` : null;
// Check track cover
if (track.cover?.urls) {
return proxyCover(track.cover.urls[sizeKey] || track.cover.urls.original || null);
}
// Check album cover
if (track.album?.cover?.urls) {
return proxyCover(track.album.cover.urls[sizeKey] || track.album.cover.urls.original || null);
}
// Check artist cover
const artist = track.artist_credit?.[0]?.artist;
if (artist?.cover?.urls) {
return proxyCover(artist.cover.urls[sizeKey] || artist.cover.urls.original || null);
}
return null;`,
candidates: [
"node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/utils.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/lib/utils.js",
],
},
{
name: "funkwhale-cover-proxy-route",
marker: "cover-proxy: routes presigned S3 URLs through server-side proxy",
oldSnippet: ` publicRouter.get("/api/stats/trends", statsController.apiTrends);
return publicRouter;`,
newSnippet: ` publicRouter.get("/api/stats/trends", statsController.apiTrends);
// cover-proxy: routes presigned S3 URLs through server-side proxy
// Presigned Wasabi URLs expire in ~1h; this endpoint fetches them server-side
// so the browser receives stable-ish URLs (fresh presigned URL per API call).
publicRouter.get("/api/cover-proxy", async (request, response) => {
const rawUrl = request.query.url;
if (!rawUrl) return response.status(400).send("Missing url parameter");
let urlObj;
try { urlObj = new URL(rawUrl); } catch { return response.status(400).send("Invalid url"); }
try {
const upstream = await fetch(rawUrl, {
signal: AbortSignal.timeout(10_000),
headers: { "User-Agent": "indiekit/1.0 (cover-proxy)" },
});
if (!upstream.ok) return response.status(upstream.status).send("Upstream error");
const contentType = upstream.headers.get("content-type") || "image/jpeg";
const buffer = Buffer.from(await upstream.arrayBuffer());
response.set({
"Content-Type": contentType,
"Cache-Control": "public, max-age=86400, stale-while-revalidate=604800",
"X-Cover-Path": urlObj.pathname,
});
return response.send(buffer);
} catch {
return response.status(502).send("Proxy error");
}
});
return publicRouter;`,
candidates: [
"node_modules/@rmdes/indiekit-endpoint-funkwhale/index.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-funkwhale/index.js",
],
},
];
async function exists(filePath) {