Files
fallback-site/scripts/generate-thumbnails.js
Yuriy Panov b210e4d2fb Initial commit: Amber Valley Community Hub fallback site
Vite + React 18 + Tailwind CSS community events video library with 42
topic-matched thumbnails, auth modal that always fails, and Docker/nginx
setup on port 8081.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:00:09 +06:00

99 lines
3.5 KiB
JavaScript

import { mkdirSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const outDir = join(__dirname, '..', 'public', 'thumbnails');
mkdirSync(outDir, { recursive: true });
// Favicon
const favicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#d97706"/>
<text x="16" y="22" text-anchor="middle" fill="white" font-size="18" font-family="sans-serif" font-weight="bold">AV</text>
</svg>`;
writeFileSync(join(outDir, 'favicon.svg'), favicon);
// Each entry: [index, search keywords] — keywords chosen to match the video title
const thumbnails = [
[1, 'jazz,concert,stage'],
[2, 'pasta,cooking,italian'],
[3, 'soccer,football,tournament'],
[4, 'watercolor,painting,art'],
[5, 'python,programming,laptop'],
[6, 'garden,spring,flowers'],
[7, 'book,reading,library'],
[8, 'acoustic,guitar,microphone'],
[9, 'thai,food,cooking'],
[10, 'basketball,court,game'],
[11, 'oil,painting,landscape'],
[12, 'web,development,code'],
[13, 'rose,pruning,garden'],
[14, 'science,fiction,books'],
[15, 'blues,folk,music'],
[16, 'pastry,baking,french'],
[17, 'table,tennis,pingpong'],
[18, 'digital,art,illustration'],
[19, 'cloud,computing,server'],
[20, 'compost,soil,garden'],
[21, 'mystery,detective,book'],
[22, 'piano,recital,concert'],
[23, 'sushi,japanese,food'],
[24, 'volleyball,beach,sport'],
[25, 'pottery,ceramics,clay'],
[26, 'artificial,intelligence,robot'],
[27, 'herbs,garden,plants'],
[28, 'classic,literature,novel'],
[29, 'latin,dance,salsa'],
[30, 'bread,baking,dough'],
[31, 'badminton,racket,sport'],
[32, 'sketch,drawing,pencil'],
[33, 'cybersecurity,hacker,lock'],
[34, 'winter,vegetables,growing'],
[35, 'fantasy,tolkien,books'],
[36, 'choir,singing,holiday'],
[37, 'curry,indian,spices'],
[38, 'running,marathon,race'],
[39, 'photography,camera,walk'],
[40, 'blockchain,crypto,technology'],
[41, 'indoor,plants,houseplant'],
[42, 'poetry,reading,writing'],
];
async function downloadImage(index, keywords) {
const num = String(index).padStart(2, '0');
const filePath = join(outDir, `thumb-${num}.jpg`);
// loremflickr serves real Creative Commons photos matching keywords
// lock parameter ensures deterministic results
const url = `https://loremflickr.com/640/360/${keywords}?lock=${index}`;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(url, { redirect: 'follow' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
if (buffer.length < 1000) throw new Error('Image too small, likely error');
writeFileSync(filePath, buffer);
console.log(` [${num}] OK (${(buffer.length / 1024).toFixed(0)} KB) — ${keywords}`);
return;
} catch (err) {
if (attempt < 2) {
console.log(` [${num}] Retry ${attempt + 1}...`);
await new Promise(r => setTimeout(r, 1000));
} else {
console.error(` [${num}] FAILED: ${err.message}`);
}
}
}
}
console.log('Downloading 42 topic-matched thumbnails from loremflickr.com...\n');
const BATCH = 4;
for (let i = 0; i < thumbnails.length; i += BATCH) {
const batch = thumbnails.slice(i, i + BATCH);
await Promise.all(batch.map(([idx, kw]) => downloadImage(idx, kw)));
}
console.log('\nDone! 42 topic-matched thumbnails in public/thumbnails/');