Speed up timeline load with portrait thumbs, bootstrap API, and caching.

Add catalog bootstrap endpoint, portrait thumbnail pipeline, lazy queued timeline images, gzip compression, and 3D texture throttling with code-split VirtualGallery.
This commit is contained in:
Danila Khodjaef
2026-07-06 14:27:29 +03:00
parent bdddadc4d6
commit 99b8559607
117 changed files with 646 additions and 171 deletions
+76
View File
@@ -0,0 +1,76 @@
/**
* 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 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();
})().catch((err) => {
console.error(err);
pool.end().finally(() => process.exit(1));
});