const fs = require('fs'); const path = require('path'); const pool = require('./db'); const { savePaintingImages } = require('../scripts/image-fetcher'); const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || './data/images'); const FETCH_TIMEOUT_MS = 15000; const inflight = new Map(); function withTimeout(promise, ms) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('Image fetch timeout')), ms)), ]); } function localFileExists(relPath) { if (!relPath) return false; return fs.existsSync(path.join(IMAGE_DIR, relPath)); } function syncPaintingFromDisk(row) { const safeBase = `${row.artist_name}_${row.title}`.replace(/[^a-zA-Z0-9_-]/g, '_'); let imagePath = row.image_path; let thumbPath = row.thumbnail_path; for (const ext of ['.jpg', '.jpeg', '.png', '.webp', '.JPG']) { const full = path.join(IMAGE_DIR, 'paintings', safeBase + ext); if (!imagePath && fs.existsSync(full)) { imagePath = `paintings/${safeBase}${ext}`; } const thumb = path.join(IMAGE_DIR, 'paintings', 'thumbs', safeBase + '_thumb' + ext); if (!thumbPath && fs.existsSync(thumb)) { thumbPath = `paintings/thumbs/${safeBase}_thumb${ext}`; } } if (!thumbPath && imagePath) thumbPath = imagePath; return { imagePath, thumbPath }; } /** Fast preload: link local files only, no external API calls */ async function preloadArtistImagesLocal(artistId) { 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.artist_id = $1`, [artistId] ); let linked = 0; for (const row of rows.rows) { const hasLocal = localFileExists(row.thumbnail_path) || localFileExists(row.image_path); if (hasLocal) { linked++; continue; } const synced = syncPaintingFromDisk(row); if (synced.imagePath || synced.thumbPath) { await pool.query( `UPDATE paintings SET image_path = COALESCE($1, image_path), thumbnail_path = COALESCE($2, thumbnail_path) WHERE id = $3`, [synced.imagePath, synced.thumbPath, row.id] ); linked++; } } return { fetched: linked, total: rows.rows.length }; } async function ensurePaintingImages(paintingId, size = 'thumb') { const key = `${paintingId}:${size}`; if (inflight.has(key)) return inflight.get(key); const promise = (async () => { const result = await pool.query( `SELECT p.id, p.title, p.wikipedia_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.id = $1`, [paintingId] ); if (result.rows.length === 0) return null; const row = result.rows[0]; const wantThumb = size !== 'full'; if (wantThumb && localFileExists(row.thumbnail_path)) return row.thumbnail_path; if (!wantThumb && localFileExists(row.image_path)) return row.image_path; if (wantThumb && localFileExists(row.image_path)) return row.image_path; const synced = syncPaintingFromDisk(row); if (synced.imagePath || synced.thumbPath) { await pool.query( `UPDATE paintings SET image_path = COALESCE($1, image_path), thumbnail_path = COALESCE($2, thumbnail_path) WHERE id = $3`, [synced.imagePath, synced.thumbPath, paintingId] ); if (wantThumb && localFileExists(synced.thumbPath)) return synced.thumbPath; if (wantThumb && localFileExists(synced.imagePath)) return synced.imagePath; if (!wantThumb && localFileExists(synced.imagePath)) return synced.imagePath; } if (!row.wikipedia_title) return null; const saved = await withTimeout( savePaintingImages( row.wikipedia_title, `${row.artist_name}_${row.title}`, IMAGE_DIR, { artistName: row.artist_name, paintingTitle: row.title, type: 'painting' } ), FETCH_TIMEOUT_MS ).catch(() => ({ imagePath: null, thumbnailPath: null })); if (saved.imagePath || saved.thumbnailPath) { await pool.query( `UPDATE paintings SET image_path = COALESCE($1, image_path), thumbnail_path = COALESCE($2, thumbnail_path) WHERE id = $3`, [saved.imagePath, saved.thumbnailPath, paintingId] ); } if (wantThumb) return saved.thumbnailPath || saved.imagePath; return saved.imagePath || saved.thumbnailPath; })().finally(() => inflight.delete(key)); inflight.set(key, promise); return promise; } module.exports = { ensurePaintingImages, preloadArtistImagesLocal, IMAGE_DIR };