tools

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

main.ts (3655B)


      1 import { DOMParser } from "linkedom";
      2 
      3 const port = Number(Deno.env.get("PORT") || "8000");
      4 const hostname = Deno.env.get("HOST") || "127.0.0.1";
      5 
      6 const SLUG_RE = /^[a-z0-9-]+$/i;
      7 
      8 function escapeHtml(s: string): string {
      9 	return s
     10 		.replace(/&/g, "&")
     11 		.replace(/</g, "&lt;")
     12 		.replace(/>/g, "&gt;")
     13 		.replace(/"/g, "&quot;");
     14 }
     15 
     16 type FeedItem = {
     17 	id: string;
     18 	url: string;
     19 	title: string;
     20 	content_html: string;
     21 	image?: string;
     22 };
     23 
     24 async function fetchDocument(url: string, label: string) {
     25 	const res = await fetch(url);
     26 	if (!res.ok) {
     27 		return {
     28 			error: Response.json(
     29 				{ error: `Could not fetch data for ${label}` },
     30 				{ status: res.status },
     31 			),
     32 		};
     33 	}
     34 	const html = await res.text();
     35 	return { document: new DOMParser().parseFromString(html, "text/html") };
     36 }
     37 
     38 function feedResponse(title: string, homePageUrl: string, items: FeedItem[]) {
     39 	return Response.json({
     40 		version: "https://jsonfeed.org/version/1.1",
     41 		title,
     42 		home_page_url: homePageUrl,
     43 		items,
     44 	});
     45 }
     46 
     47 Deno.serve({ port, hostname }, async (req: Request) => {
     48 	const url = new URL(req.url);
     49 	const artist = url.searchParams.get("artist");
     50 	const fan = url.searchParams.get("fan");
     51 
     52 	if (artist) {
     53 		if (!SLUG_RE.test(artist)) {
     54 			return Response.json({ error: "invalid artist" }, { status: 400 });
     55 		}
     56 		return handleArtist(artist);
     57 	} else if (fan) {
     58 		if (!SLUG_RE.test(fan)) {
     59 			return Response.json({ error: "invalid fan" }, { status: 400 });
     60 		}
     61 		return handleFan(fan);
     62 	} else {
     63 		return Response.json(
     64 			{ error: "Our princess is in another castle!" },
     65 			{ status: 400 },
     66 		);
     67 	}
     68 });
     69 
     70 async function handleArtist(artist: string) {
     71 	const baseUrl = `https://${artist}.bandcamp.com`;
     72 	const result = await fetchDocument(`${baseUrl}/music`, artist);
     73 	if (result.error) return result.error;
     74 	const { document } = result;
     75 
     76 	// deno-lint-ignore no-explicit-any
     77 	const items = Array.from(document.querySelectorAll("#music-grid li")).map((item: any) => {
     78 		const titleEl = item.querySelector(".title");
     79 		const title = titleEl
     80 			? titleEl.textContent.replace(/(\r\n|\n|\r)/gm, "").trim()
     81 			: "Unknown Title";
     82 
     83 		const href = item.querySelector("a")?.getAttribute("href") ?? "";
     84 		const url = href
     85 			? href.startsWith("http") ? href : `${baseUrl}${href}`
     86 			: "";
     87 
     88 		return { id: url, url, title, content_html: title } satisfies FeedItem;
     89 	});
     90 
     91 	return feedResponse(artist, baseUrl, items);
     92 }
     93 
     94 async function handleFan(fan: string) {
     95 	const homePageUrl = `https://bandcamp.com/${fan}`;
     96 	const result = await fetchDocument(homePageUrl, fan);
     97 	if (result.error) return result.error;
     98 	const { document } = result;
     99 
    100 	const items = Array.from(
    101 		document.querySelectorAll("#collection-grid .collection-item-container"),
    102 		// deno-lint-ignore no-explicit-any
    103 	).map((item: any) => {
    104 		const titleEl = item.querySelector(".collection-item-title");
    105 		const title = titleEl
    106 			? titleEl.textContent.replace(/(\r\n|\n|\r)/gm, "").replace("(gift given)", "").trim()
    107 			: "Unknown Title";
    108 
    109 		const artistEl = item.querySelector(".collection-item-artist");
    110 		const artistName = artistEl
    111 			? artistEl.textContent.replace(/(\r\n|\n|\r)/gm, "").trim().replace(/^by\s+/, "")
    112 			: "Unknown Artist";
    113 
    114 		const href = item.querySelector(".item-link")?.getAttribute("href") ?? "";
    115 		const imgSrc = item.querySelector(".collection-item-art")?.getAttribute("src") ?? "";
    116 
    117 		return {
    118 			id: href,
    119 			url: href,
    120 			title: `${escapeHtml(title)} by ${escapeHtml(artistName)}`,
    121 			content_html: `<img src="${escapeHtml(imgSrc)}" /><br/>${escapeHtml(title)} by ${escapeHtml(artistName)}`,
    122 			image: imgSrc,
    123 		} satisfies FeedItem;
    124 	});
    125 
    126 	return feedResponse(fan, homePageUrl, items);
    127 }