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>
103 lines
3.1 KiB
JavaScript
103 lines
3.1 KiB
JavaScript
/**
|
|
* Generate timeline portrait thumbnails from existing full portraits.
|
|
* Writes portraits/thumbs/{Artist}_thumb.jpg and updates portrait_thumb_path.
|
|
*/
|
|
require('dotenv').config();
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
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');
|
|
const PORTRAIT_THUMB_WIDTH = 256;
|
|
|
|
function safeArtistPortraitBase(artistName) {
|
|
return artistName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
}
|
|
|
|
function localPath(rel) {
|
|
return path.join(IMAGE_DIR, rel);
|
|
}
|
|
|
|
async function generatePortraitThumb(row) {
|
|
if (!row.portrait_path) return { skipped: true, reason: 'no portrait' };
|
|
|
|
const fullPath = localPath(row.portrait_path);
|
|
if (!fs.existsSync(fullPath)) return { skipped: true, reason: 'missing file' };
|
|
|
|
const safeBase = safeArtistPortraitBase(row.name);
|
|
const thumbsDir = path.join(IMAGE_DIR, 'portraits', 'thumbs');
|
|
if (!fs.existsSync(thumbsDir)) fs.mkdirSync(thumbsDir, { recursive: true });
|
|
|
|
const thumbDest = path.join(thumbsDir, `${safeBase}_thumb.jpg`);
|
|
await generateThumbnailFromFull(fullPath, thumbDest, PORTRAIT_THUMB_WIDTH);
|
|
const portraitThumbPath = path.join('portraits', 'thumbs', `${safeBase}_thumb.jpg`).replace(/\\/g, '/');
|
|
|
|
await pool.query(`UPDATE artists SET portrait_thumb_path = $1 WHERE id = $2`, [
|
|
portraitThumbPath,
|
|
row.id,
|
|
]);
|
|
|
|
return { skipped: false, portraitThumbPath };
|
|
}
|
|
|
|
(async () => {
|
|
const { rows } = await pool.query(
|
|
`SELECT id, name, portrait_path, portrait_thumb_path FROM artists
|
|
WHERE portrait_path IS NOT NULL AND portrait_path <> ''
|
|
ORDER BY id`
|
|
);
|
|
|
|
let generated = 0;
|
|
let skipped = 0;
|
|
const errors = [];
|
|
|
|
for (const row of rows) {
|
|
try {
|
|
const result = await generatePortraitThumb(row);
|
|
if (result.skipped) skipped++;
|
|
else generated++;
|
|
} catch (err) {
|
|
errors.push({ id: row.id, name: row.name, error: err.message });
|
|
}
|
|
}
|
|
|
|
console.log(`Portrait thumbs: ${generated} generated, ${skipped} skipped, ${errors.length} errors`);
|
|
if (errors.length) {
|
|
for (const e of errors.slice(0, 20)) {
|
|
console.warn(` #${e.id} ${e.name}: ${e.error}`);
|
|
}
|
|
}
|
|
|
|
await pool.end();
|
|
|
|
if (errors.length) {
|
|
printCliResult({
|
|
script: 'regenerate-portrait-thumbs',
|
|
ok: false,
|
|
summary: `Portrait thumbnail regeneration finished with ${errors.length} error(s).`,
|
|
details: [
|
|
`Generated: ${generated}, skipped: ${skipped}`,
|
|
...errors.slice(0, 5).map((e) => ` #${e.id} ${e.name}: ${e.error}`),
|
|
],
|
|
});
|
|
}
|
|
|
|
printCliResult({
|
|
script: 'regenerate-portrait-thumbs',
|
|
ok: true,
|
|
summary: 'Portrait thumbnail regeneration complete.',
|
|
details: [`Generated: ${generated}, skipped: ${skipped}`],
|
|
});
|
|
})().catch((err) => {
|
|
console.error(err);
|
|
pool.end().finally(() => {
|
|
printCliResult({
|
|
script: 'regenerate-portrait-thumbs',
|
|
ok: false,
|
|
summary: err.message || String(err),
|
|
});
|
|
});
|
|
});
|