← back to Costa Rica

scripts/cr-localize-images.js

68 lines

'use strict';
// costa-rica: localize region + place images. Wikimedia hotlinks 429 on
// burst-loads (grids fire ~50 at once), so download each once (~900px thumb)
// into public/img/{regions,places}/ and repoint the DB, preserving the
// original in image_remote_url. Idempotent: skips rows already local.
// Run ON Kamatera from /root/Projects/costa-rica with DATABASE_URL exported.
const fs = require('fs');
const path = require('path');
const { Pool } = require('pg');

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const UA = 'CR-Directory/1.0 (https://costarica.agentabrams.com; info@agentabrams.com) image-localizer';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

function thumbUrl(remote) {
  const clean = remote.split('?')[0];
  if (clean.includes('upload.wikimedia.org')) {
    // /commons/thumb/<h>/<hh>/<File>/<Npx-File> → real filename is 2nd-to-last
    const segs = clean.split('/');
    const file = decodeURIComponent(clean.includes('/thumb/') ? segs[segs.length - 2] : segs[segs.length - 1]);
    return `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(file)}?width=900`;
  }
  return remote; // non-wikimedia: fetch as-is
}

async function fetchWithRetry(url) {
  for (let a = 1; a <= 4; a++) {
    const res = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow' });
    if (res.status === 429) { console.log(`  429 — backoff 20s (${a})`); await sleep(20000); continue; }
    return res;
  }
  return null;
}

async function localize(table, dir) {
  await pool.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS image_remote_url TEXT`);
  const { rows } = await pool.query(
    `SELECT id, slug, image_url FROM ${table} WHERE image_url LIKE 'http%' ORDER BY slug`);
  console.log(`${table}: ${rows.length} remote images`);
  const out = path.join(__dirname, 'public', 'img', dir);
  fs.mkdirSync(out, { recursive: true });
  let ok = 0, fail = 0;
  for (const r of rows) {
    try {
      const res = await fetchWithRetry(thumbUrl(r.image_url));
      if (!res || !res.ok) { console.log(`FAIL ${r.slug}: HTTP ${res && res.status}`); fail++; continue; }
      const ct = res.headers.get('content-type') || '';
      const ext = ct.includes('png') ? 'png' : 'jpg';
      const buf = Buffer.from(await res.arrayBuffer());
      if (buf.length < 4096) { console.log(`FAIL ${r.slug}: tiny (${buf.length}B)`); fail++; continue; }
      fs.writeFileSync(path.join(out, `${r.slug}.${ext}`), buf);
      await pool.query(
        `UPDATE ${table} SET image_remote_url = image_url, image_url = $1 WHERE id = $2`,
        [`/img/${dir}/${r.slug}.${ext}`, r.id]);
      ok++;
      console.log(`ok ${r.slug} (${Math.round(buf.length / 1024)}KB)`);
    } catch (e) { console.log(`FAIL ${r.slug}: ${e.message}`); fail++; }
    await sleep(1100);
  }
  console.log(`${table} DONE ok=${ok} fail=${fail}`);
}

(async () => {
  await localize('regions', 'regions');
  await localize('places', 'places');
  await pool.end();
})();