feat: add Game8 parser with Genshin and NTE adapters

Parsers are keyed by site template, not game: one Game8 parser serves
every Game8 page, and adapters bind a URL and game to a parser. Adding a
source for a known site is one SOURCES entry.

Game8 uses at least three templates. This handles label/value detail
tables (Genshin, 9 events) and column tables (NTE, 13 events). Its
image-grid schedule has no year and no end date, so Arknights: Endfield
is not supportable from that source and gets no adapter.

canParse is a structural tripwire: a site redesign fails the run loudly
instead of publishing an empty calendar that reads as "no events".

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 00:10:53 +02:00
co-authored by Claude Opus 5
parent caefe6b0d7
commit 489ce92ef5
12 changed files with 9274 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
/**
* Run an adapter against a checked-in fixture and print the events it yields.
*
* Offline and free — this never touches the network. Use it to eyeball an
* adapter's output before writing the .expected.json, and to regenerate that
* file after an intentional change.
*
* bun run parse <adapter-id> <fixture-path> [--now ISO] [--json]
*/
import { adapterById, ADAPTERS } from "../src/ingest/adapters/index.ts";
import { GachaEvent } from "../src/shared/schema.ts";
const [adapterId, fixturePath] = Bun.argv.slice(2);
const nowFlag = Bun.argv.indexOf("--now");
const now =
nowFlag !== -1 ? (Bun.argv[nowFlag + 1] ?? "") : "2026-08-14T00:00:00.000Z";
const asJson = Bun.argv.includes("--json");
if (!adapterId || !fixturePath) {
console.error("usage: bun run parse <adapter-id> <fixture-path> [--now ISO] [--json]");
console.error(`adapters: ${ADAPTERS.map((a) => a.id).join(", ")}`);
process.exit(1);
}
const adapter = adapterById(adapterId);
if (!adapter) {
console.error(`unknown adapter '${adapterId}'`);
console.error(`adapters: ${ADAPTERS.map((a) => a.id).join(", ")}`);
process.exit(1);
}
const html = await Bun.file(fixturePath).text();
const events = adapter.parse(html, {
now,
sourceUrl: adapter.url,
sourceId: adapter.id,
game: adapter.game,
});
// Validate here too — the script should fail loudly on a schema violation
// rather than print something the pipeline would later reject.
for (const e of events) GachaEvent.parse(e);
if (asJson) {
console.log(JSON.stringify(events, null, 2));
} else {
console.log(`${adapter.id}: ${events.length} events\n`);
for (const e of events) {
const end = e.endsAt ? e.endsAt.slice(0, 10) : " (unknown)";
console.log(
` ${e.startsAt.slice(0, 10)}${end} [${e.type.padEnd(11)}] ${e.title}`,
);
}
}