/** * Seed historical eras, art movements, artists, periods, and flagship paintings. * Run: npm run dev:seed * Flags: --force (re-seed), --fetch-images (download portraits and paintings from Wikipedia) */ require('dotenv').config(); const path = require('path'); const pool = require('../server/db'); const { ERAS, MOVEMENTS, buildArtists } = require('./seed-catalog-data'); const { savePaintingImages, saveImageForItem } = require('./image-fetcher'); const FORCE = process.argv.includes('--force'); const FETCH_IMAGES = process.argv.includes('--fetch-images'); const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || path.join(__dirname, '../data/images')); async function tableCount(table) { const { rows } = await pool.query(`SELECT COUNT(*)::int AS n FROM ${table}`); return rows[0].n; } async function clearCatalog() { await pool.query(` TRUNCATE TABLE painting_annotations, painting_influence_sources, painting_influences, paintings, artist_periods, artists, art_movements, historical_eras RESTART IDENTITY CASCADE `); } async function insertEras() { const eraIds = new Map(); for (const era of ERAS) { const { rows } = await pool.query( `INSERT INTO historical_eras (name, start_year, end_year, start_definite, end_definite, description, sort_order) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, [era.name, era.start_year, era.end_year, era.start_definite, era.end_definite, era.description, era.sort_order] ); eraIds.set(era.name, rows[0].id); } return eraIds; } async function insertMovements(eraIds) { const movementIds = new Map(); for (const mov of MOVEMENTS) { const eraId = eraIds.get(mov.era) ?? null; const { rows } = await pool.query( `INSERT INTO art_movements (name, start_year, end_year, era_id, description, color) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, [mov.name, mov.start_year, mov.end_year, eraId, mov.description, mov.color] ); movementIds.set(mov.name, rows[0].id); } return movementIds; } function periodName(artist) { if (artist.birth_year != null && artist.death_year != null) { return `${artist.birth_year}–${artist.death_year}`; } return 'Main period'; } async function insertArtists(movementIds, artists) { let inserted = 0; let paintings = 0; let skippedPaintings = 0; let portraitsFetched = 0; let imagesFetched = 0; for (const artist of artists) { const movementId = movementIds.get(artist.movement); if (!movementId) { console.warn(` Unknown movement "${artist.movement}" for ${artist.name} — skipped`); continue; } const artistRes = await pool.query( `INSERT INTO artists (name, birth_year, death_year, movement_id, wikipedia_title, century) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, [artist.name, artist.birth_year, artist.death_year, movementId, artist.wikipedia_title, artist.century] ); const artistId = artistRes.rows[0].id; inserted += 1; const periodRes = await pool.query( `INSERT INTO artist_periods (artist_id, name, start_year, end_year, sort_order) VALUES ($1, $2, $3, $4, 0) RETURNING id`, [artistId, periodName(artist), artist.birth_year, artist.death_year] ); const periodId = periodRes.rows[0].id; if (FETCH_IMAGES) { try { const portraitBase = artist.name.replace(/[^a-zA-Z0-9_-]/g, '_'); const saved = await saveImageForItem(artist.wikipedia_title, 'portraits', portraitBase, IMAGE_DIR); if (saved.path) { await pool.query('UPDATE artists SET portrait_path = $1 WHERE id = $2', [saved.path, artistId]); portraitsFetched += 1; } } catch (err) { console.warn(` portrait: ${artist.name}: ${err.message}`); } } if (!artist.painting?.title) { skippedPaintings += 1; console.warn(` no flagship painting for ${artist.name}`); continue; } const wikiTitle = artist.painting.wikipedia_title || artist.painting.title; const paintingRes = await pool.query( `INSERT INTO paintings (artist_id, period_id, title, year, wikipedia_title, sort_order) VALUES ($1, $2, $3, $4, $5, 0) RETURNING id`, [artistId, periodId, artist.painting.title, artist.painting.year ?? null, wikiTitle] ); const paintingId = paintingRes.rows[0].id; paintings += 1; if (FETCH_IMAGES) { try { const base = `${artist.name}_${artist.painting.title}`.replace(/[^a-zA-Z0-9_-]/g, '_'); const saved = await savePaintingImages(wikiTitle, base, IMAGE_DIR, { artistName: artist.name, paintingTitle: artist.painting.title, }); if (saved.imagePath || saved.thumbnailPath) { await pool.query( 'UPDATE paintings SET image_path = $1, thumbnail_path = $2 WHERE id = $3', [saved.imagePath, saved.thumbnailPath, paintingId] ); imagesFetched += 1; } } catch (err) { console.warn(` image: ${artist.name} — ${artist.painting.title}: ${err.message}`); } } console.log(`+ ${artist.name} (${artist.movement}) — ${artist.painting.title}`); } return { inserted, paintings, skippedPaintings, portraitsFetched, imagesFetched }; } async function main() { const existing = await tableCount('artists'); if (existing > 0 && !FORCE) { console.log(`Catalog already has ${existing} artists. Use --force to re-seed.`); return; } if (FORCE && existing > 0) { console.log('Clearing existing catalog…'); await clearCatalog(); } const artists = buildArtists(); console.log(`Seeding ${ERAS.length} eras, ${MOVEMENTS.length} movements, ${artists.length} artists…`); const eraIds = await insertEras(); const movementIds = await insertMovements(eraIds); const stats = await insertArtists(movementIds, artists); console.log('\nSeed complete.'); console.log(` Artists: ${stats.inserted}`); console.log(` Paintings: ${stats.paintings}${stats.skippedPaintings ? ` (${stats.skippedPaintings} without flagship)` : ''}`); if (FETCH_IMAGES) { console.log(` Portraits downloaded: ${stats.portraitsFetched}`); console.log(` Painting images: ${stats.imagesFetched}`); } else { console.log(' Run with --fetch-images to download artwork files, or npm run dev:fetch-images later.'); } console.log('\nNext steps:'); console.log(' npm run dev:fetch-artist-bios'); console.log(' npm run dev:expand-catalog'); console.log(' npm run dev:update-influences'); } main() .then(() => pool.end()) .catch((err) => { console.error('Seed failed:', err.message); pool.end().finally(() => process.exit(1)); });