feat(sources): Genshin's Miliastra Pass windows from Fandom

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-16 06:20:22 +02:00
co-authored by Claude Opus 5.5
parent 0a4be4e339
commit 9f8150fda7
7 changed files with 76 additions and 6 deletions
+9
View File
@@ -89,6 +89,7 @@ Two things this method cannot tell you, and both matter:
| Silver Palace | — | — | — | Unreleased; beta only |
| Genshin Impact (Fandom) | `genshin-impact.fandom.com/api.php?…page=Event` | `fandom`, fifth template | yes (via `api.php`) | **BUILT** 2026-09-12 — adds 11 events Game8 missed; Game8 priority preserves localStorage IDs |
| Star Rail — Nameless Honor | `honkai-star-rail.fandom.com/api.php?…page=Nameless_Honor` | new (`fandomtable`) | yes (via `api.php`) | **BUILT** 2026-10-16 — the pass window as one `other` event, day precision |
| Genshin — Miliastra Pass | `genshin-impact.fandom.com/api.php?…page=Miliastra_Pass` | existing (`fandomtable`) | yes (via `api.php`) | **BUILT** 2026-10-16 — the live pass run as one `other` event, day precision |
| hololive Dreams | `holodori.wiki/wiki/Events` (still) | `holodoriwiki` | yes, but nothing new to fetch | **LAPSED** 2026-10-14 — NamuWiki is behind a Cloudflare challenge, the official site posts no events, Game8 has no page |
---
@@ -869,6 +870,14 @@ passed. A `fetched` with more than zero events means the wiki is back.
**Reuse:** the parser is `fandomtable` (`src/ingest/parsers/fandomTable.ts`). The next pass pages (ZZZ `New_Eridu_City_Fund`, Genshin `Miliastra_Pass`) are one `PASS_SOURCES` entry each (title as a function of the row's Name cell) plus a `SOURCES` entry; columns are found by heading. A table headed with `Duration` is deliberately not this template (it is a version table).
## 18. Genshin — Miliastra Pass window (Fandom) — BUILT 2026-10-16
**Source:** `https://genshin-impact.fandom.com/api.php?action=parse&page=Miliastra_Pass&prop=text&formatversion=2&format=json` (page `https://genshin-impact.fandom.com/wiki/Miliastra_Pass`), source id `genshin-fandom-miliastra-pass`. Second pass-window source; no parser change.
**What it is:** the first table on the page, `Image | Name | Start | End | Version`, 9 rows (Awakening Chronicle, 2025-10-22, to Silvery Moonlight Chronicle, 2026-09-23 to 2026-11-02; counted independently of the parser). Each row not ended by `ctx.now` becomes one `other` event titled `Miliastra Pass: <name>`, day precision. At capture one is live.
**Conduct:** the `RobotsCache` with the real User-Agent got `403` (the interstitial challenge) for `robots.txt` on 2026-10-16, so this host rests on the person-recorded reading for `genshin-fandom-events` and the standing `--assume-robots-on-403` decision. The API answered `200` to the real User-Agent for the single fixture fetch.
## What every one of these costs, beyond the source
Adding a game is never only a `SOURCES` entry:
@@ -0,0 +1,23 @@
[
{
"id": "genshin:miliastra-pass-silvery-moonlight-chronicle:2026-09-23",
"game": "genshin",
"title": "Miliastra Pass: Silvery Moonlight Chronicle",
"type": "other",
"summary": null,
"startsAt": "2026-09-23T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-11-02T00:00:00.000Z",
"endPrecision": "day",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://genshin-impact.fandom.com/wiki/Miliastra_Pass/2026-09-23",
"sourceId": "genshin-fandom-miliastra-pass",
"status": "published",
"confidence": 0.85,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-10-16T00:00:00.000Z",
"updatedAt": "2026-10-16T00:00:00.000Z"
}
]
File diff suppressed because one or more lines are too long
+21
View File
@@ -137,6 +137,27 @@ const SOURCES: SourceSpec[] = [
url: "https://honkai-star-rail.fandom.com/api.php?action=parse&page=Nameless_Honor&prop=text&formatversion=2&format=json",
parserId: "fandomtable",
},
{
id: "genshin-fandom-miliastra-pass",
game: "genshin",
// The API, as every Fandom source (AGENTS.md § Fandom). This host's
// robots.txt is the one a person recorded for `genshin-fandom-events`
// above (standard Fandom file: `Allow: /api.php?action=` for
// `User-agent: *`, no `Disallow: /` for `*`). Re-checked with the runner's
// own client (`RobotsCache`, the real User-Agent) on 2026-10-16: it
// answered 403 (the interstitial challenge), so that person-recorded
// reading and the standing `--assume-robots-on-403` decision
// (AGENTS.md § Scraping conduct) are what cover this source, as they do
// the other Fandom sources. The API path itself answered 200 to the real
// User-Agent for the single fixture fetch; no browser-shaped headers.
//
// A pass history, not an event page: the same `Name | Start | End |
// Version` table as Nameless Honor, day precision, one row per run. Read by
// the `fandomtable` parser; the title (`Miliastra Pass: <name>`) comes from
// `PASS_SOURCES` in `parsers/fandomTable.ts`, keyed by this id.
url: "https://genshin-impact.fandom.com/api.php?action=parse&page=Miliastra_Pass&prop=text&formatversion=2&format=json",
parserId: "fandomtable",
},
{
id: "r1999-fandom-events",
game: "r1999",
+6
View File
@@ -52,6 +52,12 @@ export const PASS_SOURCES: Readonly<Record<string, PassSource>> = {
pass: "Nameless Honor",
title: () => "Nameless Honor",
},
"genshin-fandom-miliastra-pass": {
// The Name cell is the run's own chronicle name (`Silvery Moonlight
// Chronicle`), a new one each version, so the title carries it.
pass: "Miliastra Pass",
title: (rowName) => `Miliastra Pass: ${rowName}`,
},
};
/** The columns a pass table is recognised by, as normalised heading text. */
+14 -6
View File
@@ -111,17 +111,25 @@ describe("fandomtable canParse across every fixture", () => {
expect(files.length).toBeGreaterThan(20);
});
test("true on the Nameless Honor fixture only", async () => {
test("true on the two pass-history fixtures only", async () => {
const accepted: string[] = [];
for (const f of files) {
if (fandomTableParser.canParse(await Bun.file(f).text())) accepted.push(f);
}
expect(accepted).toEqual([FIXTURE]);
expect(accepted).toEqual([
"fixtures/genshin/fandom-miliastra-pass-2026-10-16.html",
FIXTURE,
]);
});
test("no other parser claims the Nameless Honor fixture", async () => {
const text = await Bun.file(FIXTURE).text();
const claimants = PARSERS.filter((p) => p.canParse(text)).map((p) => p.id);
expect(claimants).toEqual(["fandomtable"]);
test("no other parser claims a pass-history fixture", async () => {
for (const f of [
FIXTURE,
"fixtures/genshin/fandom-miliastra-pass-2026-10-16.html",
]) {
const text = await Bun.file(f).text();
const claimants = PARSERS.filter((p) => p.canParse(text)).map((p) => p.id);
expect(claimants).toEqual(["fandomtable"]);
}
});
});
+2
View File
@@ -73,6 +73,8 @@ const CASES: Array<{ adapter: Adapter; fixture: string; now?: string }> = [
// Pinned at the capture day, not NOW: the page is a history, so which rows
// survive depends on the clock, and at capture exactly one does (4.6).
{ adapter: adapter("hsr-fandom-nameless-honor"), fixture: "fixtures/hsr/fandom-nameless-honor-2026-10-16", now: "2026-10-16T00:00:00.000Z" },
// Genshin's Miliastra Pass history (same reader).
{ adapter: adapter("genshin-fandom-miliastra-pass"), fixture: "fixtures/genshin/fandom-miliastra-pass-2026-10-16", now: "2026-10-16T00:00:00.000Z" },
];
async function runAdapter(adapter: Adapter, fixture: string, now: string = NOW) {