tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

commit fe0232cc55496ca5dff1adf147f7725c81f507fa
parent b7055a61a1b2014113baf22a4a263d3d72331183
Author: mtmn <miro@haravara.org>
Date:   Fri,  8 May 2026 11:32:35 +0200

bandeno: add shared blocks

Diffstat:
Dbandeno/deploy | 2--
Mbandeno/main.ts | 186+++++++++++++++++++++++++++++++++++++------------------------------------------
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, "&amp;") + .replace(/</g, "&lt;") + .replace(/>/g, "&gt;") + .replace(/"/g, "&quot;"); +} + +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); }