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:
+41
-17
@@ -10,6 +10,7 @@ const {
|
||||
|
||||
const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || './data/images');
|
||||
const FETCH_TIMEOUT_MS = 15000;
|
||||
const PORTRAIT_THUMB_WIDTH = 256;
|
||||
const inflight = new Map();
|
||||
|
||||
function safePaintingBase(artistName, title) {
|
||||
@@ -183,10 +184,34 @@ function unlinkPaintingFiles(row, safeBase) {
|
||||
|
||||
function unlinkPortraitFiles(row, safeBase) {
|
||||
unlinkIfExists(row.portrait_path ? path.join(IMAGE_DIR, row.portrait_path) : null);
|
||||
unlinkIfExists(row.portrait_thumb_path ? path.join(IMAGE_DIR, row.portrait_thumb_path) : null);
|
||||
const portraitsDir = path.join(IMAGE_DIR, 'portraits');
|
||||
const thumbsDir = path.join(portraitsDir, 'thumbs');
|
||||
for (const ext of ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.JPG']) {
|
||||
unlinkIfExists(path.join(portraitsDir, safeBase + ext));
|
||||
}
|
||||
unlinkIfExists(path.join(thumbsDir, safeBase + '_thumb.jpg'));
|
||||
unlinkIfExists(path.join(thumbsDir, safeBase + '_thumb' + '.jpeg'));
|
||||
}
|
||||
|
||||
async function writePortraitThumb(fullPath, safeBase) {
|
||||
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`);
|
||||
try {
|
||||
await generateThumbnailFromFull(fullPath, thumbDest, PORTRAIT_THUMB_WIDTH);
|
||||
return path.join('portraits', 'thumbs', `${safeBase}_thumb.jpg`).replace(/\\/g, '/');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath) {
|
||||
await pool.query(
|
||||
`UPDATE artists SET portrait_path = $1, portrait_thumb_path = $2 WHERE id = $3`,
|
||||
[portraitPath, portraitThumbPath, artistId]
|
||||
);
|
||||
return { portraitPath, portraitThumbPath };
|
||||
}
|
||||
|
||||
async function deletePainting(paintingId) {
|
||||
@@ -239,7 +264,7 @@ async function clearPaintingImage(paintingId) {
|
||||
|
||||
async function clearArtistPortrait(artistId) {
|
||||
const result = await pool.query(
|
||||
`SELECT id, name, portrait_path FROM artists WHERE id = $1`,
|
||||
`SELECT id, name, portrait_path, portrait_thumb_path FROM artists WHERE id = $1`,
|
||||
[artistId]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
@@ -250,9 +275,12 @@ async function clearArtistPortrait(artistId) {
|
||||
const safeBase = safeArtistPortraitBase(row.name);
|
||||
unlinkPortraitFiles(row, safeBase);
|
||||
|
||||
await pool.query(`UPDATE artists SET portrait_path = NULL WHERE id = $1`, [artistId]);
|
||||
await pool.query(
|
||||
`UPDATE artists SET portrait_path = NULL, portrait_thumb_path = NULL WHERE id = $1`,
|
||||
[artistId]
|
||||
);
|
||||
|
||||
return { portraitPath: null };
|
||||
return { portraitPath: null, portraitThumbPath: null };
|
||||
}
|
||||
|
||||
async function replacePaintingImageFromBuffer(paintingId, buffer, mimeType) {
|
||||
@@ -323,7 +351,7 @@ async function replaceArtistPortraitFromBuffer(artistId, buffer, mimeType) {
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT id, name, portrait_path FROM artists WHERE id = $1`,
|
||||
`SELECT id, name, portrait_path, portrait_thumb_path FROM artists WHERE id = $1`,
|
||||
[artistId]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
@@ -349,14 +377,13 @@ async function replaceArtistPortraitFromBuffer(artistId, buffer, mimeType) {
|
||||
const fullDest = path.join(portraitsDir, safeBase + fullExt);
|
||||
fs.writeFileSync(fullDest, buffer);
|
||||
const portraitPath = path.join('portraits', safeBase + fullExt).replace(/\\/g, '/');
|
||||
await pool.query(`UPDATE artists SET portrait_path = $1 WHERE id = $2`, [portraitPath, artistId]);
|
||||
return { portraitPath };
|
||||
const portraitThumbPath = (await writePortraitThumb(fullDest, safeBase)) || portraitPath;
|
||||
return updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
|
||||
}
|
||||
|
||||
const portraitPath = path.join('portraits', safeBase + '.jpg').replace(/\\/g, '/');
|
||||
await pool.query(`UPDATE artists SET portrait_path = $1 WHERE id = $2`, [portraitPath, artistId]);
|
||||
|
||||
return { portraitPath };
|
||||
const portraitThumbPath = (await writePortraitThumb(jpgDest, safeBase)) || portraitPath;
|
||||
return updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
|
||||
}
|
||||
|
||||
async function replacePaintingImageFromUrl(paintingId, imageUrl, context = {}) {
|
||||
@@ -409,7 +436,7 @@ async function replacePaintingImageFromUrl(paintingId, imageUrl, context = {}) {
|
||||
|
||||
async function replaceArtistPortraitFromUrl(artistId, imageUrl, context = {}) {
|
||||
const result = await pool.query(
|
||||
`SELECT id, name, portrait_path FROM artists WHERE id = $1`,
|
||||
`SELECT id, name, portrait_path, portrait_thumb_path FROM artists WHERE id = $1`,
|
||||
[artistId]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
@@ -424,10 +451,7 @@ async function replaceArtistPortraitFromUrl(artistId, imageUrl, context = {}) {
|
||||
const fullExt = pickExt(imageUrl);
|
||||
const fullDest = path.join(portraitsDir, safeBase + fullExt);
|
||||
|
||||
unlinkIfExists(row.portrait_path ? path.join(IMAGE_DIR, row.portrait_path) : null);
|
||||
for (const ext of ['.jpg', '.jpeg', '.png', '.webp', '.JPG']) {
|
||||
unlinkIfExists(path.join(portraitsDir, safeBase + ext));
|
||||
}
|
||||
unlinkPortraitFiles(row, safeBase);
|
||||
|
||||
await downloadImageForFix(imageUrl, fullDest, context);
|
||||
|
||||
@@ -449,9 +473,9 @@ async function replaceArtistPortraitFromUrl(artistId, imageUrl, context = {}) {
|
||||
// keep downloaded file as-is
|
||||
}
|
||||
|
||||
await pool.query(`UPDATE artists SET portrait_path = $1 WHERE id = $2`, [portraitPath, artistId]);
|
||||
|
||||
return { portraitPath };
|
||||
const sourceForThumb = fs.existsSync(jpgDest) ? jpgDest : path.join(IMAGE_DIR, portraitPath);
|
||||
const portraitThumbPath = (await writePortraitThumb(sourceForThumb, safeBase)) || portraitPath;
|
||||
return updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user