All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 43s
The player now holds a queue instead of a single item, so albums play through and single tracks share the same code path. Adds a "stream" item kind for expiring, remotely-hosted sources: Bandcamp signs its mp3 URLs for ~24h, so an item carries the page it came from and re-resolves itself on error or on a stale restored session, falling back to the iframe embed if that fails too. Gated behind GERBEUR_BANDCAMP_PLAYER (default "embed"), injected into a meta tag the same way as site-name/site-emoji. Also: player header gains artwork and a subtitle, volume persists across queue advances and reloads, and cross-origin streams get a plain seek bar rather than a waveform that can never decode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SbtjNsT5wvuABnfqhegZEJ
46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
import { Context, Next, send } from "@oak/oak";
|
|
import { BANDCAMP_PLAYER, OG_SITE_NAME, SITE_EMOJI } from "../config.ts";
|
|
import { emojiToCodepoint } from "./site-icons.ts";
|
|
|
|
const ICON_VERSION = emojiToCodepoint(SITE_EMOJI);
|
|
|
|
async function serveIndexHtml(
|
|
context: Context<Record<string, object>>,
|
|
root: string,
|
|
) {
|
|
const filePath = `${root}/index.html`;
|
|
const raw = await Deno.readTextFile(filePath);
|
|
const html = raw
|
|
.replaceAll("__SITE_NAME__", OG_SITE_NAME)
|
|
.replaceAll("__SITE_EMOJI__", SITE_EMOJI)
|
|
.replaceAll("__ICON_VERSION__", ICON_VERSION)
|
|
.replaceAll("__BANDCAMP_PLAYER__", BANDCAMP_PLAYER);
|
|
context.response.type = "text/html";
|
|
context.response.body = html;
|
|
}
|
|
|
|
export function routeStaticFilesFrom(staticPaths: string[]) {
|
|
return async (context: Context<Record<string, object>>, next: Next) => {
|
|
const pathname = context.request.url.pathname;
|
|
|
|
// Serve index.html with runtime placeholder replacement
|
|
if (pathname === "/" || pathname === "/index.html") {
|
|
await serveIndexHtml(context, staticPaths[0]);
|
|
return;
|
|
}
|
|
|
|
for (const path of staticPaths) {
|
|
try {
|
|
await send(context, pathname, { root: path });
|
|
return;
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// SPA fallback: serve index.html so client-side routes work on direct navigation
|
|
await serveIndexHtml(context, staticPaths[0]);
|
|
await next();
|
|
};
|
|
}
|