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>
98 lines
2.9 KiB
JavaScript
98 lines
2.9 KiB
JavaScript
/**
|
|
* Regenerate all painting thumbnails from their full-size local files.
|
|
* Fixes mismatches where Commons/Wikipedia thumb URLs pointed to the wrong work.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const sharp = require('sharp');
|
|
const pool = require('../server/db');
|
|
const { generateThumbnailFromFull } = require('./image-fetcher');
|
|
|
|
const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || './data/images');
|
|
|
|
function safeBase(artistName, title) {
|
|
return `${artistName}_${title}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
}
|
|
|
|
function localPath(rel) {
|
|
return path.join(IMAGE_DIR, rel);
|
|
}
|
|
|
|
async function aspectRatio(filePath) {
|
|
const meta = await sharp(filePath).metadata();
|
|
if (!meta.width || !meta.height) return null;
|
|
return meta.width / meta.height;
|
|
}
|
|
|
|
(async () => {
|
|
const rows = await pool.query(
|
|
`SELECT p.id, p.title, p.image_path, p.thumbnail_path, a.name AS artist_name
|
|
FROM paintings p
|
|
JOIN artists a ON a.id = p.artist_id
|
|
WHERE p.image_path IS NOT NULL AND p.image_path <> ''
|
|
ORDER BY p.id`
|
|
);
|
|
|
|
let regenerated = 0;
|
|
let skipped = 0;
|
|
let mismatchesBefore = 0;
|
|
const errors = [];
|
|
|
|
for (const row of rows.rows) {
|
|
const fullPath = localPath(row.image_path);
|
|
if (!fs.existsSync(fullPath)) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
const base = safeBase(row.artist_name, row.title);
|
|
const thumbRel = `paintings/thumbs/${base}_thumb.jpg`;
|
|
const thumbPath = localPath(thumbRel);
|
|
|
|
if (row.thumbnail_path && fs.existsSync(localPath(row.thumbnail_path))) {
|
|
try {
|
|
const [fullAr, thumbAr] = await Promise.all([
|
|
aspectRatio(fullPath),
|
|
aspectRatio(localPath(row.thumbnail_path)),
|
|
]);
|
|
if (fullAr && thumbAr && Math.abs(fullAr - thumbAr) / fullAr > 0.15) {
|
|
mismatchesBefore++;
|
|
}
|
|
} catch {
|
|
mismatchesBefore++;
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (fs.existsSync(thumbPath)) fs.unlinkSync(thumbPath);
|
|
if (
|
|
row.thumbnail_path &&
|
|
row.thumbnail_path !== thumbRel &&
|
|
fs.existsSync(localPath(row.thumbnail_path))
|
|
) {
|
|
fs.unlinkSync(localPath(row.thumbnail_path));
|
|
}
|
|
|
|
await generateThumbnailFromFull(fullPath, thumbPath);
|
|
await pool.query(`UPDATE paintings SET thumbnail_path = $1 WHERE id = $2`, [thumbRel, row.id]);
|
|
regenerated++;
|
|
} catch (err) {
|
|
errors.push({ id: row.id, title: row.title, error: err.message });
|
|
}
|
|
}
|
|
|
|
console.log('Paintings with full image path:', rows.rows.length);
|
|
console.log('Thumbnails regenerated:', regenerated);
|
|
console.log('Skipped (full file missing):', skipped);
|
|
console.log('Aspect-ratio mismatches before fix:', mismatchesBefore);
|
|
if (errors.length) {
|
|
console.log('Errors:', errors.length);
|
|
errors.slice(0, 20).forEach((e) => console.log(` #${e.id} ${e.title}: ${e.error}`));
|
|
}
|
|
|
|
await pool.end();
|
|
})().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|