← back to Costa Rica

scripts/ingest/region-images.js

98 lines

'use strict';

// Fetches a hero image for every region from Wikipedia's REST summary API.
// Returns {originalimage, thumbnail} + an attributable page URL. Public/CC
// licensed via Wikimedia Commons by definition. We never inline-host images
// from the Wikimedia upload origin in production (browser hot-links the
// commons URL directly, which is permitted under their TOU when paired
// with attribution + source link).

const { fetchJson, sleep, pool } = require('./_lib');

const TITLE_FALLBACKS = {
  'san-jose':       ['San_José,_Costa_Rica', 'San_José_(canton)', 'San_José_Province'],
  'la-fortuna':     ['La_Fortuna,_San_Carlos', 'Arenal_Volcano'],
  'manuel-antonio': ['Manuel_Antonio_National_Park', 'Manuel_Antonio_(canton)'],
  'monteverde':     ['Monteverde', 'Monteverde_Cloud_Forest_Reserve'],
  'santa-elena':    ['Santa_Elena,_Puntarenas', 'Monteverde'],
  'puerto-viejo':   ['Puerto_Viejo_de_Talamanca', 'Puerto_Viejo,_Limón'],
  'cahuita':        ['Cahuita_National_Park', 'Cahuita'],
  'tamarindo':      ['Tamarindo,_Costa_Rica'],
  'nosara':         ['Nosara'],
  'santa-teresa':   ['Santa_Teresa,_Costa_Rica'],
  'montezuma':      ['Montezuma,_Costa_Rica'],
  'jaco':           ['Jacó,_Costa_Rica'],
  'quepos':         ['Quepos'],
  'uvita':          ['Uvita'],
  'dominical':      ['Dominical,_Costa_Rica'],
  'limon':          ['Limón'],
  'puntarenas':     ['Puntarenas'],
  'papagayo':       ['Papagayo,_Costa_Rica', 'Gulf_of_Papagayo'],
  'playas-coco':    ['Playas_del_Coco'],
  'flamingo':       ['Playa_Flamingo'],
  'samara':         ['Sámara,_Costa_Rica'],
  'nicoya':         ['Nicoya', 'Nicoya_Peninsula'],
  'liberia':        ['Liberia,_Costa_Rica'],
  'heredia':        ['Heredia,_Costa_Rica'],
  'cartago':        ['Cartago,_Costa_Rica'],
  'alajuela':       ['Alajuela'],
  'atenas':         ['Atenas,_Costa_Rica'],
  'grecia':         ['Grecia,_Costa_Rica'],
  'escazu':         ['Escazú_(canton)'],
  'santa-ana':      ['Santa_Ana_(canton)'],
};

// Generic fallback: try "<Name>,_Costa_Rica" then "<Name>" alone.
function candidateTitles(slug, name) {
  if (TITLE_FALLBACKS[slug]) return TITLE_FALLBACKS[slug];
  const safeName = name.replace(/\s+/g, '_');
  return [`${safeName},_Costa_Rica`, safeName];
}

async function fetchWikiSummary(title) {
  const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
  try {
    const j = await fetchJson(url, { headers: { 'Api-User-Agent': 'CR-Directory/0.1 (info@agentabrams.com)' } });
    if (j.type === 'disambiguation') return null;
    return j;
  } catch (e) {
    if (/HTTP 404/.test(e.message)) return null;
    throw e;
  }
}

(async () => {
  const { rows: regions } = await pool.query(
    `SELECT id, slug, name, image_url FROM regions WHERE region_type IN ('city','town','canton') ORDER BY id`
  );
  console.log(`[region-images] fetching for ${regions.length} regions`);

  let hits = 0, miss = 0, skipped = 0;
  for (const r of regions) {
    if (r.image_url) { skipped++; continue; }
    const titles = candidateTitles(r.slug, r.name);
    let summary = null, usedTitle = null;
    for (const t of titles) {
      const s = await fetchWikiSummary(t);
      if (s && (s.thumbnail || s.originalimage)) { summary = s; usedTitle = t; break; }
      await sleep(300);
    }
    if (!summary) { miss++; console.log(`[region-images] miss  ${r.slug.padEnd(20)} (${r.name})`); continue; }

    const img = (summary.originalimage || summary.thumbnail).source;
    const sourceUrl = summary.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${encodeURIComponent(usedTitle)}`;
    const credit = `Wikipedia: ${summary.title}`;
    const license = 'See Wikimedia Commons file page';
    await pool.query(
      `UPDATE regions SET image_url=$1, image_credit=$2, image_license=$3, image_source_url=$4, image_fetched_at=NOW() WHERE id=$5`,
      [img, credit, license, sourceUrl, r.id]
    );
    hits++;
    console.log(`[region-images] HIT   ${r.slug.padEnd(20)} → ${usedTitle}`);
    await sleep(400);
  }

  console.log(`[region-images] DONE hits=${hits} miss=${miss} skipped=${skipped}`);
  await pool.end();
})();