Files
Danila KhodjaefandCursor 96285737f5 Rename npm scripts to environment-prefixed names (dev:/prod:/devtoprod:/prodto:dev:/infra:).
Align package.json scripts and their references across scripts/, infra/, and db/ SQL with the dev-first workflow naming scheme.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 12:37:21 +03:00

197 lines
6.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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));
});