Initial commit: virtual art gallery application.

Express API with PostgreSQL, React/Vite/Three.js frontend, and locally cached artwork images.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-19 09:38:22 +03:00
co-authored by Cursor
commit 40db99d734
378 changed files with 6498 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
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 };