[object Object]

← 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

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 →