/** * 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), }); }); });