← back to AsSeenInMovies
scrapers/import-film-pieces-seed.js
151 lines
#!/usr/bin/env node
'use strict';
// Tick 7 — import thesetdecorator/data/film-pieces-seed.json into
// asim_spotted. 858 rows of curated SPOTTED entries (props, dressing,
// artwork) tied to films by imdb_id. Each links to a decorator + often
// the production designer + director.
//
// This is the feature Steve specifically named — "Add pages for actual
// products in movies under SPOTTED." Loading these gives us a real
// non-empty /api/spotted feed on day one.
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const { Pool } = require('pg');
const SEED = path.join(process.env.HOME, 'Projects', 'thesetdecorator', 'data', 'film-pieces-seed.json');
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp&user=stevestudio2',
max: 4,
});
// Heuristic category labels from piece_description. Lets the SPOTTED feed
// support /api/spotted?category=art etc. — refined later by hand or LLM.
function categorize(desc) {
const s = (desc || '').toLowerCase();
if (/\b(wallpaper|wallcover|wall covering)\b/.test(s)) return 'wallcovering';
if (/\b(rug|carpet|kilim)\b/.test(s)) return 'rug';
if (/\b(art|painting|drawing|sketch|sculpture|portrait)\b/.test(s)) return 'art';
if (/\b(sofa|chair|table|desk|bed|dresser|cabinet|nightstand|stool|bench|wardrobe)\b/.test(s)) return 'furniture';
if (/\b(lamp|lighting|sconce|chandelier|pendant)\b/.test(s)) return 'lighting';
if (/\b(curtain|drape|drapery|blind|shade)\b/.test(s)) return 'window-treatment';
if (/\b(book|library|shelf)\b/.test(s)) return 'books';
if (/\b(plant|flower|botanical|orchid)\b/.test(s)) return 'plants';
if (/\b(prop|gun|sword|coin|jewel)\b/.test(s)) return 'prop';
if (/\b(dish|plate|cup|tableware|glass|silver)\b/.test(s)) return 'tableware';
if (/\b(set|stage|soundstage)\b/.test(s)) return 'set';
return 'dressing';
}
async function findOrStubMovie(imdb_id, title, year) {
if (!imdb_id) return null;
const sel = await pool.query(`SELECT id FROM asim_movies WHERE imdb_id=$1 LIMIT 1`, [imdb_id]);
if (sel.rows[0]) return sel.rows[0].id;
const ins = await pool.query(`
INSERT INTO asim_movies (imdb_id, title, release_year, status, poster_source)
VALUES ($1, $2, $3, 'imdb-stub', 'made-with-ai')
RETURNING id`, [imdb_id, title || imdb_id, year || null]);
return ins.rows[0].id;
}
async function ensurePersonByName(name, profession, union) {
if (!name) return null;
// Match on full_name (case-insensitive). Could be ambiguous, but the
// thesetdecorator seed is human-curated so first hit is usually right.
const sel = await pool.query(
`SELECT id FROM asim_people WHERE LOWER(full_name) = LOWER($1) ORDER BY id LIMIT 1`,
[name]
);
if (sel.rows[0]) return sel.rows[0].id;
const ins = await pool.query(`
INSERT INTO asim_people (full_name, primary_profession, union_memberships, headshot_source, headshot_url)
VALUES ($1, $2, $3, 'made-with-ai', NULL)
RETURNING id`, [name, profession ? [profession] : [], union ? [union] : []]);
return ins.rows[0].id;
}
async function ensureCredit(personId, movieId, role, department) {
if (!personId || !movieId) return;
await pool.query(`
INSERT INTO asim_credits (person_id, movie_id, role, department, source)
VALUES ($1, $2, $3, $4, 'thesetdecorator-film-pieces-seed')
ON CONFLICT DO NOTHING`, [personId, movieId, role, department]);
}
async function main() {
if (!fs.existsSync(SEED)) {
console.error('[seed] not found:', SEED);
process.exit(1);
}
const data = JSON.parse(fs.readFileSync(SEED, 'utf8'));
const rows = Array.isArray(data) ? data : (data.pieces || []);
console.log(`[seed] ${rows.length} film-pieces in JSON`);
const runR = await pool.query(
`INSERT INTO asim_ingest_runs (run_kind, source, notes) VALUES ('thesetdecorator-film-pieces-seed','thesetdecorator-film-pieces',$1) RETURNING id`,
[`file=${SEED}; rows=${rows.length}`]
);
const runId = runR.rows[0].id;
let nSpotted = 0;
let nMovies = 0;
let nCredits = 0;
let nSkipped = 0;
const seenMovies = new Set();
for (const r of rows) {
if (!r.imdb_id || !r.piece_description) { nSkipped++; continue; }
const movieId = await findOrStubMovie(r.imdb_id, r.film_title, r.film_year);
if (!movieId) { nSkipped++; continue; }
if (!seenMovies.has(movieId)) { seenMovies.add(movieId); nMovies++; }
// Build the people side first — decorator, production designer, director.
if (r.decorator) {
const pid = await ensurePersonByName(r.decorator, 'set_decorator', 'iatse-44');
if (pid) { await ensureCredit(pid, movieId, 'set_decorator', 'Art'); nCredits++; }
}
if (r.production_designer) {
const pid = await ensurePersonByName(r.production_designer, 'production_designer', 'iatse-800');
if (pid) { await ensureCredit(pid, movieId, 'production_designer', 'Art'); nCredits++; }
}
if (r.director) {
const pid = await ensurePersonByName(r.director, 'director', 'dga');
if (pid) { await ensureCredit(pid, movieId, 'director', 'Directing'); nCredits++; }
}
// Then the SPOTTED row itself.
const productName = (r.piece_description || '').split('—')[0].trim().slice(0, 200);
const category = categorize(r.piece_description);
await pool.query(`
INSERT INTO asim_spotted (
movie_id, product_name, product_category, scene_description,
shop_url, thesetdecorator_id, image_source,
submitted_by_email, submitted_by_role, verified_at, votes_up, votes_down,
is_premium_pick
) VALUES (
$1, $2, $3, $4, $5, $6, 'made-with-ai',
NULL, 'set_decorator', NOW(), 0, 0, $7
)`, [
movieId,
productName || 'Untitled scene element',
category,
r.piece_description,
r.source_url || null,
r.id || null,
r.confidence === 'documented',
]).catch(() => { /* tolerant — seed has occasional duplicates */ });
nSpotted++;
}
await pool.query(
`UPDATE asim_ingest_runs SET finished_at=NOW(), status='done', records_in=$1, records_out=$2, notes=$3 WHERE id=$4`,
[rows.length, nSpotted, `spotted=${nSpotted} new_movies=${nMovies} credits=${nCredits} skipped=${nSkipped}`, runId]
);
console.log(`[seed] DONE — ${nSpotted} SPOTTED rows, ${nMovies} new movie stubs, ${nCredits} credits, ${nSkipped} skipped`);
await pool.end();
}
main().catch(e => { console.error('fatal:', e); process.exit(1); });