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>
139 lines
4.6 KiB
JavaScript
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);
|
|
});
|