Add seed scripts, proxy Vite on public domain, and seed painting images.
Restore seed-wikipedia.js and seed-catalog-data.js so npm run setup works on a fresh clone. Point nginx at the Vite dev client and allow the public hostname in Vite config.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Seed historical eras, art movements, artists, periods, and flagship paintings.
|
||||
* Run: npm run 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 fetch-images later.');
|
||||
}
|
||||
console.log('\nNext steps:');
|
||||
console.log(' npm run fetch-artist-bios');
|
||||
console.log(' npm run expand-catalog');
|
||||
console.log(' npm run update-influences');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => pool.end())
|
||||
.catch((err) => {
|
||||
console.error('Seed failed:', err.message);
|
||||
pool.end().finally(() => process.exit(1));
|
||||
});
|
||||
Reference in New Issue
Block a user