gaming updates

This commit is contained in:
Greg Kilmer
2026-07-06 19:13:15 -04:00
parent 9b3c15b67b
commit 7808c2ead8
36 changed files with 825 additions and 84 deletions
+178
View File
@@ -0,0 +1,178 @@
#!/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/<slug>.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 "<game title>" [options]
Options:
--platform <p> narrow search to a parent platform: pc, playstation, xbox, nintendo
--id <rawgId> fetch a specific game by its RAWG id (skips search)
--out <name> 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}"`);