← back to Nineoh Guide

db/ingest-guestcast.mjs

60 lines

// Per-episode cast detail: pulls the guest/recurring cast for every episode from
// TVmaze (factual — actor + character names) into episode_guest_cast. The app user
// owns its DB, so it can CREATE this table itself (no superuser DDL needed).
// Throttled to respect TVmaze's rate limit. Run: node db/ingest-guestcast.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 sleep = (ms) => new Promise((r) => setTimeout(r, ms));

await pool.query(`
  create table if not exists episode_guest_cast (
    id serial primary key,
    episode_id uuid not null references episodes(id) on delete cascade,
    person_name text not null,
    character_name text,
    ord int default 0
  )`);
await pool.query(
  `create index if not exists idx_egc_ep on episode_guest_cast(episode_id)`
);

const eps = (
  await pool.query(
    `select id, season_number, episode_number, external_ids->>'tvmaze' as tvmaze
       from episodes where external_ids ? 'tvmaze' order by season_number, episode_number`
  )
).rows;

let done = 0,
  rows = 0;
for (const ep of eps) {
  await sleep(380); // TVmaze rate limit (>=20 req / 10s)
  try {
    const r = await fetch(
      `https://api.tvmaze.com/episodes/${ep.tvmaze}?embed=guestcast`,
      { headers: { "User-Agent": "nineoh-guide/0.1 (fan project)" } }
    );
    const j = await r.json();
    const guests = j._embedded?.guestcast ?? [];
    await pool.query(`delete from episode_guest_cast where episode_id=$1`, [ep.id]);
    let i = 0;
    for (const g of guests) {
      const name = g.person?.name;
      if (!name) continue;
      await pool.query(
        `insert into episode_guest_cast (episode_id, person_name, character_name, ord) values ($1,$2,$3,$4)`,
        [ep.id, name, g.character?.name ?? null, i++]
      );
      rows++;
    }
    done++;
    if (done % 20 === 0) console.log(`  ${done}/${eps.length} episodes, ${rows} guest credits`);
  } catch (e) {
    console.warn(`S${ep.season_number}E${ep.episode_number} failed: ${e.message}`);
  }
}
console.log(`done: ${done}/${eps.length} episodes, ${rows} guest credits total`);
await pool.end();