← back to Animals
auto-save: 2026-07-22T20:16:30 (1 files) — scripts/localize_breed_heroes.cjs
3fc5e1ec1163fdba0bdec91db2b1921eb15585b8 · 2026-07-22 20:16:35 -0700 · Steve Abrams
Files touched
A scripts/localize_breed_heroes.cjs
Diff
commit 3fc5e1ec1163fdba0bdec91db2b1921eb15585b8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 22 20:16:35 2026 -0700
auto-save: 2026-07-22T20:16:30 (1 files) — scripts/localize_breed_heroes.cjs
---
scripts/localize_breed_heroes.cjs | 79 +++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/scripts/localize_breed_heroes.cjs b/scripts/localize_breed_heroes.cjs
new file mode 100644
index 0000000..a6ccf62
--- /dev/null
+++ b/scripts/localize_breed_heroes.cjs
@@ -0,0 +1,79 @@
+'use strict';
+// Localize breed hero images: the /breeds page hotlinked 140 full-res
+// Wikimedia originals, and Wikimedia 429s burst-loads — most cards rendered
+// broken. Download each hero ONCE as a ~900px thumb via Special:FilePath,
+// store under public/img/breeds/, keep the original URL in
+// breeds.hero_image_remote_url (attribution/credit column untouched).
+//
+// Idempotent: skips breeds whose hero_image_url is already local.
+// Polite: 1.1s between requests, UA identifies us, 429 → 20s backoff x3.
+
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+
+const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgresql:///animals_directory?host=/tmp' });
+const OUT_DIR = path.join(__dirname, '..', 'public', 'img', 'breeds');
+const UA = 'AnimalsDirect/1.0 (https://animals.agentabrams.com; info@agentabrams.com) breed-hero-localizer';
+const WIDTH = 900;
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+function commonsFilename(remoteUrl) {
+ // https://upload.wikimedia.org/wikipedia/commons/2/29/2_Beagles....jpg?utm_...
+ const clean = remoteUrl.split('?')[0];
+ const seg = clean.split('/').pop();
+ return decodeURIComponent(seg);
+}
+
+async function fetchWithRetry(url) {
+ for (let attempt = 1; attempt <= 4; attempt++) {
+ const res = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow' });
+ if (res.status === 429) {
+ console.log(` 429 — backing off 20s (attempt ${attempt})`);
+ await sleep(20_000);
+ continue;
+ }
+ return res;
+ }
+ return null;
+}
+
+(async () => {
+ fs.mkdirSync(OUT_DIR, { recursive: true });
+ const { rows } = await pool.query(`
+ SELECT id, slug, hero_image_url FROM breeds
+ WHERE hero_image_url LIKE 'http%' ORDER BY slug`);
+ console.log(`${rows.length} breeds with remote heroes`);
+
+ await pool.query(`ALTER TABLE breeds ADD COLUMN IF NOT EXISTS hero_image_remote_url TEXT`);
+
+ let ok = 0, fail = 0;
+ for (const b of rows) {
+ const file = commonsFilename(b.hero_image_url);
+ const thumbUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(file)}?width=${WIDTH}`;
+ try {
+ const res = await fetchWithRetry(thumbUrl);
+ if (!res || !res.ok) { console.log(`FAIL ${b.slug}: HTTP ${res && res.status}`); fail++; continue; }
+ const ct = res.headers.get('content-type') || '';
+ const ext = ct.includes('png') ? 'png' : ct.includes('gif') ? 'gif' : 'jpg';
+ const buf = Buffer.from(await res.arrayBuffer());
+ if (buf.length < 4096) { console.log(`FAIL ${b.slug}: tiny body (${buf.length}B)`); fail++; continue; }
+ const localFile = `${b.slug}.${ext}`;
+ fs.writeFileSync(path.join(OUT_DIR, localFile), buf);
+ await pool.query(
+ `UPDATE breeds SET hero_image_remote_url = hero_image_url,
+ hero_image_url = $1
+ WHERE id = $2`,
+ [`/static/img/breeds/${localFile}`, b.id]);
+ ok++;
+ console.log(`ok ${b.slug} (${Math.round(buf.length / 1024)}KB)`);
+ } catch (e) {
+ console.log(`FAIL ${b.slug}: ${e.message}`);
+ fail++;
+ }
+ await sleep(1_100);
+ }
+ console.log(`DONE ok=${ok} fail=${fail}`);
+ await pool.end();
+})();
← a4bf2fe animals: basic-auth creds admin/DW2024!; deployed on mac3:97
·
back to Animals
·
Fix flag panel popping open on every lightbox view (CSS disp 1ed0c8d →