fix: proxy Funkwhale cover images server-side to fix presigned URL expiry
Deploy Indiekit Server / deploy (push) Successful in 1m23s
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:
@@ -691,6 +691,93 @@ const patchSpecs = [
|
|||||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-lastfm/lib/stats.js",
|
"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) {
|
async function exists(filePath) {
|
||||||
|
|||||||
Reference in New Issue
Block a user