mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
gaming updates
This commit is contained in:
@@ -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}"`);
|
||||
Reference in New Issue
Block a user