#!/usr/bin/env node /* Adds a LEGO set to the collection using Rebrickable API. Usage: node tools/add_lego.js Flow: 1) Search sets with the provided query 2) Interactive choice if multiple results 3) Fetch chosen set details and its theme 4) Create content/collections/lego/// - Always create images/ - Create data/images/ - Download all available set images from Rebrickable - Create data/images/.yaml with attribution - Create index.md with title and date */ const fs = require('fs/promises'); const fsSync = require('fs'); const path = require('path'); const https = require('https'); const readline = require('readline'); const ROOT = process.cwd(); const LEGO_ROOT = path.join(ROOT, 'content', 'collections', 'lego'); const CONFIG_PATH = path.join(ROOT, 'tools', 'config.json'); const CACHE_ROOT = path.join(ROOT, 'tools', 'cache', 'rebrickable'); function loadConfig() { const raw = fsSync.readFileSync(CONFIG_PATH, 'utf8'); return JSON.parse(raw); } function slugify(input) { return input .normalize('NFD') .replace(/\p{Diacritic}/gu, '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .replace(/-{2,}/g, '-'); } function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); })); } function apiGetJson(host, path, apiKey) { return new Promise((resolve) => { const req = https.request({ host, path, method: 'GET', headers: { 'Authorization': `key ${apiKey}`, 'Accept': 'application/json', 'User-Agent': 'add_lego_script/1.0', }, }, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { resolve(JSON.parse(data)); }); }); req.end(); }); } function downloadFile(url, destPath) { return new Promise((resolve) => { const file = fsSync.createWriteStream(destPath); https.get(url, (res) => { res.pipe(file); file.on('finish', () => file.close(() => resolve())); }); }); } async function ensureDir(dir) { await fs.mkdir(dir, { recursive: true }); } async function saveJson(relPath, data) { const out = path.join(CACHE_ROOT, relPath); await ensureDir(path.dirname(out)); await fs.writeFile(out, JSON.stringify(data, null, 2), 'utf8'); } async function main() { const query = process.argv[2]; if (!query) { console.log('Usage: node tools/add_lego.js '); process.exit(1); } const config = loadConfig(); const apiKey = config.rebrickable.apiKey; const host = 'rebrickable.com'; // 1) Search const searchPath = `/api/v3/lego/sets/?search=${encodeURIComponent(query)}&page_size=25`; const search = await apiGetJson(host, searchPath, apiKey); const results = search.results || []; await saveJson(`search/${encodeURIComponent(query)}.json`, search); if (results.length === 0) { console.log('No sets found for query:', query); process.exit(0); } // 2) Selection let selected = results[0]; if (results.length > 1) { console.log(`Found ${results.length} sets:`); results.forEach((r, i) => { const id = (r.set_num || '').split('-')[0]; console.log(`${i + 1}. ${r.set_num} — ${r.name} (${r.year}) [id: ${id}]`); }); const ans = await prompt('Choose a set (1..N): '); const idx = Math.max(1, Math.min(results.length, parseInt(ans, 10) || 1)) - 1; selected = results[idx]; } const setNum = selected.set_num; // e.g., 10283-1 const setId = (setNum || '').split('-')[0]; // 3) Fetch exact set and theme const setDetails = await apiGetJson(host, `/api/v3/lego/sets/${encodeURIComponent(setNum)}/`, apiKey); await saveJson(`sets/${setNum}.json`, setDetails); const themeId = setDetails.theme_id || selected.theme_id; const theme = await apiGetJson(host, `/api/v3/lego/themes/${themeId}/`, apiKey); await saveJson(`themes/${themeId}.json`, theme); const themeName = theme.name; const themeSlug = slugify(themeName); // 4) Prepare dirs const setDir = path.join(LEGO_ROOT, themeSlug, setId); const imagesDir = path.join(setDir, 'images'); const dataImagesDir = path.join(setDir, 'data', 'images'); await ensureDir(imagesDir); // created even if no images provided await ensureDir(dataImagesDir); // Images provided by Rebrickable (set images) const imageUrls = []; if (setDetails.set_img_url) imageUrls.push(setDetails.set_img_url); if (setDetails.set_img_url_2) imageUrls.push(setDetails.set_img_url_2); for (const url of imageUrls) { const urlObj = new URL(url); const base = path.basename(urlObj.pathname); const dest = path.join(imagesDir, base); await downloadFile(url, dest); const nameNoExt = base.replace(/\.[^.]+$/, ''); const yamlPath = path.join(dataImagesDir, `${nameNoExt}.yaml`); const titleForSetImage = (setDetails.name || selected.name || '').replace(/"/g, '\\"'); const yaml = `attribution: "© Rebrickable"\n` + `title: "${titleForSetImage}"\n`; await fs.writeFile(yamlPath, yaml, 'utf8'); } // Minifigs images (stored directly in images/) const figsResp = await apiGetJson(host, `/api/v3/lego/sets/${encodeURIComponent(setNum)}/minifigs/?page_size=1000`, apiKey); const figs = figsResp.results || []; await saveJson(`sets/${setNum}_minifigs.json`, figsResp); if (figs.length > 0) { for (const fig of figs) { const figNum = fig.fig_num || fig.set_num; let figImg = fig.set_img_url || fig.fig_img_url; let figTitle = fig.name || ''; if (!figImg && figNum) { const figDetails = await apiGetJson(host, `/api/v3/lego/minifigs/${encodeURIComponent(figNum)}/`, apiKey); await saveJson(`minifigs/${figNum}.json`, figDetails); figImg = figDetails.set_img_url || figDetails.fig_img_url; figTitle = figTitle || figDetails.name || ''; } if (!figImg) continue; const urlObj = new URL(figImg); const base = path.basename(urlObj.pathname); const dest = path.join(imagesDir, base); await downloadFile(figImg, dest); const nameNoExt = base.replace(/\.[^.]+$/, ''); const yamlPath = path.join(dataImagesDir, `${nameNoExt}.yaml`); const yaml = `attribution: "© Rebrickable"\n` + `title: "${(figTitle || '').replace(/"/g, '\\"')}"\n`; await fs.writeFile(yamlPath, yaml, 'utf8'); } } // index.md const title = setDetails.name || selected.name; const today = new Date(); const date = today.toISOString().slice(0, 10); const md = `---\n` + `title: "${title.replace(/"/g, '\\"')}"\n` + `date: ${date}\n` + `---\n`; await fs.writeFile(path.join(setDir, 'index.md'), md, 'utf8'); console.log(`\nAdded set ${setNum} → ${path.relative(ROOT, setDir)}`); } main();