Files
Art-gallery/scripts/expand-paintings.js
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

139 lines
4.6 KiB
JavaScript

require('dotenv').config();
const path = require('path');
const pool = require('../server/db');
const ADDITIONS = require('./famous-paintings-data');
const { savePaintingImages } = require('./image-fetcher');
const MIN_PAINTINGS = parseInt(process.env.MIN_PAINTINGS || '6', 10);
const FETCH_IMAGES = process.argv.includes('--fetch-images');
const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || path.join(__dirname, '../data/images'));
function normalizeTitle(s) {
return s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
}
function titleMatches(existingNorm, title) {
const norm = normalizeTitle(title);
if (existingNorm.has(norm)) return true;
for (const e of existingNorm) {
if (e.includes(norm) || norm.includes(e)) return true;
}
return false;
}
async function main() {
const artistsRes = await pool.query('SELECT id, name FROM artists ORDER BY name');
const artistByName = new Map(artistsRes.rows.map((a) => [a.name, a]));
const unknown = new Set();
for (const entry of ADDITIONS) {
if (!artistByName.has(entry.artist)) unknown.add(entry.artist);
}
if (unknown.size) {
console.warn('Unknown artist names in data (skipped):', [...unknown].join(', '));
}
let inserted = 0;
let skipped = 0;
let imagesFetched = 0;
const newIds = [];
for (const artist of artistsRes.rows) {
const countRes = await pool.query(
'SELECT COUNT(*)::int AS n FROM paintings WHERE artist_id = $1',
[artist.id]
);
let count = countRes.rows[0].n;
if (count >= MIN_PAINTINGS) continue;
const existingRes = await pool.query(
'SELECT title FROM paintings WHERE artist_id = $1',
[artist.id]
);
const existingNorm = new Set(existingRes.rows.map((r) => normalizeTitle(r.title)));
const candidates = ADDITIONS.filter((e) => e.artist === artist.name);
for (const entry of candidates) {
if (count >= MIN_PAINTINGS) break;
if (titleMatches(existingNorm, entry.title)) {
skipped += 1;
continue;
}
const wikiTitle = entry.wikipedia_title || entry.title;
const sortRes = await pool.query(
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS next FROM paintings WHERE artist_id = $1',
[artist.id]
);
const sortOrder = sortRes.rows[0].next;
const insert = await pool.query(
`INSERT INTO paintings (artist_id, title, year, wikipedia_title, sort_order)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[artist.id, entry.title, entry.year ?? null, wikiTitle, sortOrder]
);
const paintingId = insert.rows[0].id;
existingNorm.add(normalizeTitle(entry.title));
count += 1;
inserted += 1;
newIds.push({ id: paintingId, artist: artist.name, title: entry.title, wikiTitle });
console.log(`+ ${artist.name}: ${entry.title}${entry.year ? ` (${entry.year})` : ''}`);
}
}
console.log(`\nInserted ${inserted} paintings (${skipped} duplicates skipped)`);
if (FETCH_IMAGES && newIds.length) {
console.log(`\nFetching images for ${newIds.length} new paintings…`);
for (const row of newIds) {
try {
const base = `${row.artist}_${row.title}`.replace(/[^a-zA-Z0-9_-]/g, '_');
const saved = await savePaintingImages(row.wikiTitle, base, IMAGE_DIR, {
artistName: row.artist,
paintingTitle: row.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, row.id]
);
imagesFetched += 1;
console.log(` ✓ image: ${row.artist}${row.title}`);
} else {
console.warn(` ✗ no image: ${row.artist}${row.title}`);
}
} catch (err) {
console.warn(` ✗ ${row.artist}${row.title}: ${err.message}`);
}
}
console.log(`\nImages fetched: ${imagesFetched}/${newIds.length}`);
} else if (newIds.length) {
console.log('Run with --fetch-images to download artwork files.');
}
const summary = await pool.query(`
SELECT
COUNT(*) FILTER (WHERE cnt < $1)::int AS below_min,
COUNT(*) FILTER (WHERE cnt >= $1)::int AS at_min,
MAX(cnt)::int AS max_paintings,
ROUND(AVG(cnt), 1) AS avg_paintings
FROM (
SELECT COUNT(p.id)::int AS cnt
FROM artists a
LEFT JOIN paintings p ON p.artist_id = a.id
GROUP BY a.id
) t
`, [MIN_PAINTINGS]);
console.log('\nCatalog summary:', summary.rows[0]);
await pool.end();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});