commit fe0232cc55496ca5dff1adf147f7725c81f507fa
parent b7055a61a1b2014113baf22a4a263d3d72331183
Author: mtmn <miro@haravara.org>
Date: Fri, 8 May 2026 11:32:35 +0200
bandeno: add shared blocks
Diffstat:
2 files changed, 87 insertions(+), 101 deletions(-)
diff --git a/bandeno/deploy b/bandeno/deploy
@@ -1,2 +0,0 @@
-#!/bin/bash
-podman build -t bandeno . && podman save bandeno | ssh clam "sudo podman load && sudo systemctl restart bandeno"
diff --git a/bandeno/main.ts b/bandeno/main.ts
@@ -3,137 +3,125 @@ import { DOMParser } from "linkedom";
const port = Number(Deno.env.get("PORT") || "8000");
const hostname = Deno.env.get("HOST") || "127.0.0.1";
+const SLUG_RE = /^[a-z0-9-]+$/i;
+
+function escapeHtml(s: string): string {
+ return s
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """);
+}
+
+type FeedItem = {
+ id: string;
+ url: string;
+ title: string;
+ content_html: string;
+ image?: string;
+};
+
+async function fetchDocument(url: string, label: string) {
+ const res = await fetch(url);
+ if (!res.ok) {
+ return {
+ error: Response.json(
+ { error: `Could not fetch data for ${label}` },
+ { status: res.status },
+ ),
+ };
+ }
+ const html = await res.text();
+ return { document: new DOMParser().parseFromString(html, "text/html") };
+}
+
+function feedResponse(title: string, homePageUrl: string, items: FeedItem[]) {
+ return Response.json({
+ version: "https://jsonfeed.org/version/1.1",
+ title,
+ home_page_url: homePageUrl,
+ items,
+ });
+}
+
Deno.serve({ port, hostname }, async (req: Request) => {
const url = new URL(req.url);
const artist = url.searchParams.get("artist");
const fan = url.searchParams.get("fan");
if (artist) {
+ if (!SLUG_RE.test(artist)) {
+ return Response.json({ error: "invalid artist" }, { status: 400 });
+ }
return handleArtist(artist);
} else if (fan) {
+ if (!SLUG_RE.test(fan)) {
+ return Response.json({ error: "invalid fan" }, { status: 400 });
+ }
return handleFan(fan);
} else {
return Response.json(
- {
- error: "Our princess is in another castle!",
- },
- {
- status: 400,
- },
+ { error: "Our princess is in another castle!" },
+ { status: 400 },
);
}
});
async function handleArtist(artist: string) {
- const res = await fetch(`https://${artist}.bandcamp.com/music`);
-
- if (!res.ok) {
- return Response.json(
- {
- error: `Could not fetch data for ${artist}`,
- },
- {
- status: res.status,
- },
- );
- }
-
- const html = await res.text();
- const document = new DOMParser().parseFromString(html, "text/html");
- const items = Array.from(document.querySelectorAll("#music-grid li")).map(
- // deno-lint-ignore no-explicit-any
- (item: any) => {
- const titleElement = item.querySelector(".title");
- const title = titleElement
- ? titleElement.textContent.replace(/(\r\n|\n|\r)/gm, "").trim()
- : "Unknown Title";
-
- const linkElement = item.querySelector("a");
- const href = linkElement ? linkElement.getAttribute("href") : "";
- const url = href
- ? href.startsWith("http")
- ? href
- : `https://${artist}.bandcamp.com${href}`
- : "";
-
- return {
- id: url,
- url,
- title,
- content_html: title,
- };
- },
- );
-
- return Response.json({
- version: "https://jsonfeed.org/version/1.1",
- title: `${artist}`,
- home_page_url: `https://${artist}.bandcamp.com`,
- items,
+ const baseUrl = `https://${artist}.bandcamp.com`;
+ const result = await fetchDocument(`${baseUrl}/music`, artist);
+ if (result.error) return result.error;
+ const { document } = result;
+
+ // deno-lint-ignore no-explicit-any
+ const items = Array.from(document.querySelectorAll("#music-grid li")).map((item: any) => {
+ const titleEl = item.querySelector(".title");
+ const title = titleEl
+ ? titleEl.textContent.replace(/(\r\n|\n|\r)/gm, "").trim()
+ : "Unknown Title";
+
+ const href = item.querySelector("a")?.getAttribute("href") ?? "";
+ const url = href
+ ? href.startsWith("http") ? href : `${baseUrl}${href}`
+ : "";
+
+ return { id: url, url, title, content_html: title } satisfies FeedItem;
});
+
+ return feedResponse(artist, baseUrl, items);
}
async function handleFan(fan: string) {
- const targetUrl = `https://bandcamp.com/${fan}`;
- const res = await fetch(targetUrl);
-
- if (!res.ok) {
- return Response.json(
- {
- error: `Could not fetch data for ${fan}`,
- },
- {
- status: res.status,
- },
- );
- }
-
- const html = await res.text();
- const document = new DOMParser().parseFromString(html, "text/html");
+ const homePageUrl = `https://bandcamp.com/${fan}`;
+ const result = await fetchDocument(homePageUrl, fan);
+ if (result.error) return result.error;
+ const { document } = result;
const items = Array.from(
document.querySelectorAll("#collection-grid .collection-item-container"),
// deno-lint-ignore no-explicit-any
).map((item: any) => {
- const titleElement = item.querySelector(".collection-item-title");
- let title = "Unknown Title";
- if (titleElement) {
- title = titleElement.textContent
- .replace(/(\r\n|\n|\r)/gm, "")
- .replace("(gift given)", "")
- .trim();
- }
-
- const artistElement = item.querySelector(".collection-item-artist");
- const artistName = artistElement
- ? artistElement.textContent
- .replace(/(\r\n|\n|\r)/gm, "")
- .trim()
- .replace(/^by\s+/, "")
+ const titleEl = item.querySelector(".collection-item-title");
+ const title = titleEl
+ ? titleEl.textContent.replace(/(\r\n|\n|\r)/gm, "").replace("(gift given)", "").trim()
+ : "Unknown Title";
+
+ const artistEl = item.querySelector(".collection-item-artist");
+ const artistName = artistEl
+ ? artistEl.textContent.replace(/(\r\n|\n|\r)/gm, "").trim().replace(/^by\s+/, "")
: "Unknown Artist";
- const linkElement = item.querySelector(".item-link");
- const href = linkElement ? linkElement.getAttribute("href") : "";
-
- const imgElement = item.querySelector(".collection-item-art");
- const imgSrc = imgElement ? imgElement.getAttribute("src") : "";
-
- const fullTitle = `${title} by ${artistName}`;
- const description = `<img src="${imgSrc}" /><br/>${title} by ${artistName}`;
+ const href = item.querySelector(".item-link")?.getAttribute("href") ?? "";
+ const imgSrc = item.querySelector(".collection-item-art")?.getAttribute("src") ?? "";
return {
id: href,
url: href,
- title: fullTitle,
- content_html: description,
+ title: `${escapeHtml(title)} by ${escapeHtml(artistName)}`,
+ content_html: `<img src="${escapeHtml(imgSrc)}" /><br/>${escapeHtml(title)} by ${escapeHtml(artistName)}`,
image: imgSrc,
- };
+ } satisfies FeedItem;
});
- return Response.json({
- version: "https://jsonfeed.org/version/1.1",
- title: `${fan}`,
- home_page_url: targetUrl,
- items,
- });
+ return feedResponse(fan, homePageUrl, items);
}