Files
gacha-event-tracker/fixtures/r1999/fandom-events-2026-08-17.expected.json
Lucas WintherandClaude Opus 5 3377fcb211 feat(ingest): add the Reverse: 1999 event source via Fandom's API
New `fandom` parser plus the r1999-fandom-events source, giving the game
six live and upcoming events at exact precision.

It fetches api.php?action=parse, not /wiki/Events. The rendered page
answers a non-browser client with a Cloudflare challenge, and getting past
that would be defeating an access control — the reason uma.moe was
declined. The wiki's robots.txt instead allows /api.php?action= for *, and
that endpoint serves our real User-Agent a 200, so this reads the
sanctioned surface with our own headers and no impersonation. The body is
therefore JSON, which is what canParse checks first: a challenge page or
an error payload must fail loudly, not parse to zero events.

Two page facts shape the parser. Titles come from each row's <b>, because
a missing banner image renders as a red link reading "File:<Event>
Banner.png" that a cell-text reader would publish as the event name. And
the page is an archive of 154 rows since v1.1 with no ongoing section to
gate on, so inclusion is decided against ctx.now — the six-event count is
asserted against an independent extraction off the fixture, per
docs/INGESTION.md § Testing.

Because robots.txt is unreadable from a challenged address, the gate fails
closed in CI and the source is skipped there — a warning, not a broken
build. Refreshing it means running `bun run refresh` from an address
Fandom serves.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-17 22:46:47 +02:00

129 lines
4.2 KiB
JSON

[
{
"id": "r1999:8-bit-arcade-remix:2026-08-13",
"game": "r1999",
"title": "8-Bit Arcade Remix",
"type": "other",
"summary": "Time-Limited Events",
"startsAt": "2026-08-13T10:00:00.000Z",
"startPrecision": "exact",
"endsAt": "2026-09-24T09:59:00.000Z",
"endPrecision": "exact",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://reverse1999.fandom.com/api.php?action=parse&page=Events&prop=text&formatversion=2&format=json",
"sourceId": "r1999-fandom-events",
"status": "published",
"confidence": 0.95,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "r1999:on-anothers-sorrow:2026-08-13",
"game": "r1999",
"title": "On Another's Sorrow",
"type": "other",
"summary": "Version Events",
"startsAt": "2026-08-13T10:00:00.000Z",
"startPrecision": "exact",
"endsAt": "2026-09-21T09:59:00.000Z",
"endPrecision": "exact",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://reverse1999.fandom.com/wiki/On_Another%27s_Sorrow",
"sourceId": "r1999-fandom-events",
"status": "published",
"confidence": 0.95,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "r1999:the-you-thats-meant-to-be:2026-08-13",
"game": "r1999",
"title": "The You That's Meant to Be",
"type": "story",
"summary": "Character Story Events",
"startsAt": "2026-08-13T10:00:00.000Z",
"startPrecision": "exact",
"endsAt": "2026-09-24T09:59:00.000Z",
"endPrecision": "exact",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://reverse1999.fandom.com/api.php?action=parse&page=Events&prop=text&formatversion=2&format=json",
"sourceId": "r1999-fandom-events",
"status": "published",
"confidence": 0.95,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "r1999:truth-a-posteriori:2026-08-22",
"game": "r1999",
"title": "Truth a Posteriori",
"type": "other",
"summary": "Side Events",
"startsAt": "2026-08-22T10:00:00.000Z",
"startPrecision": "exact",
"endsAt": "2026-09-24T09:59:00.000Z",
"endPrecision": "exact",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://reverse1999.fandom.com/api.php?action=parse&page=Events&prop=text&formatversion=2&format=json",
"sourceId": "r1999-fandom-events",
"status": "published",
"confidence": 0.95,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "r1999:a-stranger-to-memory-lane:2026-09-03",
"game": "r1999",
"title": "A Stranger to Memory Lane",
"type": "story",
"summary": "Character Story Events",
"startsAt": "2026-09-03T10:00:00.000Z",
"startPrecision": "exact",
"endsAt": "2026-09-24T09:59:00.000Z",
"endPrecision": "exact",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://reverse1999.fandom.com/api.php?action=parse&page=Events&prop=text&formatversion=2&format=json",
"sourceId": "r1999-fandom-events",
"status": "published",
"confidence": 0.95,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "r1999:what-does-a-brick-want:2026-09-17",
"game": "r1999",
"title": "What Does a Brick Want",
"type": "other",
"summary": "Warm Up Events",
"startsAt": "2026-09-17T10:00:00.000Z",
"startPrecision": "exact",
"endsAt": "2026-09-24T09:59:00.000Z",
"endPrecision": "exact",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://reverse1999.fandom.com/api.php?action=parse&page=Events&prop=text&formatversion=2&format=json",
"sourceId": "r1999-fandom-events",
"status": "published",
"confidence": 0.95,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
}
]