require('dotenv').config(); const path = require('path'); const pool = require('../server/db'); const INFLUENCES = require('./art-influences-data'); const { savePaintingImages } = require('./image-fetcher'); const { discoverInfluencesForWork } = require('./influence-discovery'); const { normalizeInfluencedBy, loadMovements, resolveWorkRef, resolveAndInsertSource, } = require('./influence-resolver'); const FETCH_IMAGES = process.argv.includes('--fetch-images'); const DISCOVER = process.argv.includes('--discover'); const DISCOVER_ONLY = process.argv.includes('--discover-only'); const LIMIT = parseInt(process.argv.find((a) => a.startsWith('--limit='))?.split('=')[1] || '0', 10); const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || path.join(__dirname, '../data/images')); async function applyEdge(edge, movementsByName, stats) { const work = await resolveWorkRef( pool, edge.work, movementsByName, FETCH_IMAGES, IMAGE_DIR, savePaintingImages ); if (!work) { stats.failed += 1; console.warn(`✗ unresolved work: ${edge.work.artist} / ${edge.work.title}`); return; } const sources = normalizeInfluencedBy(edge.influencedBy); for (const sourceRef of sources) { const result = await resolveAndInsertSource( pool, work, sourceRef, edge, movementsByName, FETCH_IMAGES, IMAGE_DIR, savePaintingImages ); if (!result.ok) { if (result.reason !== 'self' && result.reason !== 'self artist') { stats.failed += 1; console.warn( `✗ ${edge.work.artist} / ${edge.work.title} <- ${sourceRef.type || 'painting'} ${result.reason}` ); } else { stats.skipped += 1; } continue; } if (result.inserted) { stats.added += 1; console.log(`→ ${edge.work.artist} / ${edge.work.title} ← [${sourceRef.type || 'painting'}] ${result.label}`); } else { stats.skipped += 1; } } if (DISCOVER || DISCOVER_ONLY) { const discovered = await discoverInfluencesForWork({ artist: edge.work.artist, title: edge.work.title, year: edge.year ?? edge.work.year ?? work.year, }); for (const sourceRef of discovered) { const result = await resolveAndInsertSource( pool, work, sourceRef, sourceRef, movementsByName, false, IMAGE_DIR, null ); if (result.inserted) { stats.discovered += 1; console.log(`~ discovered ${edge.work.artist} / ${edge.work.title} ← [${sourceRef.type}] ${result.label}`); } } } } async function discoverCatalog(limit) { const { rows } = await pool.query( `SELECT p.id, p.title, p.year, a.name AS artist_name FROM paintings p JOIN artists a ON a.id = p.artist_id ORDER BY a.name, p.sort_order, p.year NULLS LAST` ); const targets = limit > 0 ? rows.slice(0, limit) : rows; const movementsByName = await loadMovements(pool); let discovered = 0; for (const row of targets) { const refs = await discoverInfluencesForWork({ artist: row.artist_name, title: row.title, year: row.year, }); for (const sourceRef of refs) { const result = await resolveAndInsertSource( pool, { id: row.id, year: row.year, artist_id: null }, sourceRef, sourceRef, movementsByName, false, IMAGE_DIR, null ); if (result.inserted) { discovered += 1; console.log(`~ discovered ${row.artist_name} / ${row.title} ← [${sourceRef.type}] ${result.label}`); } } } return discovered; } async function main() { const tableCheck = await pool.query(` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'painting_influence_sources' ) AS ok `); if (!tableCheck.rows[0]?.ok) { console.error('Run npm run migrate:influence-sources before update-influences.'); process.exit(1); } const movementsByName = await loadMovements(pool); const stats = { added: 0, skipped: 0, failed: 0, discovered: 0 }; if (!DISCOVER_ONLY) { const edges = LIMIT > 0 ? INFLUENCES.slice(0, LIMIT) : INFLUENCES; for (const edge of edges) { await applyEdge(edge, movementsByName, stats); } } if (DISCOVER_ONLY) { stats.discovered += await discoverCatalog(LIMIT); } const [totalSources, totalLegacy, connected] = await Promise.all([ pool.query('SELECT COUNT(*)::int AS n FROM painting_influence_sources'), pool.query('SELECT COUNT(*)::int AS n FROM painting_influences'), pool.query(` SELECT COUNT(DISTINCT a.id)::int AS n FROM artists a JOIN paintings p ON p.artist_id = a.id LEFT JOIN painting_influence_sources pis ON pis.painting_id = p.id LEFT JOIN painting_influences pi ON pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id WHERE pis.id IS NOT NULL OR pi.id IS NOT NULL `), ]); console.log( `\nDone: ${stats.added} curated added, ${stats.discovered} discovered, ${stats.skipped} skipped, ${stats.failed} failed` ); console.log(`Total influence sources: ${totalSources.rows[0].n} (legacy painting edges: ${totalLegacy.rows[0].n})`); console.log(`Artists connected to influence graph: ${connected.rows[0].n}`); await pool.end(); } main().catch((err) => { console.error(err); process.exit(1); });