← back to Nineoh Guide
scripts/seed-episode-cast.mjs
79 lines
// Seed episode_guest_cast from TVmaze (factual, no API key) for every episode
// that carries a tvmaze external id. Idempotent: clears + reinserts per episode.
// Also prints an aggregate of recurring guest people (episode counts) so we can
// pick the real recurring cast to add to the Cast tab.
//
// node scripts/seed-episode-cast.mjs
//
import { readFileSync } from "node:fs";
import { Pool } from "pg";
// --- load DATABASE_URL from apps/web/.env.local ---
const env = readFileSync(new URL("../apps/web/.env.local", import.meta.url), "utf8");
const DB = env.match(/^DATABASE_URL=(.+)$/m)?.[1]?.replace(/^["']|["']$/g, "");
if (!DB) throw new Error("DATABASE_URL not found in apps/web/.env.local");
const pool = new Pool({ connectionString: DB, max: 4 });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function tvmaze(url, tries = 3) {
for (let i = 0; i < tries; i++) {
const res = await fetch(url);
if (res.status === 429) { await sleep(1500); continue; }
if (!res.ok) throw new Error(`${res.status} ${url}`);
return res.json();
}
throw new Error(`retries exhausted ${url}`);
}
const { rows: eps } = await pool.query(
`select id, season_number, episode_number, external_ids
from episodes order by season_number, episode_number`
);
console.log(`episodes in DB: ${eps.length}`);
const agg = new Map(); // person -> { count, character }
let seeded = 0;
for (const ep of eps) {
const tv = ep.external_ids?.tvmaze;
if (!tv) { console.log(` S${ep.season_number}E${ep.episode_number} — no tvmaze id, skip`); continue; }
let data;
try {
data = await tvmaze(`https://api.tvmaze.com/episodes/${tv}?embed=guestcast`);
} catch (e) {
console.log(` S${ep.season_number}E${ep.episode_number} — fetch failed: ${e.message}`);
continue;
}
const guests = (data._embedded?.guestcast ?? []).map((c, i) => ({
name: c.person?.name ?? null,
character: c.character?.name ?? null,
ord: i,
})).filter((g) => g.name);
await pool.query(`delete from episode_guest_cast where episode_id = $1`, [ep.id]);
for (const g of guests) {
await pool.query(
`insert into episode_guest_cast (episode_id, person_name, character_name, ord)
values ($1,$2,$3,$4)`,
[ep.id, g.name, g.character, g.ord]
);
const k = g.name;
const cur = agg.get(k) ?? { count: 0, character: g.character };
cur.count++;
agg.set(k, cur);
}
seeded += guests.length;
process.stdout.write(`\r seeded S${ep.season_number}E${ep.episode_number} (+${guests.length}) `);
await sleep(140); // be polite to TVmaze
}
console.log(`\n\ntotal guest-cast rows: ${seeded}`);
console.log(`\n=== recurring guests (>=3 episodes) — candidates for the Cast tab ===`);
[...agg.entries()]
.filter(([, v]) => v.count >= 3)
.sort((a, b) => b[1].count - a[1].count)
.forEach(([name, v]) => console.log(`${String(v.count).padStart(3)} ${name} — ${v.character ?? "?"}`));
await pool.end();