← back to Nineoh Guide

db/ingest-original.mjs

150 lines

// Ingest the ORIGINAL "Beverly Hills, 90210" (1990–2000) as a SECOND show,
// alongside the existing 2008 reboot. FACTUAL episode metadata only (title,
// season, episode number, air date) from the free TVmaze API — NO synopses
// (those stay original editorial). Names-only cast; photos are added later by
// ingest-wikimedia-images.mjs which accepts ONLY commercial-free licenses.
// Run: node db/ingest-original.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 });

// Find the 1990 ORIGINAL (not the 2008 reboot).
const searchRes = await fetch(
  "https://api.tvmaze.com/search/shows?q=beverly%20hills%2090210"
);
const results = await searchRes.json();
const pick =
  results
    .map((r) => r.show)
    .find(
      (s) =>
        /beverly hills,? 90210/i.test(s.name) &&
        (s.premiered ?? "").startsWith("1990")
    ) ?? results.map((r) => r.show).find((s) => (s.premiered ?? "").startsWith("1990"));

if (!pick) {
  console.error("No TVmaze match for the 1990 Beverly Hills, 90210");
  process.exit(1);
}
console.log(`TVmaze show: ${pick.name} (${pick.premiered}) id=${pick.id} — ${pick.network?.name ?? "?"}`);

// Seed the ORIGINAL show row (idempotent) — distinct canonical_title from the reboot.
const CANON = "beverly-hills-90210";
await pool.query(
  `insert into shows (canonical_title, display_title, franchise_title, description_original, disclaimer_text)
   select $1,$2,$3,$4,$5
   where not exists (select 1 from shows where canonical_title = $1)`,
  [
    CANON,
    "Beverly Hills, 90210 (1990)",
    "Beverly Hills, 90210 franchise",
    "A fan-made companion to the original 1990–2000 teen drama set in the 90210 ZIP code. Unofficial and not affiliated with the producers, network, or cast.",
    "Unofficial, fan-made. Not affiliated with or endorsed by the series' producers, network, or cast.",
  ]
);
const showId = (
  await pool.query(`select id from shows where canonical_title=$1`, [CANON])
).rows[0].id;
console.log(`original show_id = ${showId}`);

// One embedded call returns every episode (factual fields only).
const epsRes = await fetch(`https://api.tvmaze.com/shows/${pick.id}?embed=episodes`);
const data = await epsRes.json();
const episodes = (data._embedded?.episodes ?? []).filter((e) => e.season && e.number);

// Upsert seasons.
const seasonNums = [...new Set(episodes.map((e) => e.season))].sort((a, b) => a - b);
for (const n of seasonNums) {
  const count = episodes.filter((e) => e.season === n).length;
  await pool.query(
    `insert into seasons (show_id, season_number, episode_count, source_last_verified_at)
     values ($1,$2,$3, now())
     on conflict (show_id, season_number)
     do update set episode_count = excluded.episode_count, source_last_verified_at = now()`,
    [showId, n, count]
  );
}

// Upsert episodes — FACTUAL fields only; summaries left NULL for original editorial recaps.
let inserted = 0;
for (const e of episodes) {
  const seasonRow = (
    await pool.query(`select id from seasons where show_id=$1 and season_number=$2`, [showId, e.season])
  ).rows[0];
  const res = await pool.query(
    `insert into episodes
       (show_id, season_id, season_number, episode_number, title, air_date,
        source_url, source_type, external_ids, last_verified_at)
     values ($1,$2,$3,$4,$5,$6,$7,'tvmaze',$8, now())
     on conflict (show_id, season_number, episode_number)
     do update set title = excluded.title, air_date = excluded.air_date, last_verified_at = now()`,
    [
      showId,
      seasonRow?.id ?? null,
      e.season,
      e.number,
      e.name,
      e.airdate || null,
      e.url || null,
      JSON.stringify({ tvmaze: String(e.id) }),
    ]
  );
  inserted += res.rowCount;
}
console.log(`ORIGINAL: seasons ${seasonNums.length}, episodes upserted ${inserted}`);

// Names-only main cast via the embedded cast endpoint (factual credits; no photos here).
try {
  const castRes = await fetch(`https://api.tvmaze.com/shows/${pick.id}/cast`);
  const castArr = await castRes.json();
  let castN = 0;
  for (let i = 0; i < castArr.length; i++) {
    const person = castArr[i]?.person, character = castArr[i]?.character;
    if (!person?.name) continue;
    // Upsert person by name (no external_ids column; names are facts).
    let personId = (
      await pool.query(
        `insert into cast_people (name, publicity_risk_level)
           select $1, 'editorial-only'
           where not exists (select 1 from cast_people where name = $1)
           returning id`,
        [person.name]
      )
    ).rows[0]?.id;
    if (!personId) {
      personId = (await pool.query(`select id from cast_people where name=$1`, [person.name])).rows[0].id;
    }
    const pRow = { id: personId };
    let charId = null;
    if (character?.name) {
      charId = (
        await pool.query(
          `insert into characters (name, show_id)
             select $1, $2
             where not exists (select 1 from characters where name=$1 and show_id=$2)
             returning id`,
          [character.name, showId]
        )
      ).rows[0]?.id;
      if (!charId) {
        charId = (await pool.query(`select id from characters where name=$1 and show_id=$2`, [character.name, showId])).rows[0].id;
      }
    }
    await pool.query(
      `insert into credits (show_id, person_id, character_id, credit_type, billing_order)
       values ($1,$2,$3,'main-cast',$4)
       on conflict do nothing`,
      [showId, pRow.id, charId, i + 1]
    );
    castN++;
  }
  console.log(`ORIGINAL: main-cast credits ${castN}`);
} catch (e) {
  console.log("cast ingest skipped:", String(e).slice(0, 80));
}

console.log("Done. Recaps/bios intentionally NULL (original editorial); photos via ingest-wikimedia-images (commercial-free only).");
await pool.end();