mirror of
https://github.com/KytyPS5/kytyps5.github.io.git
synced 2026-10-09 08:49:37 -07:00
- Markdown: remove escapeHtml in parseInline to prevent React double-escaping entities in JSX - Pipeline: prioritize unattempted games over noStore retries in import-games.mjs - Frontmatter: only coerce known numeric fields (score) to prevent numeric titles/versions from failing string validation - Workflows: pass step outputs through env in compat-convert.yml to prevent bash script injection - UI & State: avoid in-place sorting mutation in GamePage and calculate remaining contributors dynamically - Performance: deduplicate concurrent in-flight fetchContributorCount requests with in-memory caching - Cleanup: delete unused findGame, timeAgo, and markdownToText dead code - Security: bump nanoid via npm audit fix
65 lines
2.8 KiB
JavaScript
65 lines
2.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Import the PS5 game title database from andshrew/PlayStation-Titles and
|
|
* enrich entries with PlayStation Store metadata (covers, publisher, release
|
|
* date, genres).
|
|
*
|
|
* npm run import # refresh title list (no enrichment)
|
|
* npm run import -- --enrich 300 # + enrich 300 concepts with metadata
|
|
* npm run import -- --only PPSA01284 # enrich specific title IDs
|
|
* npm run import -- --force # accept a >5% shrink of the list
|
|
*
|
|
* Output: src/data/games.json (committed; regenerated weekly by CI).
|
|
*/
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { dedupeByConcept, mergeExisting } from "./lib/transform.mjs";
|
|
import { applyEnrichment, fetchConcept, guardShrink } from "./lib/enrich.mjs";
|
|
|
|
const SOURCE = "https://raw.githubusercontent.com/andshrew/PlayStation-Titles/main/Json/PS5_Titles.json";
|
|
const OUT = new URL("../src/data/games.json", import.meta.url);
|
|
const THROTTLE_MS = 600;
|
|
|
|
const args = process.argv.slice(2);
|
|
const force = args.includes("--force");
|
|
const enrichIdx = args.indexOf("--enrich");
|
|
const enrichCount = enrichIdx === -1 ? 0 : Number(args[enrichIdx + 1] ?? 0);
|
|
const onlyIdx = args.indexOf("--only");
|
|
const only = onlyIdx === -1 ? null : (args[onlyIdx + 1] ?? "").split(",").filter(Boolean);
|
|
|
|
const res = await fetch(SOURCE);
|
|
if (!res.ok) throw new Error(`source fetch failed: HTTP ${res.status}`);
|
|
const rows = await res.json();
|
|
console.log(`source rows: ${rows.length}`);
|
|
|
|
const fresh = dedupeByConcept(rows);
|
|
const existing = JSON.parse(await readFile(OUT, "utf8").catch(() => "[]"));
|
|
guardShrink(fresh.length, existing.length, force);
|
|
const games = mergeExisting(fresh, existing);
|
|
|
|
const unattempted = games.filter((g) => !g.enriched && !g.noStore);
|
|
const retries = games.filter((g) => !g.enriched && g.noStore);
|
|
const pending = only
|
|
? games.filter((g) => !g.enriched && g.allTitleIds.some((t) => only.includes(t)))
|
|
: [...unattempted, ...retries];
|
|
const todo = pending.slice(0, enrichCount);
|
|
let done = 0;
|
|
for (const game of todo) {
|
|
try {
|
|
const concept = await fetchConcept(game.conceptId);
|
|
Object.assign(game, applyEnrichment(game, concept));
|
|
done++;
|
|
} catch (err) {
|
|
console.error(`enrich ${game.titleId} (${game.conceptId}) failed: ${err.message}`);
|
|
if (String(err).includes("hash rejected")) break;
|
|
}
|
|
await new Promise((r) => setTimeout(r, THROTTLE_MS));
|
|
if (done % 25 === 0 && done > 0) console.log(`enriched ${done}/${todo.length}`);
|
|
}
|
|
|
|
await mkdir(new URL("../src/data/", import.meta.url), { recursive: true });
|
|
await writeFile(OUT, JSON.stringify(games, null, 1) + "\n");
|
|
const enrichedTotal = games.filter((g) => g.enriched).length;
|
|
console.log(
|
|
`wrote ${games.length} games (${enrichedTotal} enriched, ${games.length - enrichedTotal} pending) to src/data/games.json`,
|
|
);
|