#!/usr/bin/env node /** * fetch-box-art.mjs * --------------------------------------------------------------------------- * Support tool for the Gaming Hall of Fame. Downloads box/banner art for a * game from the RAWG API (https://rawg.io/apidocs) and saves it into * `public/images/games/.jpg`, then prints the public path to paste into * a game file's `art:` field. * * RAWG covers Steam, PlayStation, Xbox, and Nintendo titles in one place, so * this works regardless of platform. Note: RAWG serves landscape banner / * screenshot art (not portrait Steam capsules), which matches the landscape * Hall of Fame cards (aspect-[4/3]). * * PREREQUISITES * 1. Get a free RAWG API key: https://rawg.io/login?forward=developer * 2. Put it in a `.env` file at the project root: * RAWG_API_KEY=your_key_here * (`.env` is gitignored; `.env.example` is the template.) * * USAGE * node scripts/fetch-box-art.mjs "Elden Ring" * node scripts/fetch-box-art.mjs "Hades" --platform pc * node scripts/fetch-box-art.mjs "Celeste" --out celeste.jpg * node scripts/fetch-box-art.mjs --id 3328 # by RAWG id * * Run with `--env-file=.env` to auto-load the key (Node 20.6+): * node --env-file=.env scripts/fetch-box-art.mjs "Elden Ring" * * If you don't use --env-file, export RAWG_API_KEY in your shell first. */ import { mkdir, writeFile } from 'node:fs/promises'; import { dirname, extname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PROJECT_ROOT = resolve(__dirname, '..'); const OUT_DIR = join(PROJECT_ROOT, 'public', 'images', 'games'); const RAWG_BASE = 'https://api.rawg.io/api'; // RAWG parent platforms that map to the --platform flag values. const PLATFORM_IDS = { pc: 1, playstation: 2, xbox: 3, nintendo: 7, mac: 4, linux: 5, ios: 5, android: 6, }; // --- arg parsing ------------------------------------------------------------ const args = process.argv.slice(2); if (args.length === 0 || args.includes('-h') || args.includes('--help')) { console.log(`Usage: node scripts/fetch-box-art.mjs "" [options] Options: --platform

narrow search to a parent platform: pc, playstation, xbox, nintendo --id fetch a specific game by its RAWG id (skips search) --out output filename (default: slug of the title, .jpg) -h, --help show this help Examples: node scripts/fetch-box-art.mjs "Elden Ring" node scripts/fetch-box-art.mjs "Hollow Knight" --platform pc node scripts/fetch-box-art.mjs --id 3328 Tip: run with \`node --env-file=.env scripts/fetch-box-art.mjs ...\` to auto-load the RAWG_API_KEY from your .env file.`); process.exit(args.length === 0 ? 1 : 0); } const positional = []; let platform = null; let rawgId = null; let outName = null; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--platform') platform = args[++i]; else if (a === '--id') rawgId = Number(args[++i]); else if (a === '--out') outName = args[++i]; else if (!a.startsWith('--')) positional.push(a); } const title = positional.join(' ').trim(); // --- helpers ---------------------------------------------------------------- const slugify = (s) => s .toLowerCase() .replace(/['’.:]/g, '') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); const fail = (msg) => { console.error(`✖ ${msg}`); process.exit(1); }; const buildImageUrl = (game) => { // Prefer the wide/landscape screenshot-style image, which suits the // aspect-[4/3] Hall of Fame cards. Fall back through RAWG's image fields. const candidates = [ game.background_image, game.background_image_additional, ]; const url = candidates.find(Boolean); if (!url) return null; // RAWG serves http(s) images; force https. return url.replace(/^http:\/\//i, 'https://'); }; // --- main ------------------------------------------------------------------- const API_KEY = process.env.RAWG_API_KEY; if (!API_KEY) { fail( 'RAWG_API_KEY is not set. Add it to .env (see .env.example) and run with\n' + ' node --env-file=.env scripts/fetch-box-art.mjs "..."', ); } let game; try { if (rawgId) { console.log(`→ Fetching RAWG game id ${rawgId}…`); const res = await fetch(`${RAWG_BASE}/games/${rawgId}?key=${API_KEY}`); if (!res.ok) { if (res.status === 404) fail(`No RAWG game with id ${rawgId}.`); fail(`RAWG detail request failed: ${res.status} ${res.statusText}`); } game = await res.json(); } else { if (!title) fail('No game title provided.'); const params = new URLSearchParams({ key: API_KEY, search: title, page_size: '5' }); if (platform) { const pid = PLATFORM_IDS[platform.toLowerCase()]; if (!pid) { fail( `Unknown --platform "${platform}". Try one of: ${Object.keys(PLATFORM_IDS).join(', ')}`, ); } params.set('parent_platforms', String(pid)); } console.log(`→ Searching RAWG for "${title}"${platform ? ` on ${platform}` : ''}…`); const res = await fetch(`${RAWG_BASE}/games?${params}`); if (!res.ok) fail(`RAWG search failed: ${res.status} ${res.statusText}`); const data = await res.json(); if (!data.results?.length) fail(`No RAWG results for "${title}".`); game = data.results[0]; console.log(` found: "${game.name}" (id ${game.id})${game.released ? `, ${game.released.slice(0, 4)}` : ''}`); } } catch (err) { fail(err?.message ? `Network error: ${err.message}` : 'Network error contacting RAWG.'); } const imageUrl = buildImageUrl(game); if (!imageUrl) { fail(`RAWG has no usable image for "${game.name}". Try another title or --id.`); } // Decide output filename + extension (keep the source extension if --out omits one). const baseName = outName ?? `${slugify(game.name)}.jpg`; const ext = extname(baseName) || '.jpg'; const fileName = baseName.replace(/\.[^.]+$/, '') + ext; const outPath = join(OUT_DIR, fileName); console.log(`→ Downloading ${imageUrl}`); const imgRes = await fetch(imageUrl); if (!imgRes.ok) fail(`Image download failed: ${imgRes.status} ${imgRes.statusText}`); const buf = Buffer.from(await imgRes.arrayBuffer()); await mkdir(OUT_DIR, { recursive: true }); await writeFile(outPath, buf); const publicPath = `/images/games/${fileName}`; console.log(`\n✔ Saved: ${outPath}`); console.log(` Use this in your game file's frontmatter:`); console.log(` art: "${publicPath}"`);