diff --git a/docs/SOURCES.md b/docs/SOURCES.md index 599bd98..89ddc1a 100644 --- a/docs/SOURCES.md +++ b/docs/SOURCES.md @@ -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: `, 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: diff --git a/fixtures/genshin/fandom-miliastra-pass-2026-10-16.expected.json b/fixtures/genshin/fandom-miliastra-pass-2026-10-16.expected.json new file mode 100644 index 0000000..3275bf3 --- /dev/null +++ b/fixtures/genshin/fandom-miliastra-pass-2026-10-16.expected.json @@ -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" + } +] diff --git a/fixtures/genshin/fandom-miliastra-pass-2026-10-16.html b/fixtures/genshin/fandom-miliastra-pass-2026-10-16.html new file mode 100644 index 0000000..5599339 --- /dev/null +++ b/fixtures/genshin/fandom-miliastra-pass-2026-10-16.html @@ -0,0 +1 @@ +{"parse":{"title":"Miliastra Pass","pageid":409781,"text":"

\n

\n\n

Miliastra Pass is a recurring Event for Miliastra Wonderland. It is a Battle Pass that uses Chronicle EXP gained from weekly and BP period quests to reward \"Geode\"Geode Geodes of Replication, Cosmetics, Interactive Expressions, Lustrous Materials, Lobby Facilities, and Chronicle Caches. It includes the Chronicles of the Sky (basic edition), Chronicles of the Realms, and Epic of the Realms.\n

\n\n\n

Overview[]

\n

The Miliastra Pass contains a total of 30 Chronicle Levels, with each level up requiring 1,000 Chronicle EXP. Upon reaching each level, players will unlock rewards depending on their Miliastra Pass type. \"Chronal\"Chronal Chronal Nexus ×150 can be used to purchase a level and to level up immediately.\n

There is a weekly Chronicle EXP cap of 5,000, which resets on 4 AM server time each Monday. The pass period missions do not count towards the EXP cap.\n

Purchasing the Chronicles of the Realms or Epic of the Realms packages allows the player to claim rewards from all previously unlocked levels in the current Miliastra Pass period. These packages must be re-purchased every new pass period.\n

\n

Boon[]

\n

After unlocking \"Chronicles of the Realms\" or \"Epic of the Realms,\" the player will unlock the Battle Pass Boon effect, granting a discount when purchasing the corresponding Battle Pass in Teyvat. Likewise, unlocking \"Gnostic Hymn\" or \"Gnostic Chorus\" will grant the Chronicle Pass Boon effect, offering a discount when purchasing the corresponding Miliastra Pass in Miliastra Wonderland. The player can check the Boon button on the Miliastra Pass or Battle Pass purchase screen for more details. The Boon effect can only be activated during the current event period.\n

\n

Chronicles of the Realms[]

\n

Chronicles of the Realms can be purchased for $5.00 USD and will unlock a second tier for the current BP Period, containing enhanced general rewards and Star Tokens.\n

\n

Epic of the Realms[]

\n

Epic of the Realms can be purchased for $14.99 USD (or $9.99 USD if the player has already purchased Chronicles of the Realms). It will advance the Chronicle Level by 5, and contains a Miliastra Pass exclusive Collection Template.\n

\n

History[]

\n

Miliastra Pass has 9 occurrences:\n

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Image\nName\nStart\nEnd\nVersion\n
\"Awakening\nAwakening Chronicle\nOctober 22, 2025\nDecember 1, 2025\nLuna II\n
\"Snowveil\nSnowveil Chronicle\nDecember 3, 2025\nJanuary 12, 2026\nLuna III\n
\"Auspicious\nAuspicious Chronicle\nJanuary 14, 2026\nFebruary 23, 2026\nLuna IV\n
\"Cloudglow\nCloudglow Chronicles\nFebruary 25, 2026\nApril 6, 2026\nLuna V\n
\"Scaled\nScaled Edge Chronicles\nApril 8, 2026\nMay 18, 2026\nLuna VI\n
\"Prismatic\nPrismatic Chronicles\nMay 20, 2026\nJune 29, 2026\nLuna VII\n
\"Chronicle\nChronicle of the Fatebound Sea\nJuly 1, 2026\nAugust 10, 2026\nLuna VIII\n
\"Phantasmagoric\nPhantasmagoric Chronicle\nAugust 12, 2026\nSeptember 21, 2026\n7.0\n
\"Silvery\nSilvery Moonlight Chronicle\nSeptember 23, 2026\nNovember 2, 2026\n7.1\n
\n

Other Languages[]

\n\n
LanguageOfficial NameLiteral Meaning
EnglishMiliastra Pass—
Chinese
(Simplified)
纪游
Jìyóu
Chinese
(Traditional)
紀遊
Jìyóu
Japanese遊記
Yuuki‍‍[!]Assumed reading‍
Korean유람
Yuram
SpanishPase Miliastra
FrenchChroniquesChronicles
RussianЛетопись
Letopis'
Thaiบันทึกพันดารา
VietnameseSử Ký
GermanChronik
IndonesianMiliastra Pass—
PortuguesePasse Miliastra
TurkishBin Yıldız Bileti
ItalianPass Miliastra
\n

Change History[]

\n
Version 7.1\n
  • Adjusted the Weeky quests:\n
    • Old: \"Complete 5/10/15 matches,\" with each mission awarding 1,000 Chronicle EXP
    • \n
    • New: \"Complete 5 matches or play for 20 minutes,\" \"Complete 10 matches or play for 40 minutes,\" and \"Complete 15 matches or play for 60 minutes,\" each of which awards 1,000 Chronicle EXP.
\n
\n

Version \"Luna VII\"\n

\n
  • Adjusted the Chronicles of the Realms quest:\n
    • Old: Earn 5,000 EXP from this week's missions to obtain Geodes of Replication ×800
    • \n
    • New: Earn 1,000/2,000/3,000/4,000/5,000 EXP from this week's missions to obtain Geodes of Replication ×160.
\n
\n

Version \"Luna IV\"\n

\n
  • Updated icon:
\n
\n

Version \"Luna II\"\n

\n
  • Miliastra Pass was released.
\n

Navigation[]

\n
\n\n\n\n\n
"}} \ No newline at end of file diff --git a/src/ingest/adapters/index.ts b/src/ingest/adapters/index.ts index 6fa41cc..5d9d522 100644 --- a/src/ingest/adapters/index.ts +++ b/src/ingest/adapters/index.ts @@ -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: `) 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", diff --git a/src/ingest/parsers/fandomTable.ts b/src/ingest/parsers/fandomTable.ts index 5c987ff..2c4899d 100644 --- a/src/ingest/parsers/fandomTable.ts +++ b/src/ingest/parsers/fandomTable.ts @@ -52,6 +52,12 @@ export const PASS_SOURCES: Readonly> = { 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. */ diff --git a/test/adapters/fandomTable.test.ts b/test/adapters/fandomTable.test.ts index ccb60a2..a6a1688 100644 --- a/test/adapters/fandomTable.test.ts +++ b/test/adapters/fandomTable.test.ts @@ -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"]); + } }); }); diff --git a/test/adapters/game8.test.ts b/test/adapters/game8.test.ts index 12b3e3d..37300c7 100644 --- a/test/adapters/game8.test.ts +++ b/test/adapters/game8.test.ts @@ -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) {