← back to Nineoh Guide
TK-12: ingest-original.mjs — load original Beverly Hills 90210 (1990) as show #2 (facts-only episodes + names-only cast; images stay commercial-free via existing wikimedia ingest)
c3861408c43fa74a46249f799d729fd722bc6a48 · 2026-08-05 15:02:27 -0700 · Steve Abrams
Files touched
Diff
commit c3861408c43fa74a46249f799d729fd722bc6a48
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 5 15:02:27 2026 -0700
TK-12: ingest-original.mjs — load original Beverly Hills 90210 (1990) as show #2 (facts-only episodes + names-only cast; images stay commercial-free via existing wikimedia ingest)
---
db/ingest-original.mjs | 141 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 141 insertions(+)
diff --git a/db/ingest-original.mjs b/db/ingest-original.mjs
new file mode 100644
index 0000000..72fe960
--- /dev/null
+++ b/db/ingest-original.mjs
@@ -0,0 +1,141 @@
+// 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;
+ const pRow = (
+ await pool.query(
+ `insert into cast_people (name, external_ids)
+ values ($1,$2)
+ on conflict (name) do update set external_ids = cast_people.external_ids || excluded.external_ids
+ returning id`,
+ [person.name, JSON.stringify({ tvmaze: String(person.id) })]
+ )
+ ).rows[0];
+ let charId = null;
+ if (character?.name) {
+ charId = (
+ await pool.query(
+ `insert into characters (show_id, name)
+ values ($1,$2)
+ on conflict (show_id, name) do update set name = excluded.name
+ returning id`,
+ [showId, character.name]
+ )
+ ).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();
← ce0f6a4 TK-12: add AdMob banner + ATT prompt (Google Mobile Ads SDK)
·
back to Nineoh Guide
·
Fix AdMob crash-on-launch: move iosAppId/androidAppId into t d49d05b →