Improve 3D gallery and debug checkup workflow for image curation.

Add parquet floor, museum-style exit, movement-tinted walls, and gold/black
frames with gallery sync after Fix it. Debug panel gets Checked and Fix it
buttons; documentation and image-fetch reliability updates included.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-20 22:05:24 +03:00
co-authored by Cursor
parent 4c6acd5a3a
commit 019ce4e136
67 changed files with 1061 additions and 190 deletions
+116 -11
View File
@@ -4,6 +4,8 @@ const fs = require('fs');
const path = require('path');
const USER_AGENT = 'VirtualArtGallery/1.0 (educational art history project; local museum gallery)';
const BROWSER_USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
const WIKI_LANGS = ['de', 'fr', 'it', 'ru'];
@@ -200,17 +202,20 @@ async function throttle() {
lastRequestTime = Date.now();
}
function fetchBuffer(url, redirectCount = 0, referer = null) {
function fetchBuffer(url, redirectCount = 0, referer = null, options = {}) {
return new Promise((resolve, reject) => {
if (redirectCount > 8) return reject(new Error('Too many redirects'));
const client = url.startsWith('https') ? https : http;
const useBrowser = options.browser !== false && (options.browser === true || !!referer);
const headers = {
'User-Agent': USER_AGENT,
Accept: 'image/*,*/*',
'User-Agent': useBrowser ? BROWSER_USER_AGENT : USER_AGENT,
Accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
};
if (referer) headers.Referer = referer;
if (url.includes('wikimedia.org') || url.includes('wikipedia.org')) {
headers.Referer = 'https://commons.wikimedia.org/';
headers['User-Agent'] = USER_AGENT;
}
if (url.includes('artic.edu')) {
headers.Referer = 'https://www.artic.edu/';
@@ -221,15 +226,15 @@ function fetchBuffer(url, redirectCount = 0, referer = null) {
if (url.includes('hermitagemuseum.org') || url.includes('pushkinmuseum.art')) {
headers.Referer = url.split('/').slice(0, 3).join('/') + '/';
}
if (url.includes('googleusercontent.com') || url.includes('artsandculture.google.com')) {
headers.Referer = 'https://artsandculture.google.com/';
if (url.includes('googleusercontent.com') || url.includes('gstatic.com') || url.includes('artsandculture.google.com')) {
headers.Referer = headers.Referer || 'https://artsandculture.google.com/';
}
const req = client.get(url, { headers }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
const next = res.headers.location.startsWith('http')
? res.headers.location
: new URL(res.headers.location, url).href;
return resolve(fetchBuffer(next, redirectCount + 1, referer));
return resolve(fetchBuffer(next, redirectCount + 1, referer, options));
}
if (res.statusCode === 429) {
res.resume();
@@ -1182,6 +1187,109 @@ async function downloadImageToFile(url, destPath, { force = false } = {}) {
return destPath;
}
function upgradeWikimediaToFull(url) {
const thumbMatch = url.match(
/^(https?:\/\/upload\.wikimedia\.org\/wikipedia\/(?:commons|en)\/)thumb\/(.+)\/\d+px-[^/]+$/
);
if (thumbMatch) return `${thumbMatch[1]}${thumbMatch[2]}`;
return url;
}
function prepareDownloadCandidates(url, context = {}) {
const candidates = [url];
if (context.thumbUrl && context.thumbUrl !== url) candidates.push(context.thumbUrl);
const google = upgradeGoogleUserContentUrl(url, FULL_WIDTH);
if (google !== url) candidates.push(google);
const wiki = upgradeWikimediaToFull(url);
if (wiki !== url) candidates.push(wiki);
try {
const parsed = new URL(url);
if (parsed.search) candidates.push(`${parsed.origin}${parsed.pathname}`);
} catch {
// ignore
}
return [...new Set(candidates)];
}
function referersForDownload(url, context = {}) {
const refs = [];
if (context.pageUrl) refs.push(context.pageUrl);
if (context.searchUrl) refs.push(context.searchUrl);
if (context.source === 'google-arts-culture') refs.push('https://artsandculture.google.com/');
if (context.source === 'google-images' || context.source === 'google-custom-search') {
refs.push('https://www.google.com/');
}
if (context.source === 'duckduckgo-images') refs.push('https://duckduckgo.com/');
if (/googleusercontent\.com|gstatic\.com/i.test(url)) refs.push('https://artsandculture.google.com/');
if (/wikimedia\.org|wikipedia\.org/i.test(url)) refs.push('https://commons.wikimedia.org/');
try {
refs.push(new URL(url).origin + '/');
} catch {
// ignore
}
return [...new Set(refs)];
}
/** Download with browser headers, referer fallbacks, and URL variants (fix / debug). */
async function downloadImageForFix(url, destPath, context = {}) {
const candidates = prepareDownloadCandidates(url, context);
const referers = referersForDownload(url, context);
let lastError = null;
await throttle();
for (const candidate of candidates) {
for (const referer of referers) {
try {
const buffer = await fetchBuffer(candidate, 0, referer, { browser: true });
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, buffer);
return destPath;
} catch (err) {
lastError = err;
await sleep(300);
}
}
}
throw lastError || new Error('Download failed');
}
async function fetchImageBuffer(url, context = {}) {
const candidates = prepareDownloadCandidates(url, context);
const referers = referersForDownload(url, context);
let lastError = null;
await throttle();
for (const candidate of candidates) {
for (const referer of referers) {
try {
return await fetchBuffer(candidate, 0, referer, { browser: true });
} catch (err) {
lastError = err;
await sleep(300);
}
}
}
throw lastError || new Error('Download failed');
}
function friendlyImageFetchError(err) {
const msg = err?.message || '';
if (/ECONNRESET|ETIMEDOUT|EPIPE|socket hang up/i.test(msg)) {
return 'Could not download this image — the host closed the connection. Try Search again; Wikimedia and Google Arts results work best.';
}
if (/HTTP 403|HTTP 401|HTTP 429/i.test(msg)) {
return 'Could not download this image — the host blocked the request. Try Search again or pick a different preview.';
}
if (/Too many redirects/i.test(msg)) {
return 'Could not download this image — too many redirects from the image URL.';
}
return msg || 'Could not download image';
}
function decodeGoogleEmbeddedUrl(raw) {
return raw
.replace(/\\u003d/g, '=')
@@ -1361,11 +1469,6 @@ async function searchGoogleImagesFirst(artistName, paintingTitle) {
return { query, imageUrl: null, searchUrl, source: 'google-images', sourceLabel: 'Google Images' };
}
async function fetchImageBuffer(url) {
await throttle();
return withRetry(() => fetchBuffer(url));
}
async function generateThumbnailFromFull(fullDest, thumbDest, width = THUMB_WIDTH) {
const sharp = require('sharp');
if (!fs.existsSync(fullDest)) return false;
@@ -1493,11 +1596,13 @@ module.exports = {
saveImageForItem,
savePaintingImages,
downloadImageToFile,
downloadImageForFix,
generateThumbnailFromFull,
searchWikipediaTitle,
searchWebForPaintingImages,
searchGoogleImagesFirst,
fetchImageBuffer,
friendlyImageFetchError,
pickExt,
getGoogleArtsCultureImages,
simplifyPaintingTitle,