← back to Stayclaim
scripts/enrich-productions-wikidata.ts
150 lines
#!/usr/bin/env -S npx tsx
/**
* Wikidata enrichment for film_production rows.
*
* For each production missing wikidata_qid / imdb_id / blurb, queries the
* Wikidata SPARQL endpoint to find the matching item (filtered by P31 to
* a film/TV-series subclass), then pulls IMDb ID (P345), Wikipedia URL,
* and falls back to a Wikipedia summary for the blurb.
*
* Tier-C source. Updates only fill empty fields.
*
* Usage:
* npx tsx scripts/enrich-productions-wikidata.ts
* npx tsx scripts/enrich-productions-wikidata.ts --slug mulholland-echoes
* npx tsx scripts/enrich-productions-wikidata.ts --dry
*/
import { Pool } from 'pg';
const pool = new Pool({
host: process.env.PGHOST ?? '/tmp',
database: process.env.PGDATABASE ?? 'stayclaim',
user: process.env.PGUSER ?? process.env.USER,
max: 4,
});
type WdRow = {
item: { value: string };
itemLabel: { value: string };
imdb?: { value: string };
wiki?: { value: string };
};
async function querySparql(name: string): Promise<WdRow | null> {
const sparql = `
SELECT ?item ?itemLabel ?imdb ?wiki WHERE {
?item rdfs:label "${name.replace(/"/g, '\\"')}"@en.
VALUES ?type { wd:Q11424 wd:Q5398426 wd:Q24856 wd:Q24862 wd:Q21191270 }
?item wdt:P31 ?type.
OPTIONAL { ?item wdt:P345 ?imdb. }
OPTIONAL {
?wiki schema:about ?item;
schema:isPartOf <https://en.wikipedia.org/>.
}
SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
LIMIT 3
`;
const url = 'https://query.wikidata.org/sparql?format=json&query=' + encodeURIComponent(sparql);
const res = await fetch(url, {
headers: {
Accept: 'application/sparql-results+json',
'User-Agent': 'pastdoor/0.1 (https://pastdoor.com)',
},
});
if (!res.ok) throw new Error(`Wikidata ${res.status}`);
const j = (await res.json()) as { results: { bindings: WdRow[] } };
return j.results.bindings[0] ?? null;
}
async function fetchWikiSummary(title: string): Promise<string | null> {
const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
const res = await fetch(url, {
headers: { 'User-Agent': 'pastdoor/0.1', Accept: 'application/json' },
});
if (!res.ok) return null;
const j = (await res.json()) as { extract?: string };
if (!j.extract) return null;
return j.extract.split(/(?<=\.)\s+/).slice(0, 2).join(' ').slice(0, 280);
}
function pageTitleFromUrl(url: string): string | null {
try {
const u = new URL(url);
if (!u.hostname.endsWith('wikipedia.org')) return null;
const m = u.pathname.match(/^\/wiki\/(.+)$/);
return m ? decodeURIComponent(m[1]!) : null;
} catch {
return null;
}
}
async function main() {
const args = process.argv.slice(2);
const slugArg = args.find((a, i) => args[i - 1] === '--slug');
const dry = args.includes('--dry');
const conds: string[] = [];
const params: unknown[] = [];
if (slugArg) {
params.push(slugArg);
conds.push(`slug = $${params.length}`);
}
const sql = `SELECT id, slug, title, year, wikidata_qid, imdb_id, blurb
FROM film_production
${conds.length ? 'WHERE ' + conds.join(' AND ') : ''}
ORDER BY title`;
const { rows } = await pool.query(sql, params);
let updated = 0;
let nohit = 0;
for (const p of rows) {
let wd;
try {
wd = await querySparql(p.title);
} catch (e) {
console.warn(`[${p.slug}] sparql err: ${(e as Error).message}`);
}
const updates: Record<string, unknown> = {};
if (!p.wikidata_qid && wd?.item?.value) {
const m = wd.item.value.match(/\/(Q\d+)$/);
if (m) updates.wikidata_qid = m[1];
}
if (!p.imdb_id && wd?.imdb?.value) updates.imdb_id = wd.imdb.value;
if (!p.blurb && wd?.wiki?.value) {
const title = pageTitleFromUrl(wd.wiki.value);
if (title) {
const summary = await fetchWikiSummary(title);
if (summary) updates.blurb = summary;
}
}
if (Object.keys(updates).length === 0) {
console.log(`[${p.slug}] nothing to add`);
if (!wd) nohit++;
await new Promise(r => setTimeout(r, 600));
continue;
}
if (dry) {
console.log(`[${p.slug}] DRY would set`, updates);
} else {
const setParts: string[] = [];
const vals: unknown[] = [];
for (const [k, v] of Object.entries(updates)) {
vals.push(v);
setParts.push(`${k} = $${vals.length}`);
}
vals.push(p.id);
await pool.query(`UPDATE film_production SET ${setParts.join(', ')} WHERE id = $${vals.length}`, vals);
console.log(`[${p.slug}] updated`, updates);
updated++;
}
await new Promise(r => setTimeout(r, 600));
}
console.log(JSON.stringify({ checked: rows.length, updated, no_match: nohit, dry }, null, 2));
await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });