Files
Art-gallery/scripts/update-influences.js
T
Danila KhodjaefandCursor d8385d83d6 Add painting detail navigation, influence lamps, and catalog tooling.
Expand the gallery with prev/next browsing and fullscreen detail view, golden influence lamps and chronological wall layout in 3D halls, and scripts/docs for catalog expansion, influence edges, and multi-source image fetching.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 16:35:33 +03:00

217 lines
6.5 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 FETCH_IMAGES = process.argv.includes('--fetch-images');
const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || path.join(__dirname, '../data/images'));
const ARTIST_ALIASES = {
'Camille Corot': 'Jean-Baptiste-Camille Corot',
};
function normalizeTitle(s) {
return (s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
}
function normalizeArtist(name) {
return ARTIST_ALIASES[name] || name;
}
function scoreTitleMatch(dbTitle, hint) {
const normDb = normalizeTitle(dbTitle);
const keywords = normalizeTitle(hint).split(' ').filter((w) => w.length > 2);
if (!keywords.length) return 0;
const hits = keywords.filter((k) => normDb.includes(k)).length;
const required = keywords.length === 1 ? 1 : Math.min(2, keywords.length);
return hits >= required ? hits : 0;
}
async function loadMovements() {
const { rows } = await pool.query('SELECT id, name FROM art_movements');
const byName = new Map(rows.map((r) => [r.name.toLowerCase(), r.id]));
return byName;
}
async function findArtist(name) {
const canonical = normalizeArtist(name);
const { rows } = await pool.query(
`SELECT id, name FROM artists WHERE name = $1 OR name ILIKE $2 LIMIT 1`,
[canonical, canonical]
);
return rows[0] || null;
}
async function ensureArtist(ref, movementsByName) {
const name = normalizeArtist(ref.artist);
let artist = await findArtist(name);
if (artist) return artist.id;
const meta = ref.artistMeta;
if (!meta) return null;
const movementId = meta.movement
? movementsByName.get(meta.movement.toLowerCase()) || null
: null;
const century =
meta.century ||
(meta.birth_year ? Math.floor(meta.birth_year / 100) * 100 : null);
const insert = await pool.query(
`INSERT INTO artists (name, birth_year, death_year, movement_id, wikipedia_title, century, portrait_path)
VALUES ($1, $2, $3, $4, $5, $6, NULL)
RETURNING id, name`,
[
name,
meta.birth_year ?? null,
meta.death_year ?? null,
movementId,
meta.wikipedia_title || name,
century,
]
);
console.log(`+ artist: ${name}`);
return insert.rows[0].id;
}
async function findPainting(artistId, titleHint) {
const { rows } = await pool.query(
'SELECT id, title FROM paintings WHERE artist_id = $1',
[artistId]
);
let best = null;
let bestScore = 0;
for (const row of rows) {
const score = scoreTitleMatch(row.title, titleHint);
if (score > bestScore) {
bestScore = score;
best = row;
}
}
return best;
}
async function ensurePainting(artistId, ref, artistName) {
const existing = await findPainting(artistId, ref.title);
if (existing) return existing.id;
const sortRes = await pool.query(
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS next FROM paintings WHERE artist_id = $1',
[artistId]
);
const wikiTitle = ref.wikipedia_title || ref.title;
const insert = await pool.query(
`INSERT INTO paintings (artist_id, title, year, wikipedia_title, sort_order)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[artistId, ref.title, ref.year ?? null, wikiTitle, sortRes.rows[0].next]
);
console.log(`+ painting: ${artistName}${ref.title}`);
if (FETCH_IMAGES) {
try {
const base = `${artistName}_${ref.title}`.replace(/[^a-zA-Z0-9_-]/g, '_');
const saved = await savePaintingImages(wikiTitle, base, IMAGE_DIR, {
artistName,
paintingTitle: ref.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, insert.rows[0].id]
);
}
} catch (err) {
console.warn(` image fetch failed: ${artistName}${ref.title}: ${err.message}`);
}
}
return insert.rows[0].id;
}
async function resolvePaintingRef(ref, movementsByName) {
const artistName = normalizeArtist(ref.artist);
const artistId = await ensureArtist(ref, movementsByName);
if (!artistId) return null;
return ensurePainting(artistId, ref, artistName);
}
async function insertInfluence(workId, sourceId, edge) {
const result = await pool.query(
`INSERT INTO painting_influences
(painting_id, influenced_by_painting_id, notes, source, aspects, quote, source_author, source_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (painting_id, influenced_by_painting_id) DO NOTHING
RETURNING id`,
[
workId,
sourceId,
edge.notes || null,
edge.source || null,
edge.aspects || null,
edge.quote || null,
edge.source_author || null,
edge.source_url || null,
]
);
return result.rowCount > 0;
}
async function main() {
const movementsByName = await loadMovements();
let added = 0;
let skipped = 0;
let failed = 0;
for (const edge of INFLUENCES) {
try {
const workId = await resolvePaintingRef(edge.work, movementsByName);
const sourceId = await resolvePaintingRef(edge.influencedBy, movementsByName);
if (!workId || !sourceId) {
failed += 1;
console.warn(
`✗ unresolved: ${edge.work.artist} / ${edge.work.title} <- ${edge.influencedBy?.artist} / ${edge.influencedBy?.title}`
);
continue;
}
if (workId === sourceId) {
skipped += 1;
continue;
}
const inserted = await insertInfluence(workId, sourceId, edge);
if (inserted) {
added += 1;
console.log(`→ ${edge.work.artist} / ${edge.work.title}${edge.influencedBy.artist} / ${edge.influencedBy.title}`);
} else {
skipped += 1;
}
} catch (err) {
failed += 1;
console.warn(`✗ ${edge.work?.artist}: ${err.message}`);
}
}
const total = await pool.query('SELECT COUNT(*)::int AS n FROM painting_influences');
const connected = await pool.query(`
SELECT COUNT(DISTINCT a.id)::int AS n
FROM artists a
JOIN paintings p ON p.artist_id = a.id
JOIN painting_influences pi ON pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id
`);
console.log(`\nDone: ${added} added, ${skipped} skipped (duplicate/self), ${failed} failed`);
console.log(`Total influence edges: ${total.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);
});