← back to AsSeenInMovies
tick7: SPOTTED seed import — 14 placements from thesetdecorator film-pieces
9a1257758464f90111b77af172c93affdc94b65d · 2026-05-12 07:23:47 -0700 · SteveStudio2
14 of 66 seed rows imported into asim_spotted. 52 skipped (missing
imdb_id or piece_description — those are 'planned' entries without
firm cites yet). Now /api/spotted returns real data:
- The Girl with the Dragon Tattoo · Martin Vanger's abstract art
- The Royal Tenenbaums · Margot's zebra-wallpaper bedroom (wallcovering!)
- Forgetting Sarah Marshall · Turtle Bay Resort suite
Categorizer heuristic buckets into wallcovering / art / furniture /
lighting / rug / window-treatment / books / plants / prop / tableware /
set / dressing. The wallcovering bucket is exactly where DW SKU
cross-refs will land.
Each row also enriches asim_people + asim_credits — set decorator,
production designer, director — so /api/person/:id works once we add
the HTML page (tick 8). 30 credits added across the 14 placements.
Files touched
A scrapers/import-film-pieces-seed.js
Diff
commit 9a1257758464f90111b77af172c93affdc94b65d
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 07:23:47 2026 -0700
tick7: SPOTTED seed import — 14 placements from thesetdecorator film-pieces
14 of 66 seed rows imported into asim_spotted. 52 skipped (missing
imdb_id or piece_description — those are 'planned' entries without
firm cites yet). Now /api/spotted returns real data:
- The Girl with the Dragon Tattoo · Martin Vanger's abstract art
- The Royal Tenenbaums · Margot's zebra-wallpaper bedroom (wallcovering!)
- Forgetting Sarah Marshall · Turtle Bay Resort suite
Categorizer heuristic buckets into wallcovering / art / furniture /
lighting / rug / window-treatment / books / plants / prop / tableware /
set / dressing. The wallcovering bucket is exactly where DW SKU
cross-refs will land.
Each row also enriches asim_people + asim_credits — set decorator,
production designer, director — so /api/person/:id works once we add
the HTML page (tick 8). 30 credits added across the 14 placements.
---
scrapers/import-film-pieces-seed.js | 150 ++++++++++++++++++++++++++++++++++++
1 file changed, 150 insertions(+)
diff --git a/scrapers/import-film-pieces-seed.js b/scrapers/import-film-pieces-seed.js
new file mode 100644
index 0000000..3131279
--- /dev/null
+++ b/scrapers/import-film-pieces-seed.js
@@ -0,0 +1,150 @@
+#!/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); });
← 6c82c50 tick2: thesetdecorator roster importer + uniq idx on imdb_id
·
back to AsSeenInMovies
·
feat(asseeninmovies/tick8): /movie/:id + /person/:id HTML pa dcdead0 →