Improve timeline UX and add catalog restore scripts for paintings and portraits.

Load the home-page catalog once with a lightweight artists API, batch pan/zoom updates per frame, use dynamic year labels, and speed up movement-flow zoom. Add sync-image-paths and fetch-artist-images plus docs for the post-seed pipeline.
This commit is contained in:
Danila Khodjaef
2026-07-03 18:33:16 +03:00
parent 7f26696328
commit 02d238b043
26 changed files with 865 additions and 195 deletions
+82
View File
@@ -1807,11 +1807,93 @@ async function saveImageForItem(wikiTitle, subdir, filename, imageDir, options =
}
}
const ARTIST_PORTRAIT_WIKI_OVERRIDES = {
Zeuxis: 'Zeuxis (painter)',
'Ivan Klyun': 'Ivan Kliun',
'Jean-Antoine Watteau': 'Antoine Watteau',
};
function artistPortraitWikiCandidates(artistName, wikiTitle) {
const seen = new Set();
const out = [];
for (const title of [
ARTIST_PORTRAIT_WIKI_OVERRIDES[artistName],
ARTIST_PORTRAIT_WIKI_OVERRIDES[wikiTitle],
wikiTitle,
artistName,
`${artistName} (painter)`,
`${wikiTitle} (painter)`,
]) {
if (title && !seen.has(title)) {
seen.add(title);
out.push(title);
}
}
return out;
}
async function resolveArtistPortrait(artistName, wikiTitle) {
for (const title of artistPortraitWikiCandidates(artistName, wikiTitle)) {
const images = await getWikipediaImages(title);
if (images?.fullUrl) {
return { ...images, wikipedia_title: title };
}
}
const search = await searchArtistPortraitFirst(artistName);
if (search?.imageUrl) {
return {
fullUrl: search.imageUrl,
source: search.sourceLabel || search.source || 'web search',
wikipedia_title: wikiTitle,
};
}
return null;
}
function findLocalPortraitPath(artistName, imageDir) {
const safeBase = artistName.replace(/[^a-zA-Z0-9_-]/g, '_');
for (const ext of ['.jpg', '.jpeg', '.png', '.webp', '.JPG']) {
const rel = `portraits/${safeBase}${ext}`;
if (fs.existsSync(path.join(imageDir, rel))) return rel.replace(/\\/g, '/');
}
return null;
}
async function saveArtistPortrait(artistName, wikiTitle, imageDir) {
const local = findLocalPortraitPath(artistName, imageDir);
if (local) return { path: local, source: 'local disk' };
const resolved = await resolveArtistPortrait(artistName, wikiTitle);
if (!resolved?.fullUrl) return { path: null, source: null };
const portraitsDir = path.join(imageDir, 'portraits');
if (!fs.existsSync(portraitsDir)) fs.mkdirSync(portraitsDir, { recursive: true });
const safeBase = artistName.replace(/[^a-zA-Z0-9_-]/g, '_');
const ext = pickExt(resolved.fullUrl);
const destPath = path.join(portraitsDir, safeBase + ext);
try {
await downloadImageToFile(resolved.fullUrl, destPath);
return {
path: path.join('portraits', safeBase + ext).replace(/\\/g, '/'),
source: resolved.source,
};
} catch (err) {
console.warn(` Portrait download failed: ${err.message}`);
return { path: null, source: resolved.source };
}
}
module.exports = {
resolveImageUrl,
resolvePaintingImages,
saveImageForItem,
savePaintingImages,
saveArtistPortrait,
findLocalPortraitPath,
resolveArtistPortrait,
downloadImageToFile,
downloadImageForFix,
generateThumbnailFromFull,