mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
179 lines
6.4 KiB
JavaScript
179 lines
6.4 KiB
JavaScript
#!/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}"`);
|