Files
Art-gallery/scripts/update-influences.js
T
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

182 lines
5.3 KiB
JavaScript

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 dev: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);
});