diff --git a/scripts/patch-listening-endpoint-runtime-guards.mjs b/scripts/patch-listening-endpoint-runtime-guards.mjs index 6789a832..0f52c39b 100644 --- a/scripts/patch-listening-endpoint-runtime-guards.mjs +++ b/scripts/patch-listening-endpoint-runtime-guards.mjs @@ -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) {