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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-16 06:20:22 +02:00
1 parent 0a4be4e339
commit 9f8150fda7
7 files changed
+76 -6

No files matched your search

+9
View File
@@ -89,6 +89,7 @@ Two things this method cannot tell you, and both matter:
| Silver Palace | — | — | — | Unreleased; beta only | | 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 | | 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 | | 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 | | 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). **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 ## What every one of these costs, beyond the source
Adding a game is never only a `SOURCES` entry: 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"
}
]
@@ -0,0 +1 @@
{"parse":{"title":"Miliastra Pass","pageid":409781,"text":"<div class=\"mw-content-ltr mw-parser-output\" lang=\"en\" dir=\"ltr\"><p class=\"mw-empty-elt\">\n</p><aside role=\"region\" class=\"portable-infobox pi-background pi-border-color pi-theme-wikia pi-layout-default\">\n<h2 class=\"pi-item pi-item-spacing pi-title pi-secondary-background\" data-source=\"title\">Miliastra Pass</h2>\n\n<figure class=\"pi-item pi-image\" data-source=\"image\">\n\t<a href=\"https://static.wikia.nocookie.net/gensin-impact/images/d/db/Icon_Miliastra_Pass.png/revision/latest?cb=20260115163308\" class=\"image image-thumbnail\" title=\"Icon Miliastra Pass\">\n\t\t<img src=\"https://static.wikia.nocookie.net/gensin-impact/images/d/db/Icon_Miliastra_Pass.png/revision/latest?cb=20260115163308\" srcset=\"https://static.wikia.nocookie.net/gensin-impact/images/d/db/Icon_Miliastra_Pass.png/revision/latest?cb=20260115163308 1x, https://static.wikia.nocookie.net/gensin-impact/images/d/db/Icon_Miliastra_Pass.png/revision/latest?cb=20260115163308 2x\" class=\"pi-image-thumbnail\" data-relevant=\"1\" alt=\"Icon Miliastra Pass\" width=\"96\" height=\"96\" data-image-key=\"Icon_Miliastra_Pass.png\" data-image-name=\"Icon Miliastra Pass.png\" />\n\t\t\n\t</a>\n\t\n</figure>\n\n<div class=\"pi-item pi-data pi-item-spacing pi-border-color\" data-source=\"type\">\n\t\n\t\t<h3 class=\"pi-data-label pi-secondary-font\">Type</h3>\n\t\n\t<div class=\"pi-data-value pi-font\"><a href=\"/wiki/Game_Systems\" class=\"mw-redirect\" title=\"Game Systems\">Game Systems</a></div>\n</div>\n\n<div class=\"pi-item pi-data pi-item-spacing pi-border-color\" data-source=\"group\">\n\t\n\t\t<h3 class=\"pi-data-label pi-secondary-font\">Group</h3>\n\t\n\t<div class=\"pi-data-value pi-font\"><a href=\"/wiki/Miliastra_Wonderland\" title=\"Miliastra Wonderland\">Miliastra Wonderland</a><br /><a href=\"/wiki/Monetization\" title=\"Monetization\">Monetization</a><br /><a href=\"/wiki/Menus\" class=\"mw-redirect\" title=\"Menus\">Menus</a><br /><a href=\"/wiki/Recurring_Events\" class=\"mw-redirect\" title=\"Recurring Events\">Recurring Events</a></div>\n</div>\n</aside>\n\n<p class=\"mw-empty-elt\"></p><p><b>Miliastra Pass</b> is a recurring <a href=\"/wiki/Event\" title=\"Event\">Event</a> for <a href=\"/wiki/Miliastra_Wonderland\" title=\"Miliastra Wonderland\">Miliastra Wonderland</a>. It is a <a href=\"/wiki/Battle_Pass\" title=\"Battle Pass\">Battle Pass</a> that uses <a href=\"/wiki/Chronicle_EXP\" title=\"Chronicle EXP\">Chronicle EXP</a> gained from weekly and BP period quests to reward <span class=\"item\"><span class=\"item-image\"><span class=\"hidden\" data-size=\"20\" style=\"height:20px;width:20px\"><span typeof=\"mw:File\"><a href=\"/wiki/Geode_of_Replication\" title=\"Geode of Replication\"><img alt=\"Geode of Replication\" src=\"data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D\" decoding=\"async\" loading=\"lazy\" width=\"20\" height=\"20\" class=\"mw-file-element lazyload\" data-image-name=\"Item Geode of Replication.png\" data-image-key=\"Item_Geode_of_Replication.png\" data-relevant=\"1\" data-src=\"https://static.wikia.nocookie.net/gensin-impact/images/a/a9/Item_Geode_of_Replication.png/revision/latest/scale-to-width-down/20?cb=20251022043930\" /></a></span></span><span class=\"mobile-only\"><span typeof=\"mw:File\"><a href=\"/wiki/Geode_of_Replication\" title=\"Geode of Replication\"><img alt=\"Geode of Replication\" src=\"data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D\" decoding=\"async\" loading=\"lazy\" width=\"30\" height=\"30\" class=\"mw-file-element lazyload\" data-image-name=\"Item Geode of Replication.png\" data-image-key=\"Item_Geode_of_Replication.png\" data-relevant=\"1\" data-src=\"https://static.wikia.nocookie.net/gensin-impact/images/a/a9/Item_Geode_of_Replication.png/revision/latest/scale-to-width-down/30?cb=20251022043930\" /></a></span></span></span><span class=\"item-text\"> <a href=\"/wiki/Geode_of_Replication\" title=\"Geode of Replication\">Geodes of Replication</a></span></span>, <a href=\"/wiki/Cosmetic\" title=\"Cosmetic\">Cosmetics</a>, <a href=\"/wiki/Interactions/Expression\" title=\"Interactions/Expression\">Interactive Expressions</a>, <a href=\"/wiki/Lustrous_Materials\" title=\"Lustrous Materials\">Lustrous Materials</a>, <a href=\"/wiki/Lobby_Facilities\" class=\"mw-redirect\" title=\"Lobby Facilities\">Lobby Facilities</a>, and <a href=\"/wiki/Chronicle_Caches\" class=\"mw-redirect\" title=\"Chronicle Caches\">Chronicle Caches</a>. It includes the Chronicles of the Sky (basic edition), Chronicles of the Realms, and Epic of the Realms.\n</p>\n<div id=\"toc\" class=\"toc\" role=\"navigation\" aria-labelledby=\"mw-toc-heading\"><input type=\"checkbox\" role=\"button\" id=\"toctogglecheckbox\" class=\"toctogglecheckbox\" style=\"display:none\" /><div class=\"toctitle\" lang=\"en\" dir=\"ltr\"><h2 id=\"mw-toc-heading\">Contents</h2><span clasLine truncated
+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", url: "https://honkai-star-rail.fandom.com/api.php?action=parse&page=Nameless_Honor&prop=text&formatversion=2&format=json",
parserId: "fandomtable", 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", id: "r1999-fandom-events",
game: "r1999", game: "r1999",
+6
View File
@@ -52,6 +52,12 @@ export const PASS_SOURCES: Readonly<Record<string, PassSource>> = {
pass: "Nameless Honor", pass: "Nameless Honor",
title: () => "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. */ /** 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); 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[] = []; const accepted: string[] = [];
for (const f of files) { for (const f of files) {
if (fandomTableParser.canParse(await Bun.file(f).text())) accepted.push(f); 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 () => { test("no other parser claims a pass-history fixture", async () => {
const text = await Bun.file(FIXTURE).text(); for (const f of [
const claimants = PARSERS.filter((p) => p.canParse(text)).map((p) => p.id); FIXTURE,
expect(claimants).toEqual(["fandomtable"]); "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 // 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). // 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" }, { 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) { async function runAdapter(adapter: Adapter, fixture: string, now: string = NOW) {