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 = ` AV `; 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/');