Files
Art-gallery/scripts/regenerate-thumbnails.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

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