← back to Nineoh Guide

db/ingest-beverlyhills-photos.mjs

91 lines

// Pulls atmospheric photos of BEVERLY HILLS (the city — Rodeo Drive, the sign,
// palm streets), NOT the show. License-verified free-for-reuse only (PD/CC0/CC-BY/
// CC-BY-SA). Stored in assets tagged 'beverly-hills-location' with attribution.
// Run: node db/ingest-beverlyhills-photos.mjs
import pg from "pg";

const DATABASE_URL =
  process.env.DATABASE_URL ?? "postgresql://localhost/nineoh_guide?host=/tmp";
const pool = new pg.Pool({ connectionString: DATABASE_URL });
const UA = "nineoh-guide/0.1 (unofficial fan project; contact: set-at-launch)";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const stripHtml = (s) => (s || "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();

const FREE = /^(cc0|cc-by-\d|cc-by-sa-\d|pd|public domain)/i;
const NONFREE = /(nc|nd|non-free|fair use)/i;

async function fetchJson(url, tries = 4) {
  for (let i = 0; i < tries; i++) {
    try {
      const r = await fetch(url, { headers: { "User-Agent": UA } });
      return JSON.parse(await r.text());
    } catch {
      await sleep(800 * (i + 1));
    }
  }
  throw new Error("rate-limited");
}

const TARGET = 8;
const SEARCHES = [
  "Rodeo Drive Beverly Hills",
  "Beverly Hills sign California",
  "Beverly Hills City Hall",
  "Beverly Hills palm trees street",
  "Beverly Hills California",
];

let stored = 0;
for (const q of SEARCHES) {
  if (stored >= TARGET) break;
  const u =
    `https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search` +
    `&gsrsearch=${encodeURIComponent(q)}&gsrnamespace=6&gsrlimit=8` +
    `&prop=imageinfo&iiprop=extmetadata|url&iiurlwidth=900`;
  let j;
  try {
    j = await fetchJson(u);
  } catch {
    continue;
  }
  const pages = Object.values(j.query?.pages ?? {});
  for (const page of pages) {
    if (stored >= TARGET) break;
    const info = page.imageinfo?.[0];
    if (!info) continue;
    const m = info.extmetadata ?? {};
    const licName = m.LicenseShortName?.value || "";
    const lic = (m.License?.value || "").toLowerCase();
    const free = FREE.test(lic) || /public domain|cc0|cc by(?!-nc|-nd)/i.test(licName);
    if (!free || NONFREE.test(lic) || /nc|nd|fair use/i.test(licName)) continue;
    // Skip maps/logos/diagrams — want photos.
    const title = (page.title || "").replace(/^File:/, "");
    if (/\.svg$|map|logo|seal|diagram|\.pdf$/i.test(title)) continue;

    const url = info.thumburl || info.url;
    const dup = await pool.query(`select 1 from assets where file_url=$1`, [url]);
    if (dup.rowCount) continue;

    const licType = /cc0|public domain|^pd/i.test(lic) ? "public-domain" : /sa/i.test(lic) ? "cc-by-sa" : "cc-by";
    await pool.query(
      `insert into assets (type, file_url, thumbnail_url, license_type, license_url,
                           attribution_text, usage_scope, source_url, rights_notes, caption, approved_for_marketing)
       values ('image',$1,$1,$2,$3,$4,'editorial',$5,'beverly-hills-location',$6,false)`,
      [
        url,
        licType,
        m.LicenseUrl?.value || null,
        `${stripHtml(m.Artist?.value || m.Credit?.value || "Unknown")} / ${licName} (via Wikimedia Commons)`,
        `https://commons.wikimedia.org/wiki/${encodeURIComponent(page.title)}`,
        title.replace(/\.[a-z]+$/i, "").replace(/_/g, " ").slice(0, 80),
      ]
    );
    stored++;
    console.log(`OK  ${licType.padEnd(13)} ${title.slice(0, 60)}`);
  }
  await sleep(700);
}

console.log(`\nstored ${stored} Beverly Hills location photos (free-licensed).`);
await pool.end();