import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"; const entries = JSON.parse(await readFile(".scratch/images.json", "utf8")); // Map every tildacdn URL (including resize derivatives) to its original full-size URL. const canonical = new Map(); for (const { url } of entries) { let orig = url.replace("thb.tildacdn.com", "static.tildacdn.com"); orig = orig.replace(/\/-\/resize[^/]*\//, "/"); orig = orig.replace(/\/20x\//, "/"); canonical.set(url, orig); } const unique = [...new Set(canonical.values())]; console.log(`${entries.length} refs -> ${unique.length} original images`); await mkdir("public/images", { recursive: true }); const usedNames = new Set(); const map = {}; let ok = 0, fail = 0; for (const url of unique) { if (url.includes("/lib/icons/")) continue; // stock bullet icons, replaced by our own let name = decodeURIComponent(path.basename(new URL(url).pathname)); if (usedNames.has(name)) { const ext = path.extname(name); name = name.slice(0, -ext.length) + "-" + Math.abs(hash(url)).toString(36) + ext; } usedNames.add(name); try { const res = await fetch(url, { headers: { "User-Agent": UA } }); if (!res.ok) throw new Error("HTTP " + res.status); const buf = Buffer.from(await res.arrayBuffer()); const min = url.endsWith(".svg") ? 50 : 1000; if (buf.length < min) throw new Error("too small"); await writeFile(path.join("public/images", name), buf); map[url] = "/images/" + name; ok++; } catch (e) { fail++; console.log(`FAIL ${url}: ${e.message}`); } } // hash helper function hash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; return h; } await writeFile(".scratch/image-map.json", JSON.stringify(map, null, 2)); console.log(`downloaded ${ok}, failed ${fail}`);