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:
co-authored by
Cursor
parent
4c6acd5a3a
commit
019ce4e136
+116
-11
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user