/** * 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 { printCliResult } = require('./lib/cli-result'); 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(); if (errors.length) { printCliResult({ script: 'regenerate-thumbnails', ok: false, summary: `Painting thumbnail regeneration finished with ${errors.length} error(s).`, details: [ `Regenerated: ${regenerated}, skipped: ${skipped}`, ...errors.slice(0, 5).map((e) => ` #${e.id} ${e.title}: ${e.error}`), ], }); } printCliResult({ script: 'regenerate-thumbnails', ok: true, summary: 'Painting thumbnail regeneration complete.', details: [ `Regenerated: ${regenerated}, skipped: ${skipped}`, `Aspect-ratio mismatches before fix: ${mismatchesBefore}`, ], }); })().catch((err) => { console.error(err); printCliResult({ script: 'regenerate-thumbnails', ok: false, summary: err.message || String(err), }); });