Introduce devtoprod:release orchestrator, config file, CLI result footers on deploy scripts, auto-thumb regeneration on curator fixes, and updated deploy documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
3.7 KiB
JavaScript
125 lines
3.7 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 { 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),
|
|
});
|
|
});
|