From 489ce92ef5b122daf7eed0a7cccac20785037675 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Sat, 15 Aug 2026 00:10:53 +0200 Subject: [PATCH] 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) --- .../endfield/game8-events-2026-08-14.html | 2495 +++++++++++++ .../game8-events-2026-08-14.expected.json | 191 + fixtures/genshin/game8-events-2026-08-14.html | 2512 +++++++++++++ .../nte/game8-events-2026-08-14.expected.json | 275 ++ fixtures/nte/game8-events-2026-08-14.html | 3114 +++++++++++++++++ scripts/parse-fixture.ts | 54 + src/ingest/adapters/index.ts | 112 + src/ingest/adapters/types.ts | 38 + src/ingest/parsers/game8.ts | 274 ++ src/ingest/parsers/index.ts | 16 + src/ingest/parsers/types.ts | 30 + test/adapters/game8.test.ts | 163 + 12 files changed, 9274 insertions(+) create mode 100644 fixtures/endfield/game8-events-2026-08-14.html create mode 100644 fixtures/genshin/game8-events-2026-08-14.expected.json create mode 100644 fixtures/genshin/game8-events-2026-08-14.html create mode 100644 fixtures/nte/game8-events-2026-08-14.expected.json create mode 100644 fixtures/nte/game8-events-2026-08-14.html create mode 100644 scripts/parse-fixture.ts create mode 100644 src/ingest/adapters/index.ts create mode 100644 src/ingest/adapters/types.ts create mode 100644 src/ingest/parsers/game8.ts create mode 100644 src/ingest/parsers/index.ts create mode 100644 src/ingest/parsers/types.ts create mode 100644 test/adapters/game8.test.ts diff --git a/fixtures/endfield/game8-events-2026-08-14.html b/fixtures/endfield/game8-events-2026-08-14.html new file mode 100644 index 0000000..ceaf216 --- /dev/null +++ b/fixtures/endfield/game8-events-2026-08-14.html @@ -0,0 +1,2495 @@ +All Events and Schedule | Arknights: Endfield|Game8 + + + + + + + + +

What can you do as a free member?

Member benefits illustration

Create your free account today and unlock all our premium features and tools to enhance your gaming experience.

Member benefits illustration

Create your free account today and save articles to your watchlist and get notified when they're updated with new information.

Member benefits illustration

Create your free account today and save your favorite games for quick access later, synced across all your devices.

Member benefits illustration

By creating a Game8 account and logging in, you'll receive instant notifications when someone replies to your posts.

Comment rating feature illustration

By creating a Game8 account and logging in, you can make use of convenient features in the comments section, such as rating and sorting comments.

Premium archive feature illustration

By creating a Game8 account and logging in, you can access Premium articles that are exclusively available to members.

Map tool feature illustration

By creating a Game8 account and logging in, the items you checked on the map tool will be saved.

Site Interface

Guest
Free Member
Article Watchlist
Game Bookmarks
Cross-device Sync
Light/Dark Theme Toggle
User Profiles
Direct Feedback
Comment Rating
Comment Notification
Premium Articles

Game Tools

Guest
Free Member
Interactive Map Access
Interactive Map Pins
Interactive Map Comments
Interactive Map Pins Cross-Device
Check List
Event Choice Checker
Deck Builder Cross-Device
Message Board Notification
Message Board Cross-Device
Build Planner
Stat Calculator
Diagnostic Tool
Weapon/Armor Wishlist
Team Builder
Character Scoring
Tier List Maker

Want more information?Learn more

+
Arknights: Endfield

All Events and Schedule

+ + +Add to Watchlist (5)
+

✪ Check Game8 Endfield Twitter for daily tips!
★ Exclusive: Endfield's Connection to Arknights
✰ Phase 2 of Version 1.4 is now out!
The Rooted Realm | HEAT RAGE! MEGA ARENA!
✰ 1.4 P1: Arcane ( Builds | Teams | Banner )
★ 1.4 P2: Liino ( Builds | Teams | Banner )

+ +
Arknights Endfield All Events and Schedule
+ +

Check out all of Arknights: Endfield's current event schedule, upcoming, and past events here!

+ + + + + + + + + + + + + + + + + +
Jump to a Free Rewards Page!
All Endfield CodesPre-Register Rewards
Daily Sign-In EventAll Web Events
All Events
+ + +

Arknights Endfield Events Calendar

All Endfield Events and Dates

+
+

Arknights Endfield Current Events

Endfield 1.4 Events Schedule

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+Echoes of War
(Version 1.4)
07/16
+
+
+Fortune Connect-and-Win
(Version 1.4)
07/16/26 - 07/31/26
+
+
+Tribute of Companionship
(Version 1.4)
07/16/26 - 08/09/26
+
+
+Gaze North to the Rift Sign-In
(Version 1.4)
07/16/26 - 08/09/26
+
+
+Gaze Towards the Northern Exclusion
(Version 1.4)
07/16/26 - 08/09/26
+
+
+Bedazzling Dawnstar Sign-In
(Version 1.4)
08/09/26 - 08/30/26
+
+
+Heat Rage! Mega Arena!
(Version 1.4)
07/30/26 - 08/13/26
+
+
+The Rooted Realm
(Version 1.4)
08/09/26 - 08/30/26
+

Upcoming Endfield Events

Endfield 1.4 Upcoming Events

+ + + + + + + + + + + + +
EventSchedule & Summary
+ Arknights Endfield The Rooted Realm Guide The Rooted Realm + + +Period: 08/09/26 - 08/30/26
During the event, gather Aurylene Fragments and complete Realm Quests to claim generous rewards.
+ Arknights Endfield Bedazzling Dawnstar Sign-In Guide Bedazzling Dawnstar Sign-In + + +Period: 08/09/26 - 08/30/26
Sign-in to get extra pulls for Liino!
+

Arknights Endfield Permanent Events

All Permanent Events

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+Awakening Sign-In
(Version 1.0)
01/22
+
+
+New Horizons
(Version 1.0)
01/22
+
+
+Authority Level Rewards
(Version 1.0)
01/22
+
+
+Protocol Reconnection
(Version 1.0)
+
+
+
+Operator Progression Booster
(Version 1.2)
04/17
+
+
+Sanity Supply
(Version 1.4)
08/26/26 - 09/02/2026
+

Awakening Sign-In Event

+ + + + + + + + +
Arknights Endfield Awakening Sign-In Beginner Event  Guide
DurationPermanently Available
+ +

Once you unlock the Event Center, log into the game for 14 days.

+ +

Awakening Sign-In Guide

+

New Horizons Beginner Event

+ + + + + + + + +
Arknights Endfield New Horizons Beginner Event  Guide
DurationPermanently Available
+ +

Complete the designated main missions to earn pulls on the New Horizons Headhunting Banner!

+ +

New Horizons Guide

+

Authority Level Rewards Event

+ + + + + + + + +
Arknights Endfield Authority Level Rewards Beginner Event  Guide
DurationPermanently Available
+ +

Claim exclusive rewards upon reaching specific Authority Levels!

+ +

Authority Level Rewards Guide

+

Protocol Reconnection Event

+ + + + + + + + +
Arknights Endfield Protocol Reconnection Sign In  Guide
DurationPermanently Available
+ +

Log into the game every day after returning to Arknights: Endfield to receive welcome back gifts!

+ +

Protocol Reconnection Guide

+

Operator Progression Booster Event

+ + + + + + + + +
Arknights Endfield Operator Progression Booster Beginner Event  Guide
DurationPermanently Available
+ +

Permanent Booster for Beginners that refunds a great amount of materials while upgrading.

+ +

Operator Progression Booster Guide

+

Other Endfield Event Guides

Other Event Guides

+ + + + + + + + +
Other Event Guides
Arknights Endfield All Web Event GuidesAll Web Event GuidesDaily Sign-In GuideDaily Sign-In Event Guide
+

Past Endfield Events

All Endfield Past Events

+ + + + + + + + + + + + + + + + + + + + + +
VersionPast Events
Ver 1.3 + +◆ Jul. 02 - Carly's Fairytale of Wuling Camera Event
+ + + +◆ Jun. 26 - Peacekeeper's Nocturne Sign-In Sign In
+ + + +◆ Jun. 12 - Trial Algorithmics Reward Event
+ + + +◆ Jun. 12 - Contingency Readiness Sanity Event
+ + + +◆ Jun. 05 - One with Her Fists Sign-In Sign In
+ + + +◆ Jun. 05 - Search for Sword Vault Dale Guide Event
+ + + +
Ver 1.2 + +◆ May. 14 - Curious Use of Xiranite Guide Event
+ + + +◆ May. 14 - Supply - Wuling Super Chef Fun and Games Event
+ + + +◆ May. 14 - Shimmering Moment Sign-In
+ + + +◆ May. 29 - Curios and Myths Sign-In Sign In
+ + + +◆ May. 01 - Party Together Pastry Tower Web Event
+ + + +◆ Apr. 28 - AIC Support - Palm-Top Savior AIC Production Event
+ + + +◆ Apr. 24 - Etchings of Our Cosmic Dreams Space Exploration Event
+ + + +◆ Apr. 17 - Blitz of Verdance Sign-In
+ + + +◆ Apr. 17 - Subduer of the Blighted Sea Guide Event
+ + + +
Ver 1.1 + +◆ Apr. 09 - Survival Training Challenge Event
+ + + + + ◆ Apr. 01 - Journey of the Long-Ear Celebration Event
+ + +◆ Mar. 29 - Pearl of the Pack Sign-In
+ + + +◆ Mar. 29 - Cleanse and Rinse Fun and Games Event
+ + + +◆ Mar. 19 - Supply Exploration Day Supply Event
+ + + +◆ Mar. 12 - Quietly Flows the Spring Guide Event
+ + + +◆ Mar. 12 - Gently Flows Qingbo Sign-In
+ + + +
Ver 1.0 + +◆ Mar. 02 - Sanity Discount
+ + + + + ◆ Feb. 24 - Spectral Splendor Sign In
+ + +◆ Feb. 16 - Steps to Sweet Success Mail Event
+ + + +◆ Feb. 15 - A Spring Note, Warmly Crafted Web Event
+ + + +◆ Feb. 07 - Glittering Life at Wuling Guide Event
+ + + +◆ Feb. 07 - Carly's Fairytale of the Valley Camera Event
+ + + + + ◆ Feb. 07 - Messenger Express Sign In
+ + + + ◆ Jan. 22 - Firewalker's Trail Sign In
+ + +◆ Jan. 16 - Voyage to the Stars Web Event
+ + + +◆ Jan. 07 - Final Prep Orders Web Event
+ + + +
+

Arknights: Endfield Related Guides

+

Arknights Endfield - Back to Top
Arknights: Endfield Wiki Front

+

Guides by Category

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Arknights: Endfield Categories
Arknights Endfield Characters PartialCharactersArknights Endfield - News and Game InfoNews and Game Info
Arknights Endfield - BannersBannersArknights Endfield - Events Events and Schedule
Arknights Endfield WeaponsWeaponsArknights Endfield - Gear PartialGear
Arknights Endfield - ItemsItemsArknights Endfield - MissionsMissions
Arknights Endfield Blueprints Partial BannerBlueprintsArknights Endfield - AIC FactoryAIC (Factory)
Arknights Endfield - MapsInteractive MapArknights Endfield - Umbral MonumentUmbral Monument
Arknights Endfield - Etchspace SalvageEtchspace SalvageArknights Endfield - Delver of the CrypticDelver of the Cryptic
Arknights Endfield - Contingency ContractContingency ContractArknights Endfield - Regional DevelopmentRegional Development
Arknights Endfield - BossesBossesArknights Endfield - EnemiesEnemies
Arknights Endfield - Tips and TricksTips and TricksArknights Endfield - AchievementsAchievements
Arknights Endfield - Message BoardMessage Board
+

Exclusive Guides

+
Arknights Endfield - Premium Article
+ +

Premium Articles

+ + +

All Tier Lists and Meta Guides

+ + + + + + + + + + + + + + + + +
Arknights Endfield Tier List and Best CharactersCharacters Tier ListReroll GuideReroll Guide and Tier ListArknights Endfield Best Team CompsBest Team Comps
Arknights Endfield Team Building Guide Team Building GuideArknights Endfield - All Character BuildsAll Character BuildsArknights Endfield - Best Weapons Tier ListWeapons Tier List
Arknights Endfield All Stats and ExplanationAll Stats and ExplanationArknights Endfield - Best Weapons for Each CharacterBest Weapons Per CharacterBest Gear and Gear Sets for Each Character - Arknights EndfieldBest Gear Sets Per Character
+

All Helpful Tools

+ + + + + + +
Tier List MakerTier List MakerArknights Endfield Team Builder ToolTeam Builder ToolArknights Endfield Essence CheckerWeapon Essence Checker
+

All Recommended Guides

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Arknights Endfield Gacha System, Rates, and Pity ExplainedGacha System ExplainedArknights Endfield - Protocol Pass Details and DifferencesProtocol Pass DetailsArknights Endfield Server Status and Reset Times Server Status and Times
Arknights Endfield Bugs and IssuesBugs and Known IssuesArknights Endfield All Weapon Supply Guides All Weapon Supply GuidesArknights Endfield - Imminent IncursionImminent Incursion Guide
Arknights Endfield - How to Farm Plants and Unlock PlantingHow to Farm Plants and Unlock PlantingArknights Endfield - All Races In-Game All Races In-GameArknights Endfield Is Amiya in EndfieldIs Amiya in Endfield?
Arknights Endfield Explosives Guide - Blueprints and How to Make Industrial Explosives Explosives GuideArknights Endfield - Best Banner To Pull On Best Banner To Pull OnArknights Endfield All Protocol Space LocationsAll Protocol Space Locations
Arknights Endfield How to Upgrade and Artifice GearHow to Upgrade GearArknights Endfield Gear System Crafting GuideGear System Crafting GuideArknights Endfield - Metastorage Transfer GuideMetastorage Transfer Guide
Arknights Endfield Essence Guide and How to FarmEssence Guide: How to Farm EssenceArknights Endfield - How to Upgrade EssenceHow to Upgrade EssenceArknights Endfield - All Dried Originium Slug LocationsAll Dried Originium Slug Locations
Arknights Endfield - Best Characters for Each OutpostBest Characters for Each OutpostArknights Endfield How to Farm T-CredsHow to Farm T-CredsArknights: Endfield Best Clue Room CharactersBest Clue Room Characters
Arknights Endfield - Best Characters for OMV DijiangBest Characters for OMV DijiangArknights Endfield Trial of Swordmancy Release DateTrial of Swordmancy GuideArknights Endfield Crisis Fragment Release DateCrisis Fragment Guide and Rewards
Arknights Endfield All Cosmetics All Cosmetics
+

Comment

3 Дмитрий 293 Дмитрий 293 29 days

WFFC0BV9H9REWPB3

2 Walkthrough Team Walkthrough Team@Game8 7 months

Hi! We have the SKPORT Community Sign-In Event in our Daily Sign-In Event Guide: https://game8.co/games/Arknights-Endfield/archives/574950 +We'll feature more events as they release, so make sure to sign up and bookmark the page to be notified of any updates. Thanks!

    Please participate in our site
    improvement survey
    08/2026
    Would you assist in
    improving Game8's site?
    ×
    This survey will take about 1 minute.
    Your answers will help us to improve our website.
    + + +
    We are listening to our users' valuable opinions and discussing how to act on them.
    The information collected through this survey will be used for the purpose of improving our services. In addition, the information will only be disclosed a part of a statistic in a format that will not allow identification of personal information.
    Begin Survey
    + + + + + + + + + + +

    Premium Articles

    + + +

    Rankings

    +

    All rights reserved

    + + +
    Guide Index
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/fixtures/genshin/game8-events-2026-08-14.expected.json b/fixtures/genshin/game8-events-2026-08-14.expected.json new file mode 100644 index 0000000..bc6bf1d --- /dev/null +++ b/fixtures/genshin/game8-events-2026-08-14.expected.json @@ -0,0 +1,191 @@ +[ + { + "id": "genshin:to-temper-thyself-and-journey-far:2026-08-10", + "game": "genshin", + "title": "To Temper Thyself and Journey Far", + "type": "other", + "summary": null, + "startsAt": "2026-08-10T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-11-02T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:battle-pass-frostfarer:2026-08-12", + "game": "genshin", + "title": "Battle Pass - Frostfarer", + "type": "other", + "summary": null, + "startsAt": "2026-08-12T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-21T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:character-test-runs:2026-08-12", + "game": "genshin", + "title": "Character Test Runs", + "type": "challenge", + "summary": null, + "startsAt": "2026-08-12T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-01T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:mutual-aid-in-bloom-into-the-frostlands:2026-08-12", + "game": "genshin", + "title": "Mutual Aid in Bloom: Into the Frostlands", + "type": "other", + "summary": null, + "startsAt": "2026-08-12T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-24T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:snezhnaya-travel-guide-web-event:2026-08-12", + "game": "genshin", + "title": "Snezhnaya Travel Guide Web Event", + "type": "other", + "summary": null, + "startsAt": "2026-08-12T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-25T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:stygian-onslaught:2026-08-19", + "game": "genshin", + "title": "Stygian Onslaught", + "type": "challenge", + "summary": null, + "startsAt": "2026-08-19T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-22T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:great-expeditionist-challenge:2026-08-28", + "game": "genshin", + "title": "Great Expeditionist Challenge", + "type": "challenge", + "summary": null, + "startsAt": "2026-08-28T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-14T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:trial-of-the-bastion:2026-09-07", + "game": "genshin", + "title": "Trial of the Bastion", + "type": "challenge", + "summary": null, + "startsAt": "2026-09-07T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-17T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "genshin:overflowing-abundance-rerun:2026-09-14", + "game": "genshin", + "title": "Overflowing Abundance Rerun", + "type": "rerun", + "summary": null, + "startsAt": "2026-09-14T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-21T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/301601", + "sourceId": "genshin-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + } +] diff --git a/fixtures/genshin/game8-events-2026-08-14.html b/fixtures/genshin/game8-events-2026-08-14.html new file mode 100644 index 0000000..306e828 --- /dev/null +++ b/fixtures/genshin/game8-events-2026-08-14.html @@ -0,0 +1,2512 @@ +Events Schedule and Calendar | Genshin Impact|Game8 + + + + + + + + +

    What can you do as a free member?

    Member benefits illustration

    Create your free account today and unlock all our premium features and tools to enhance your gaming experience.

    Member benefits illustration

    Create your free account today and save articles to your watchlist and get notified when they're updated with new information.

    Member benefits illustration

    Create your free account today and save your favorite games for quick access later, synced across all your devices.

    Member benefits illustration

    By creating a Game8 account and logging in, you'll receive instant notifications when someone replies to your posts.

    Comment rating feature illustration

    By creating a Game8 account and logging in, you can make use of convenient features in the comments section, such as rating and sorting comments.

    Premium archive feature illustration

    By creating a Game8 account and logging in, you can access Premium articles that are exclusively available to members.

    Map tool feature illustration

    By creating a Game8 account and logging in, the items you checked on the map tool will be saved.

    Site Interface

    Guest
    Free Member
    Article Watchlist
    Game Bookmarks
    Cross-device Sync
    Light/Dark Theme Toggle
    User Profiles
    Direct Feedback
    Comment Rating
    Comment Notification
    Premium Articles

    Game Tools

    Guest
    Free Member
    Interactive Map Access
    Interactive Map Pins
    Interactive Map Comments
    Interactive Map Pins Cross-Device
    Check List
    Event Choice Checker
    Deck Builder Cross-Device
    Message Board Notification
    Message Board Cross-Device
    Build Planner
    Stat Calculator
    Diagnostic Tool
    Weapon/Armor Wishlist
    Team Builder
    Character Scoring
    Tier List Maker

    Want more information?Learn more

    +
    Genshin Impact

    Events Schedule and Calendar

    + + +Add to Watchlist (58)
    Genshin Impact - Event Schedule and Calendar
    + +

    Here's a list of all current and upcoming Genshin Impact events. Check out all event calendar dates, schedules, and guides here!

    + + + + + + + + +
    Paimon
    Keep track of all event updates!
    Sign up or Log in and use Add to Watchlist under the page title to be notified of more updates!
    +

    Events Calendar

    +
    +

    List of Current Events

    Version 7.0 Current Events

    + + + + + + + + + + + + + + + + + +
    Current EventsDates
    +CheckmarkMutual Aid in Bloom: Into the Frostlands +
    08/12 - 08/24
    +CheckmarkSnezhnaya Travel Guide +
    08/12 - 08/25
    +CheckmarkTo Temper Thyself +
    08/10 - 11/02
    +

    Mutual Aid in Bloom: Into the Frostlands

    +
    Mutual Aid in Bloom: Into the Frostlands Event Information
    + + + + + + + + + + +
    Event StartAugust 12, 2026
    Event EndAugust 24, 2026
    + +

    Take photos and complete objectives in this Snezhnaya exploration event!

    + +

    Mutual Aid in Bloom: Into the Frostlands Event Guide

    +

    Snezhnaya Travel Guide Web Event

    +
    Snezhnaya Travel Guide Event Information
    + + + + + + + + + + +
    Event StartAugust 12, 2026
    Event EndAugust 25, 2026
    + +

    Earn rewards from progressing the 7.0 quests, initiating new combat and exploration mechanics, and connecting with other players via invitation codes!

    + +

    Snezhnaya Travel Guide Event Guide

    +

    To Temper Thyself and Journey Far

    +
    Genshin Impact - To Temper Thyself and Journey Far Free 5-star Constellation Event
    + + + + + + + + + + +
    Event Start August 10, 2026
    Event End November 2, 2026
    + +

    In the To Temper Thyself and Journey Far event, players can complete the Daily and Weekly Training Goals to claim various rewards, including a 5-Star constellation from a character of their choice in the selection pool!

    + +

    Free Constellation Event Guide

    +

    List of Upcoming Events

    Version 7.0 Upcoming Events

    + + + + + + + + + + + + + + + + + +
    Upcoming EventsDates
    +CheckmarkGreat Expeditionist Challenge +
    08/28 - 09/14
    +CheckmarkTrial of the Bastion +
    09/7 - 09/17
    +CheckmarkOverflowing Abundance +
    09/14 - 09/21
    +

    Great Expeditionist Challenge

    +
    Genshin Impact - Great Expeditionist Challenge
    + + + + + + + + + + +
    Event StartAugust 28, 2026
    Event EndSeptember 14, 2026
    + +

    Join the Adventurer's Guild Tournament to play minigames and win prizes in the 7.0 flagship event, Great Expeditionist Challenge!

    + +

    Great Expeditionist Challenge Event Guide

    +

    Trial of the Bastion

    +
    Genshin Impact - Trial of the Bastion
    + + + + + + + + + + +
    Event StartSeptember 7, 2026
    Event EndSeptember 17, 2026
    + +

    Construct mechanisms and use firearms to defend your stronghold in the base defense event, Trial of the Bastion!

    + +

    Trial of the Bastion Event Guide

    +

    Overflowing Abundance Rerun

    +
    Genshin Impact - Overflowing Abundance
    + + + + + + + + + + +
    Event StartSeptember 14, 2026
    Event EndSeptember 21, 2026
    + +

    Get double the Talent Level-Up and Weapon Ascension rewards from domains in the return of the Overflowing Abundance event!

    + +

    Overflowing Abundance Event Guide

    +

    Recurring Events

    Stygian Onslaught

    +
    Genshin Impact - Stygian Onslaught Event
    + + + + + + + + + + +
    Event Start August 19, 2026
    Event End September 22, 2026
    + +

    The Stygian Onslaught event is a recurring boss rush event where you must defeat three field bosses in different difficulties, solo or in Co-Op.

    + +

    Stygian Onslaught

    +

    Character Test Runs

    + + + + + + + + +
    Version 7.0 Phase 1 Banners
    + +Genshin Impact - SwanSwan's Shadow in Silken Ice + + + + + + +Genshin Impact - The HearthThe Hearth's Ashen Shadow + + + + +
    +

    Availability Period

    + + + + + + + + + +
    Test Run StartAugust 12, 2026
    Test Run EndSeptember 1, 2026
    + +

    The Character Test Run is available at the same time as the Character Banners in Phase 1 of Version 7.0 from August 12, 2026 to September 1, 2026!

    + +

    Version 7.0 Release Date and Characters

    +

    Characters & Rewards for this Test Run

    + + + + + + + + + + + + + + + + + + + + + +
    CharacterRewards
    + +Odette Image Odette + + + + + +
    +Genshin - Primogem Image Primogem + x20
    + + +
    + +Arlecchino Image Arlecchino + + + + + +
    +Genshin - Primogem Image Primogem + x20
    + + +
    + +Alyosha Image Alyosha + + + + + + + + +
    + +Sucrose Image Sucrose + + + + + + + + +
    + +

    Test Run Mechanics and Rewards

    +

    Battle Pass - Frostfarer

    + + + + + + + + + +
    Availability PeriodAugust 12 - September 21, 2026
    How to UnlockReach Adventure Rank 20+
    + +

    Participate in the free Sojourner's Battle Pass and the for-purchase Gnostic Hymn to receive prizes as you increase your BP Level.

    + +

    Players who reach Gnostic Hymn Level 30 will also receive a 4-star weapon of choice as a BP Bounty!

    + +

    Battle Pass Guide and All Weapons

    +

    Permanent Events

    Travelers' Tales

    +
    Genshin Impact - Travelers
    + + + + + + +
    How to UnlockReach Adventure Rank 28+
    Complete "The Many Matters Learned in One's Travels" World Quest
    + +

    Travelers' Tales is a permanent character interaction game mode added in Version 6.0 Luna I, where players can find characters in the open world and get unique interactions!

    + +

    Travelers' Tales Event Guide

    +

    Repertoire of Myriad Melodies

    +
    Genshin - Repertoire of Myriad Melodies
    + + + + + + +
    How to UnlockReach Adventure Rank 35+
    Complete "To the Winds of Freedom" World Quest
    + +

    Repertoire of Myriad Melodies is a permanent rhythm game mode added in Version 5.3, where players can choose amongst a variety of instruments and different soundtracks to play from the game!

    + +

    Repertoire of Myriad Melodies Guide

    +

    Daily Check-In

    +
    Genshin Impact - Daily Check-In Event Guide
    +

    Similar to a Web Event

    +

    Receive daily login rewards by having your account registered at the HoYoLAB Community!

    + +

    Daily Check-In Event Guide

    +

    Stellar Reunion

    +

    Genshin - Stellar Reunion Version 4.2 Update
    Stellar Reunion is an event for returnee players to Genshin Impact. Players who have not logged for at least 14 days and have not activated Stellar Reunion for at least 45 days are eligible to participate.

    + + + + + + + + + + +
    Rewards
    + +Genshin Impact - Primogem Image Primogem + + + + + + +Genshin Impact - Fragile Resin Image Fragile Resin + + + + + + +Genshin Impact - Hero Hero's Wit + + + + +
    + +

    Stellar Reunion Returnee Event Guide

    +

    Fate Foretold

    +
    Genshin - Fate Foretold Event
    + +

    The Fate Foretold listing in the Events screen allows players to check upcoming Characters and Weapons which can be obtained from Quests!

    + + + + + + + + + + + +
    Rewards
    + +Genshin Impact - Lisa ImageLisa + + + + + + +Genshin Impact - Kaeya ImageKaeya + + + + + + +Prototype Rancour Image Prototype Rancour + + + + + + +Favonius Warbow Image Favonius Warbow + + + + +
    +

    People's Choice

    +
    Genshin - People
    + + + + + + +
    How to UnlockReach Adventure Rank 20
    + +

    The People's Choice Event offers the option to unlock the 4-Star Character Xiangling as long as you complete Floor 3 Chamber 3 of Spiral Abyss.

    + +

    How to Raise Your Adventure Rank

    +

    Curtain of Leaves

    Reach Floor 4, Chamber 3 to Unlock Collei

    +
    Genshin - Curtain of Leaves Free Collei Spiral Abyss Event
    + + + + + + +
    How to UnlockReach Adventure Rank 20
    + +

    The Curtain of Leaves event offers the option to unlock the 4-Star Character Collei!To get a free copy of Collei, beat Spiral Abyss at least up to Floor 4, Chamber 3.

    + +

    Spiral Abyss Guide

    +

    List of Past Events

    Seize the Day Login Bonus

    +
    Genshin Impact - Seize the Day
    + + + + + + + + + + +
    Availability Period +2021/01/16 04:00 - 2021/01/31 03:59
    + Currently Unavailable +
    How to UnlockReach Adventure Rank 5
    + +

    The Login Bonus event was an event that appeared three times for the Version 1.1 update. It rewarded players who logged in at least 7 times consistently!

    +

    List of Login Bonuses

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Day #Reward
    Day 1
    + +Genshin Impact - Hero Hero's Wit + + + + + ×3
    Day 2
    + +Genshin Impact - Mystic Enhancement Ore Image Mystic Enhancement Ore + + + + + ×6
    Day 3
    + +Genshin Impact - Primogem Image Primogem + + + + + ×100
    Day 4
    + +Genshin Impact - Mora Image Mora + + + + + ×50,000
    Day 5
    + +Genshin Impact - Hero Hero's Wit + + + + + ×5
    Day 6
    + +Genshin Impact - Mystic Enhancement Ore Image Mystic Enhancement Ore + + + + + ×10
    Day 7
    + +Genshin Impact - Primogem Image Primogem + + + + + ×200
    +

    List of All Past Events

    +
    Genshin - List of Past Events
    + +

    To see all the events that passed in Genshin Impact check out the List of Past Events!

    + +

    List of Past Events

    +

    Version 5.2 Event Poll Results

    +
    Iktomi Spiritseeking Scrolls 17
    Votes
    +
    Adventurer's Trials: Metamorphosis 6
    Votes
    +
    Exercise Surging Storm 44
    Votes
    +
    Claw Convoy 5
    Votes
    +
    Ley Line Overflow 4
    Votes
    +

    Version 5.1 Event Poll Results

    + + + + + + + + + + + + + + + + + +
    Winners
    1st PlaceChromatic Ode of Candies and Roses
    2nd PlaceMarvelous Merchandise
    3rd PlaceFeast of Pursuit
    +

    Genshin Impact Related Guides

    Genshin Impact Wiki Guide

    +
    Genshin Impact - Wiki and Guide Top
    + +

    Genshin Impact Wiki Front Page

    +

    Guides by Category

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    All Categories
    Genshin Impact - Story WalkthroughStory WalkthroughGenshin - News and Game InfoNews & Game Info
    Genshin - List of CharactersCharactersGenshin - GachasGacha (Wishes)
    Genshin - Rerolling GuidesRerollingGenshin - All Tier ListsTier Lists
    +Genshin Impact - Redeem CodesRedeem Codes +Genshin Impact - VersionsVersions
    Genshin - Tips and TricksTips & Tricks +Genshin Impact - List of All Quest WalkthroughsQuests +
    Genshin Impact - List of WeaponsWeaponsGenshin Impact - List of ArtifactsArtifacts
    Genshin Impact - List of Bosses and Boss Battle GuidesBossesGenshin - List of EnemiesEnemies
    Genshin - List of DomainsDomainsGenshin - Items and MaterialsItems & Materials
    Genshin - List of Events and Event GuidesEventsGenshin - List of AchievementsAchievements
    Genshin Impact - Message BoardsMessage BoardsGenshin - Spiral Abyss GuidesSpiral Abyss
    Genshin - Areas and MapsAreas and Maps +Genshin - ArchiveArchive
    +Genshin Impact - Locations and LoreLocations & Lore +Genshin - HousingHousing
    Genshin Impact - Fishing System Slim BannerFishingGenshin Impact - Genius Invokation TCGGenius Invokation
    +Genshin Impact - Imaginarium TheaterImaginarium Theater Genshin - UGCMiliastra Wonderland
    Genshin Impact ReviewGame Review-
    +

    Exclusive Guides

    +
    Genshin - Premium Articles
    + +

    Premium Articles

    + + +

    Comment

    34 Anonymous Anonymous 4 months

    can i join?

    29 Anonymous Anonymous about 1 year

    Whirling walts mentioned that it ends on september in calendar but the caption says may 26

      Please participate in our site
      improvement survey
      08/2026
      Would you assist in
      improving Game8's site?
      ×
      This survey will take about 1 minute.
      Your answers will help us to improve our website.
      + + +
      We are listening to our users' valuable opinions and discussing how to act on them.
      The information collected through this survey will be used for the purpose of improving our services. In addition, the information will only be disclosed a part of a statistic in a format that will not allow identification of personal information.
      Begin Survey
      +

      All rights reserved

      + + +
      Guide Index
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      \ No newline at end of file diff --git a/fixtures/nte/game8-events-2026-08-14.expected.json b/fixtures/nte/game8-events-2026-08-14.expected.json new file mode 100644 index 0000000..9387045 --- /dev/null +++ b/fixtures/nte/game8-events-2026-08-14.expected.json @@ -0,0 +1,275 @@ +[ + { + "id": "nte:999-nights:2026-07-08", + "game": "nte", + "title": "999 Nights", + "type": "other", + "summary": "The party's assembled! Ready to conquer the Warren Continent!", + "startsAt": "2026-07-08T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:circle-gift:2026-07-08", + "game": "nte", + "title": "Circle Gift", + "type": "other", + "summary": "Log in to NTE for a week days to get 10 Solid Dice!", + "startsAt": "2026-07-08T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:mews-flash:2026-07-08", + "game": "nte", + "title": "Mews Flash", + "type": "other", + "summary": "Scratch cards daily at the Midnight Catzebo for massive amounts of Fons!", + "startsAt": "2026-07-08T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:neon-rift-mystery-box:2026-07-08", + "game": "nte", + "title": "Neon Rift Mystery Box", + "type": "other", + "summary": "Win the coveted Draco motorcycle, a new outfit, and more in this mystery box event!", + "startsAt": "2026-07-08T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:shadow-n-seek:2026-07-17", + "game": "nte", + "title": "Shadow-n-Seek", + "type": "other", + "summary": "One, two, three. Don't move! A 6-player game of hide-and-seek!", + "startsAt": "2026-07-17T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:gold-clash:2026-07-20", + "game": "nte", + "title": "Gold Clash", + "type": "challenge", + "summary": "Earn Fons twice as fast in Pink Paws Heist.", + "startsAt": "2026-07-20T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-03T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:going-going-gone:2026-07-29", + "game": "nte", + "title": "Going, Going, Gone", + "type": "other", + "summary": "Win or lose, it all comes down to one swing of the gavel.", + "startsAt": "2026-07-29T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:market-opening-rehearsal:2026-07-29", + "game": "nte", + "title": "Market Opening Rehearsal", + "type": "other", + "summary": "This is a free combat trial for the Appraisers Iroi, Haniel, Aurelia, and Skia.", + "startsAt": "2026-07-29T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:wayfarer-perks:2026-07-29", + "game": "nte", + "title": "Wayfarer Perks", + "type": "other", + "summary": "Pull for characters a certain amount of times to receive cosmetics!", + "startsAt": "2026-07-29T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:fishing-frenzy:2026-08-03", + "game": "nte", + "title": "Fishing Frenzy", + "type": "other", + "summary": "A rod's just the beginning, there's more than one way to land your catch!", + "startsAt": "2026-08-03T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:pixel-surge:2026-08-03", + "game": "nte", + "title": "Pixel Surge", + "type": "other", + "summary": "During the event, spend Character Pixels in Houdini's Magic Stage, Houdini's Schemes, Bubble Can Factory, and Rabbit Hole to receive double rewards.", + "startsAt": "2026-08-03T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-10T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:warren-lucky-flip:2026-08-05", + "game": "nte", + "title": "Warren Lucky Flip", + "type": "other", + "summary": "Complete daily quests to earn card flips.", + "startsAt": "2026-08-05T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "nte:fons-rush:2026-08-10", + "game": "nte", + "title": "Fons Rush", + "type": "other", + "summary": "During the event, spend City Stamina Hethereau Hobbies activities to earn double the Fons.", + "startsAt": "2026-08-10T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-17T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Neverness-to-Everness/archives/592073", + "sourceId": "nte-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + } +] diff --git a/fixtures/nte/game8-events-2026-08-14.html b/fixtures/nte/game8-events-2026-08-14.html new file mode 100644 index 0000000..b5a1e01 --- /dev/null +++ b/fixtures/nte/game8-events-2026-08-14.html @@ -0,0 +1,3114 @@ +List of All Events and Schedule | Neverness to Everness (NTE)|Game8 + + + + + + + + +

      What can you do as a free member?

      Member benefits illustration

      Create your free account today and unlock all our premium features and tools to enhance your gaming experience.

      Member benefits illustration

      Create your free account today and save articles to your watchlist and get notified when they're updated with new information.

      Member benefits illustration

      Create your free account today and save your favorite games for quick access later, synced across all your devices.

      Member benefits illustration

      By creating a Game8 account and logging in, you'll receive instant notifications when someone replies to your posts.

      Comment rating feature illustration

      By creating a Game8 account and logging in, you can make use of convenient features in the comments section, such as rating and sorting comments.

      Premium archive feature illustration

      By creating a Game8 account and logging in, you can access Premium articles that are exclusively available to members.

      Map tool feature illustration

      By creating a Game8 account and logging in, the items you checked on the map tool will be saved.

      Site Interface

      Guest
      Free Member
      Article Watchlist
      Game Bookmarks
      Cross-device Sync
      Light/Dark Theme Toggle
      User Profiles
      Direct Feedback
      Comment Rating
      Comment Notification
      Premium Articles

      Game Tools

      Guest
      Free Member
      Interactive Map Access
      Interactive Map Pins
      Interactive Map Comments
      Interactive Map Pins Cross-Device
      Check List
      Event Choice Checker
      Deck Builder Cross-Device
      Message Board Notification
      Message Board Cross-Device
      Build Planner
      Stat Calculator
      Diagnostic Tool
      Weapon/Armor Wishlist
      Team Builder
      Character Scoring
      Tier List Maker

      Want more information?Learn more

      +
      Neverness to Everness (NTE)

      List of All Events and Schedule

      + + +Add to Watchlist (2)
      +

      Version 1.3 | Redeem Codes
      Version 1.2 | 999 Nights
      ★ Check our Best Builds and Best Team Guides!
      ☆ New Character: Iroi (Profile/Builds) | Arc: The Wrong Gate
      ★ Tier Lists: Characters | Arcs | CartridgesReroll
      ☆ Share your thoughts about the NTE in our survey!

      + +

      List of All Events
      Check out our list of all events in Neverness to Everness (NTE)! Read on to see the events calendar, ongoing current events, upcoming events, and past events.

      + + + + + + + + +
      Attention!Attention! Enjoying the wiki? We'd love to hear from you! Take part in our Neverness to Everness Survey and help us improve our wiki.
      +

      Neverness to Everness Events Calendar

      +
      +

      List of All Events

      List of Upcoming Events

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      EventDurationEvent DetailsRewards
      NTE - Fons RushFons RushAugust 10 - August 17, 2026During the event, spend City Stamina Hethereau Hobbies activities to earn double the Fons. +・x2 Fons
      + + + + + + + + + + + +
      NTE - Pixel SurgePixel SurgeAugust 3 - August 10, 2026During the event, spend Character Pixels in Houdini's Magic Stage, Houdini's Schemes, Bubble Can Factory, and Rabbit Hole to receive double rewards. +・Double Event Rewards
      + + + + + + + + + + + +
      NTE - Fishing FrenzyFishing FrenzyAugust 3 - August 19, 2026A rod's just the beginning, there's more than one way to land your catch! +・Annulith
      + +・Elite Hunter Guide
      + + +・Chaotic Dye
      + + + + + + + + + + +
      NTE - Warren Lucky FlipWarren Lucky FlipAugust 5 - August 19, 2026Complete daily quests to earn card flips. +・Annulith
      + +・Fabricated Dice
      + + +・?
      + + + + + + + + + + +
      +
      + +

      List of Current Events

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      EventDurationEvent DetailsRewards
      NTE - Market Opening RehearsalMarket Opening RehearsalJuly 29 - August 19, 2026This is a free combat trial for the Appraisers Iroi, Haniel, Aurelia, and Skia. +・Annulith
      + +・Expansion Core
      + + + + + + + + + + + +
      NTE - Circle GiftCircle GiftJuly 8 - August 19, 2026Log in to NTE for a week days to get 10 Solid Dice! +・Solid Dice
      + + + + + + + + + + + +
      NTE - 999 Nights999 NightsJuly 8 - August 19, 2026The party's assembled! Ready to conquer the Warren Continent! +・Annulith
      + +・Fabricated Dice
      + + +・Floe Cinema Ticket
      + + + + + + + + + + +
      NTE - Gold ClashGold ClashJuly 20 - August 3, 2026Earn Fons twice as fast in Pink Paws Heist. +・Fons
      + + + + + + + + + + + +
      NTE - Mews FlashMews FlashJuly 8 - August 19, 2026Scratch cards daily at the Midnight Catzebo for massive amounts of Fons! +・Fons
      + +・Beyond the Screen
      + + +・Hethereau Daily
      + + +・What Do Cats Dream Of
      + + +・The Art of Ramen
      + + +・Beyond the Book
      + + + + + + + +
      NTE - Neon Rift Mystery BoxNeon Rift Mystery BoxJuly 8 - August 19, 2026Win the coveted Draco motorcycle, a new outfit, and more in this mystery box event! +・Draco
      + +・Glory Days
      + + +・Recolor - Glory Days - Braveheart
      + + +・Recolor - Glory Days - Starfall
      + + +・Ember Trail
      + + +・Fabricated Dice
      + + + + + + + +
      NTE - Shadow-n-SeekShadow-n-SeekJuly 17 - August 19, 2026One, two, three. Don't move! A 6-player game of hide-and-seek! +・Annulith
      + +・Fons
      + + +・Floe Cinema Ticket
      + + +・Chaotic Dye
      + + +・Manhole Boss
      + + +・Elite Hunter Guide
      + + + + + + + +
      NTE - Wayfarer PerksWayfarer PerksJuly 29 - August 19, 2026Pull for characters a certain amount of times to receive cosmetics! +・Sheepcopter - Glider
      + +・Threes Company - Livery
      + + +・Archmage
      + + + + + + + + + + +
      NTE - Going, Going, GoneGoing, Going, GoneJuly 29 - August 19, 2026Win or lose, it all comes down to one swing of the gavel. +・Annulith
      + +・Fons
      + + +・Floe Cinema Ticket
      + + +・Beetle Coins
      + + +・Growth Materials
      + + + + + + + + +
      +
      +

      All Permanent Events

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      EventDurationEvent DetailsRewards
      NTE - Login GiftLogin GiftPermanentLog in to NTE for fifteen days since joining for daily rewards! +・Solid Dice
      + +・Aurelia
      + + +・Stellar Veil
      + + + + + + + + + + +
      NTE - Anomaly HunterAnomaly Hunter's JourneyPermanentIncrease your Hunter Level to claim generous rewards! +・Fabricated Dice
      + +・Solid Dice
      + + +・Tri-Key
      + + +・Annulith
      + + + + + + + + + +
      NTE - HunterHunter's JournalPermanentReach certain milestones to receive special rewards. +・Solid Dice
      + +・Fabricated Dice
      + + +・Tri-Key
      + + +・Appraiser: Chiz
      + + +・Appraiser: Haniel
      + + +・Arc: Contemplative Cat
      + + +・Permanently Available
      + + +・S Class Cartridges
      + + + + + +
      NTE - BigOwl ExpressBigOwl ExpressPermanentClear Beyond the Rails - Fracture Circle to receive Haniel and other rewards! +・Haniel
      + +・Annulith
      + + +・Fons
      + + +・Shadow Creed
      + + +・Speedy Hedgehog
      + + +・Silver Carrota
      + + +・Permanently Available
      + + + + + + +
      NTE - City GuideCity GuidePermanentComplete things around Hethereau City to receive special rewards! +・Annulith
      + + + + + + + + + + + +
      NTE - Arc CollectionArc CollectionPermanentFind certain Arcs around Hethereau City to receive rewards! +・Annulith
      + +・Chaotic Dye
      + + +・Beetle Coin
      + + + + + + + + + + +
      +
      +

      Beyond the Rails is a Recurring Event

      +
      Clear Beyond The Rails
      + +

      Beyond the Rails is a gauntlet-type content event that pits you against multiple opponents. This event actively resets upon major updates, so actively check it for Annulith and currency!

      + +

      Beyond the Rails Guide

      +

      List of Previous Events

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      EventDurationEvent DetailsRewards
      NTE - Tiger PerksTiger PerksApr. 29 - May 13, 2026Pull for characters a certain amount of times to receive cosmetics! +・Glider Skin: Underboss-of-the-Underboss
      + +・Livery: Tiger Incoming!
      + + +・Nanally Skin: Phoenix Kick
      + + + + + + + + + + +
      NTE - Zero’s CompanionZero’s CompanionMay 13 - May 27, 2026Assign a companion to grow and get additional resources as you do so. +・Annulith
      + +・Hunter Guides
      + + +・Dyes
      + + +・Beetle Coins
      + + + + + + + + + +
      NTE - Hourglass PerksHourglass PerksMay 13 - June 3, 2026Pull in Misty Tipsy Style banner a certain amount of times to receive cosmetics! +・Glider Skin: Orchid Breeze
      + +・Livery: Autumn Haze
      + + +・Hotori Skin: Priceless Orchid
      + + + + + + + + + + +
      NTE - Whisker PatrolWhisker PatrolMay 27 - June 3, 2026Beat Garuda Members and Anomalies for event points +・Annulith
      + +・Fons
      + + +・Expansion Core
      + + + + + + + + + + +
      NTE - Daydream PerksDaydream PerksJune 3 - June 24, 2026Pull in Fading Reverie banner a certain amount of times to receive cosmetics! +・Tomato Duo - Glider
      + +・Tomato Cruise - Livery
      + + +・Gilded Rhapsody
      + + + + + + + + + + +
      NTE - Everdriving Mystery BoxEverdriving Mystery BoxJune 3 - July 8, 2026Purchase pulls for a chance to get the Porsche 918 Spyder +・Porsche 918 Spyder
      + +・Perfect Partners
      + + +・Heartbeat Moment
      + + +・Fabricated Dice
      + + + + + + + + + +
      NTE - Underground CircuitUnderground CircuitJune 3 - July 8, 2026A 6 player free-for-all race where the only goal is reaching the finish line! +・Fabricated Dice
      + +・Annulith
      + + +・Floe Cinema Ticket
      + + +・Fons
      + + + + + + + + + +
      NTE - WhatWhat's BakingJune 3 - July 8, 2026Post in the Bagel Social Media app for likes to get rewards! +・Fabricated Dice
      + +・Annulith
      + + +・Fons
      + + +・Elite Hunter Guide
      + + +・Chaotic Dye
      + + +・Manhole Boss
      + + + + + + + +
      NTE - Sunward TravelogueSunward TravelogueJune 3 - July 8, 2026Explore the new location: Sunward Island! +・Annulith
      + +・Beetle Coins
      + + +・Manhole Boss
      + + +・Elite Hunter Guide
      + + +・Chaotic Dye
      + + +・Expansion Core
      + + +・June 3, 2026
      + + + + + + +
      NTE - The Long DreamThe Long DreamJune 3 - July 8, 2026Find Lacrimosa in different parts of Hethereau! +・Annulith
      + +・Floe Cinema Ticket
      + + +・Character Materials
      + + + + + + + + + + +
      NTE - HunterHunter's CrucibleJune 18 - July 8, 2026Steel is forged in fire. Hunters are forged by worthier prey. +・Annulith
      + +・Fons
      + + +・Expert Hunter Guide
      + + + + + + + + + + +
      NTE - Fight ChampionshipFight ChampionshipJune 24 - July 8, 2026Fight in prison to get rewards. +・Annulith
      + +・Fabricated Dice
      + + +・Fons
      + + + + + + + + + + +
      NTE - Fogshroud PerksFogshroud PerksJune 24 - July 8, 2026Pull in the Forsaken Path banner a certain amount of times to receive cosmetics! +・Skyrider - Glider
      + +・ Hound Blitz - Novis ST-X 950 Livery Skin
      + + +・Clear Skies
      + + + + + + + + + + +
      NTE - Stamina RechargeStamina RechargeJuly 13 - July 20, 2026Spend City Stamina twice as fast in select Hethereau Hobby activities. +・Fons
      + + + + + + + + + + + +
      NTE - Resolve PerksResolve PerksJuly 8 - July 29, 2026Pull in Before the Dawn banner a certain amount of times to receive cosmetics! +・Overcast Canopy - Glider Skin
      + +・ Hidden Dragon - Livery
      + + +・Student of Terrasea
      + + + + + + + + + + +
      +
      +

      Neverness to Everness (NTE) Related Guides

      +
      Main Links Partial
      + +

      Neverness to Everness Wiki Top Page

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Neverness to Everness Categories
      Episodes size:440x110EpisodesSpinoffs size:440x110Spinoffs
      Side QuestsSide QuestsNTE - 999 Nights Partial999 Nights
      Anomaly CommissionsAnomaly CommissionsCharacters size:440x110Characters
      BuildsBuildsArcsArcs
      CartridgesCartridgesModulesModules
      BossesBossesNTE Enemies Partial BannerEnemies
      Anomaly ZoneAnomaly ZonesBannersBanners (Gacha)
      EventsEventsCity TycoonCity Tycoon
      Items PartialItemsFurnitureFurniture
      VehiclesVehiclesCosmeticsCosmetics
      MapsMapsTips and TricksTips and Tricks
      Latest NewsNews and Updates-
      +

      Recommended Guides

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NTE - Best Character Tier ListTier List and Best CharactersReroll Tier ListReroll Tier List
      NTE - Best Arcs Tier ListBest Arcs Tier ListBest Starting CharactersBest Starting Characters
      Reroll Guide - How to RerollHow to RerollDailies Guide - What to Do Every DayDailies Guide: What to Do Every Day
      Best TeamsBest TeamsWhich S Class Character Should You Pick?Which S Class Character Should You Pick?
      Best Vehicles Tier ListBest Vehicles Tier ListTeam Comp GuideTeam Building Guide
      How to Get Character Signature ArcsAll Character Signature ArcsHow to Raise Your City Tycoon Level FastHow to Raise Your City Tycoon Level Fast
      Neverness to Everness - How to Earn Fons FastHow to Earn Fons FastNeverness to Everness - All Safe LocationsAll Safe Locations
      List of Voice ActorsList of Voice ActorsHow to Unlock the Blue Flame BikeHow to Unlock the Blue Flame Bike
      NTE - Easter Eggs and ReferencesEaster Eggs and ReferencesNeverness to Everness - Auction HouseHow to Unlock the Auction House
      NTE - Where to Use City StaminaCity Stamina GuideNTE - Character Pixel GuideCharacter Pixels Guide
      Neverness to Everness - Best Cartridges Tier ListBest Cartridges Tier ListNeverness to Everness - Best PropertiesBest Properties to Buy
      Achievements GuideAchievements GuideNeverness to Everness - Best Life SkillsBest Life Skills +
      Neverness to Everness - Best EmployeesBest Employees +Neverness to Everness - How to Increase Bonds FastHow to Increase Bonds Fast
      What to Do After Completing the Main Story What to Do After Completing the Story +List of SecretsList of Secrets
      Neverness to Everness - Tier List MakerCharacter Tier List MakerAnomalies.pngList of Anomalies
      GlossaryGlossary +NTE Story ExplainedStory Explained +
      Neverness to Everness - 999 Nights999 Nights +Volley Star GuideVolley Star Guide
      Shipwreck Salvage GuideShipwreck Salvage-
      +

      Comment

      1 Anonymous Anonymous 19 days

      So like why’s it still hotori’s banner mentioned in the market opening rehearsal?

      Author

        Please participate in our site
        improvement survey
        08/2026
        Would you assist in
        improving Game8's site?
        ×
        This survey will take about 1 minute.
        Your answers will help us to improve our website.
        + + +
        We are listening to our users' valuable opinions and discussing how to act on them.
        The information collected through this survey will be used for the purpose of improving our services. In addition, the information will only be disclosed a part of a statistic in a format that will not allow identification of personal information.
        Begin Survey
        +

        All rights reserved

        + + +
        Guide Index
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        \ No newline at end of file diff --git a/scripts/parse-fixture.ts b/scripts/parse-fixture.ts new file mode 100644 index 0000000..629c4ef --- /dev/null +++ b/scripts/parse-fixture.ts @@ -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 [--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 [--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}`, + ); + } +} diff --git a/src/ingest/adapters/index.ts b/src/ingest/adapters/index.ts new file mode 100644 index 0000000..5aa569e --- /dev/null +++ b/src/ingest/adapters/index.ts @@ -0,0 +1,112 @@ +import type { GachaEvent, GameId } from "../../shared/schema.ts"; +import { mergeEvents, type MergeResult } from "../merge.ts"; +import { parserById } from "../parsers/index.ts"; +import { SIX_HOURS_MS, type Adapter, type ParseContext } from "./types.ts"; + +/** + * The source registry. + * + * One entry per (game, page). Adding a source for a site we already parse is a + * single entry here. Adding a new site means a parser in `../parsers` first. + */ + +interface SourceSpec { + id: string; + game: GameId; + url: string; + parserId: string; + priority?: number; + minIntervalMs?: number; +} + +const SOURCES: SourceSpec[] = [ + { + id: "genshin-game8-events", + game: "genshin", + url: "https://game8.co/games/Genshin-Impact/archives/301601", + parserId: "game8", + }, + { + id: "nte-game8-events", + game: "nte", + url: "https://game8.co/games/Neverness-to-Everness/archives/592073", + parserId: "game8", + }, +]; + +function toAdapter(spec: SourceSpec): Adapter { + const parser = parserById(spec.parserId); + if (parser === undefined) { + throw new Error( + `source '${spec.id}' references unknown parser '${spec.parserId}'`, + ); + } + + return { + id: spec.id, + game: spec.game, + url: spec.url, + parserId: spec.parserId, + minIntervalMs: spec.minIntervalMs ?? SIX_HOURS_MS, + priority: spec.priority ?? 0, + parse(html: string, ctx: ParseContext): GachaEvent[] { + // A site redesign should fail the run loudly rather than publish an empty + // calendar, which would read as "no events" to a user. + if (!parser.canParse(html)) { + throw new Error( + `${spec.id}: document does not match the '${parser.label}' template; the source has likely been redesigned`, + ); + } + return parser.parse(html, ctx); + }, + }; +} + +export const ADAPTERS: Adapter[] = SOURCES.map(toAdapter); + +export function adaptersForGame(game: GameId): Adapter[] { + return ADAPTERS.filter((a) => a.game === game); +} + +export function adapterById(id: string): Adapter | undefined { + return ADAPTERS.find((a) => a.id === id); +} + +export function gamesWithSources(): GameId[] { + return [...new Set(ADAPTERS.map((a) => a.game))]; +} + +/** + * Parse every source for one game and combine them. + * + * Callers supply already-fetched documents so this stays pure and offline — + * fetching is stage 1's job, not the parser's. + */ +export function parseGame( + game: GameId, + documents: Map, + now: string, +): MergeResult { + const groups = adaptersForGame(game) + .sort((a, b) => b.priority - a.priority) + .flatMap((adapter) => { + const html = documents.get(adapter.id); + if (html === undefined) return []; + return [ + adapter.parse(html, { + now, + sourceUrl: adapter.url, + sourceId: adapter.id, + game: adapter.game, + }), + ]; + }); + + return mergeEvents(groups); +} + +// Convenience handles for tests and scripts. +export const genshinGame8 = ADAPTERS.find( + (a) => a.id === "genshin-game8-events", +)!; +export const nteGame8 = ADAPTERS.find((a) => a.id === "nte-game8-events")!; diff --git a/src/ingest/adapters/types.ts b/src/ingest/adapters/types.ts new file mode 100644 index 0000000..bc729f4 --- /dev/null +++ b/src/ingest/adapters/types.ts @@ -0,0 +1,38 @@ +import type { GachaEvent, GameId } from "../../shared/schema.ts"; + +export interface ParseContext { + /** + * Injected wall-clock, never read from Date.now() inside a parser. This is + * what lets a fixture captured months ago assert byte-identical output. + */ + now: string; + sourceUrl: string; + sourceId: string; + game: GameId; +} + +/** + * An adapter binds one URL, for one game, to one parser. + * + * It holds no parsing logic of its own — that lives in `../parsers`, keyed by + * site template. A game may have several adapters (several sources); their + * outputs are combined by `mergeEvents`. + */ +export interface Adapter { + /** Unique, stable: "--", e.g. "genshin-game8-events". */ + id: string; + game: GameId; + url: string; + /** Parser id from `../parsers`. */ + parserId: string; + minIntervalMs: number; + /** + * Higher wins when two sources disagree and neither is clearly better. + * Official feeds should outrank community wikis. + */ + priority: number; + + parse(html: string, ctx: ParseContext): GachaEvent[]; +} + +export const SIX_HOURS_MS = 6 * 60 * 60 * 1000; diff --git a/src/ingest/parsers/game8.ts b/src/ingest/parsers/game8.ts new file mode 100644 index 0000000..47f1891 --- /dev/null +++ b/src/ingest/parsers/game8.ts @@ -0,0 +1,274 @@ +import { + eventId, + type EventType, + type GachaEvent, +} from "../../shared/schema.ts"; +import { + parseMonthDayRange, + parseMonthDayYear, + parseSlashDateTimeRange, + type ParsedInstant, +} from "../dates.ts"; +import { assertFlatTables, scanDocument } from "../html.ts"; +import type { ParseContext } from "../adapters/types.ts"; +import type { SourceParser } from "./types.ts"; + +/** + * Shared parsing for Game8 event-calendar pages. + * + * Game8 does not use one template. Across games there are (at least) three: + * + * 1. Label/value detail tables — `Event Start` / `Event End` rows under a + * per-event `h3`. Full dates with year. (Genshin Impact) + * 2. Column tables — `Event | Duration | Event Details | Rewards`, one row per + * event, under a section heading. (Neverness to Everness) + * 3. Image-grid schedules with a bare `MM/DD` and no end date. Unsupportable + * without inventing a year and an end, so it yields nothing. (Endfield) + * + * Shapes 1 and 2 are both handled here; a page may contain either or both. + * Anything undated is skipped rather than guessed — see docs/PRD.md § Quality + * bar. + */ + +/** Section headings whose events belong on the calendar. */ +const INCLUDED_SECTIONS = [ + /current events/i, + /upcoming events/i, + /^recurring events$/i, + /events? schedule/i, +]; + +/** + * Sections deliberately skipped. Permanent events have no end and are not + * time-boxed; past events are over and would bury what is live. + */ +const EXCLUDED_SECTIONS = [ + /permanent events/i, + /past events/i, + /previous events/i, + /ended events/i, +]; + +/** Label/value rows carrying a single boundary instant. */ +const START_LABEL = /^(event|test run|banner)\s+start$/i; +const END_LABEL = /^(event|test run|banner)\s+end$/i; +/** Label/value rows carrying a whole range in one cell. */ +const RANGE_LABEL = /^(availability period|event period|duration|period|dates)$/i; + +/** Column-table header matchers. */ +const COL_TITLE = /^(.*\b)?events?$/i; +const COL_RANGE = /^(duration|dates?|period|availability period|schedule)$/i; +const COL_SUMMARY = /^(event )?(details?|description|overview)$/i; + +interface Candidate { + title: string; + summary: string | null; + start: ParsedInstant; + end: ParsedInstant | null; +} + +export function parseGame8EventsPage( + html: string, + ctx: ParseContext, +): GachaEvent[] { + // The table reader assumes flat tables. Assert rather than mis-parse silently. + if (!assertFlatTables(html)) { + throw new Error( + `${ctx.sourceId}: source contains nested tables; the flat-table reader cannot parse it safely`, + ); + } + + const nodes = scanDocument(html); + const candidates: Candidate[] = []; + + let sectionIncluded = false; + let currentTitle: string | null = null; + + for (const node of nodes) { + // Sections are marked by h2 on some pages and h3 on others, so inclusion is + // tracked at whichever level actually names the section. A heading matching + // neither list leaves the current state alone — it is an event name. + if (node.kind === "h2" || node.kind === "h3") { + const heading = node.text; + if (EXCLUDED_SECTIONS.some((re) => re.test(heading))) { + sectionIncluded = false; + currentTitle = null; + } else if (INCLUDED_SECTIONS.some((re) => re.test(heading))) { + sectionIncluded = true; + currentTitle = null; + } else { + currentTitle = heading; + } + continue; + } + + if (!sectionIncluded) continue; + + const fromColumns = readColumnTable(node.rows); + if (fromColumns.length > 0) { + candidates.push(...fromColumns); + continue; + } + + if (currentTitle === null) continue; + const dates = readLabelledDates(node.pairs); + if (dates === null) continue; + + candidates.push({ title: currentTitle, summary: null, ...dates }); + // One dated table per heading; ignore follow-on reward tables until the + // next heading. + currentTitle = null; + } + + return dedupe(candidates.map((c) => toEvent(c, ctx))); +} + +/** Shape 1: `Event Start` / `Event End` / `Availability Period` rows. */ +function readLabelledDates( + pairs: Array<{ label: string; value: string }>, +): { start: ParsedInstant; end: ParsedInstant | null } | null { + let start: ParsedInstant | null = null; + let end: ParsedInstant | null = null; + + for (const { label, value } of pairs) { + if (RANGE_LABEL.test(label)) { + const range = parseRange(value); + if (range) return range; + // "Permanently Available", "TBD" — not an error, just not datable. + continue; + } + if (START_LABEL.test(label)) start ??= parseMonthDayYear(value); + if (END_LABEL.test(label)) end ??= parseMonthDayYear(value); + } + + return start === null ? null : { start, end }; +} + +/** Shape 2: one row per event, with a title column and a range column. */ +function readColumnTable(rows: string[][]): Candidate[] { + if (rows.length < 2) return []; + const header = rows[0]; + if (header === undefined) return []; + + const titleIdx = header.findIndex((h) => COL_TITLE.test(h)); + const rangeIdx = header.findIndex((h) => COL_RANGE.test(h)); + if (titleIdx === -1 || rangeIdx === -1) return []; + + const summaryIdx = header.findIndex((h) => COL_SUMMARY.test(h)); + + const out: Candidate[] = []; + for (const row of rows.slice(1)) { + const title = row[titleIdx]?.trim(); + const rangeCell = row[rangeIdx]; + if (!title || rangeCell === undefined) continue; + + const range = parseRange(rangeCell); + // A row we cannot date is skipped, not guessed. This is also what keeps + // year-less summary tables ("08/12 - 08/24") from producing events. + if (range === null) continue; + + const summaryCell = summaryIdx === -1 ? undefined : row[summaryIdx]; + const summary = + summaryCell && summaryCell.length > 0 ? summaryCell.slice(0, 500) : null; + + out.push({ title, summary, start: range.start, end: range.end }); + } + return out; +} + +function parseRange( + value: string, +): { start: ParsedInstant; end: ParsedInstant } | null { + return parseSlashDateTimeRange(value) ?? parseMonthDayRange(value); +} + +/** + * Type is inferred from the title by keyword. This is presentation metadata + * used for filtering, not a date, so a conservative "other" default beats a + * confident mislabel. + */ +export function inferType(title: string): EventType { + const t = title.toLowerCase(); + if (/\brerun\b/.test(t)) return "rerun"; + if (/\b(banner|wish|warp|convene|gacha)\b/.test(t)) return "banner"; + if (/\b(login|log-in|sign-in|check-in|daily bonus)\b/.test(t)) return "login"; + if (/\b(challenge|trial|onslaught|abyss|tower|test runs?|clash|combat)\b/.test(t)) + return "challenge"; + if (/\b(shop|exchange|store)\b/.test(t)) return "shop"; + if (/\bmaintenance\b/.test(t)) return "maintenance"; + if (/\b(story|chapter|quest|act)\b/.test(t)) return "story"; + return "other"; +} + +function toEvent(c: Candidate, ctx: ParseContext): GachaEvent { + // Confidence reflects how much the source actually pinned down. Day precision + // and unknown ends are legitimate and common, but they are weaker evidence + // than an exact range and the gate should be able to see that. + let confidence = 0.95; + if (c.start.precision === "day") confidence -= 0.05; + if (c.end === null) confidence -= 0.15; + else if (c.end.precision === "day") confidence -= 0.05; + + return { + id: eventId(ctx.game, c.title, c.start.iso), + game: ctx.game, + title: c.title, + type: inferType(c.title), + summary: c.summary, + startsAt: c.start.iso, + startPrecision: c.start.precision, + endsAt: c.end?.iso ?? null, + endPrecision: c.end?.precision ?? "unknown", + // Game8 does not state whether an end follows per-region reset, so these + // are recorded as global rather than guessing a region split. A source that + // does state it should populate these properly. + regionScoped: false, + regionEnds: null, + sourceUrl: ctx.sourceUrl, + sourceId: ctx.sourceId, + status: "published", + confidence: Math.round(confidence * 100) / 100, + extractionMethod: "parser", + version: 1, + firstSeenAt: ctx.now, + updatedAt: ctx.now, + }; +} + +/** Same event listed in two shapes on one page — keep the better-dated one. */ +function dedupe(events: GachaEvent[]): GachaEvent[] { + const byId = new Map(); + for (const e of events) { + const existing = byId.get(e.id); + if (existing === undefined || e.confidence > existing.confidence) { + byId.set(e.id, e); + } + } + return [...byId.values()].sort((a, b) => + a.startsAt === b.startsAt + ? a.id.localeCompare(b.id) + : a.startsAt.localeCompare(b.startsAt), + ); +} + +/** + * Game8 as a pluggable source parser. Registered in `parsers/index.ts`; bound + * to concrete URLs by the adapters in `adapters/index.ts`. + */ +export const game8Parser: SourceParser = { + id: "game8", + label: "Game8", + canParse(html: string): boolean { + // Structural markers, not content: if Game8 redesigns, this goes false and + // the run fails loudly instead of quietly returning zero events. + // + // Quote style varies between Game8 pages — the Genshin page emits + // class="a-table", the NTE page class='a-table' — so match either. Every + // regex in html.ts is attribute-agnostic for the same reason. + return ( + /class=['"][^'"]*a-table/.test(html) && + /class=['"][^'"]*a-header--3/.test(html) + ); + }, + parse: parseGame8EventsPage, +}; diff --git a/src/ingest/parsers/index.ts b/src/ingest/parsers/index.ts new file mode 100644 index 0000000..7a5e7f3 --- /dev/null +++ b/src/ingest/parsers/index.ts @@ -0,0 +1,16 @@ +import { game8Parser } from "./game8.ts"; +import type { SourceParser } from "./types.ts"; + +/** + * Every known site template. Adding a source for a site already listed here is + * an entry in `adapters/index.ts`; adding a new *site* means a parser module + * here and one line below. + */ +export const PARSERS: SourceParser[] = [game8Parser]; + +export function parserById(id: string): SourceParser | undefined { + return PARSERS.find((p) => p.id === id); +} + +export type { SourceParser } from "./types.ts"; +export { game8Parser } from "./game8.ts"; diff --git a/src/ingest/parsers/types.ts b/src/ingest/parsers/types.ts new file mode 100644 index 0000000..8d0843b --- /dev/null +++ b/src/ingest/parsers/types.ts @@ -0,0 +1,30 @@ +import type { GachaEvent } from "../../shared/schema.ts"; +import type { ParseContext } from "../adapters/types.ts"; + +/** + * A parser understands one *site template* — not one game. + * + * The split matters as sources multiply: Game8 publishes calendars for a dozen + * games with the same markup, so one parser serves all of them. Adding a second + * site (an official JSON feed, a Fandom wiki) means a new parser here, and the + * adapters that use it are three lines each. + * + * Parsers must be pure over their input: no network, no clock, no randomness. + * `now` arrives via ParseContext. + */ +export interface SourceParser { + /** Stable identifier, e.g. "game8". Recorded on runs for diagnostics. */ + id: string; + + /** Human-readable, for the review UI and health output. */ + label: string; + + /** + * Cheap structural check: does this document look like something this parser + * can read? Used to fail loudly when a site is redesigned, instead of + * silently returning zero events. + */ + canParse(html: string): boolean; + + parse(html: string, ctx: ParseContext): GachaEvent[]; +} diff --git a/test/adapters/game8.test.ts b/test/adapters/game8.test.ts new file mode 100644 index 0000000..1874a28 --- /dev/null +++ b/test/adapters/game8.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, test } from "bun:test"; +import { genshinGame8, nteGame8 } from "../../src/ingest/adapters/index.ts"; +import type { Adapter } from "../../src/ingest/adapters/types.ts"; +import { inferType } from "../../src/ingest/parsers/game8.ts"; +import { GachaEvent, type EventType } from "../../src/shared/schema.ts"; + +/** + * Pinned clock. Parsers take `now` from context and never read it themselves, + * so a fixture captured months ago still asserts byte-identical output. + */ +const NOW = "2026-08-14T00:00:00.000Z"; + +const CASES: Array<{ adapter: Adapter; fixture: string }> = [ + { + adapter: genshinGame8, + fixture: "fixtures/genshin/game8-events-2026-08-14", + }, + { + adapter: nteGame8, + fixture: "fixtures/nte/game8-events-2026-08-14", + }, +]; + +async function runAdapter(adapter: Adapter, fixture: string) { + const html = await Bun.file(`${fixture}.html`).text(); + return adapter.parse(html, { + now: NOW, + sourceUrl: adapter.url, + sourceId: adapter.id, + game: adapter.game, + }); +} + +describe.each(CASES)("$adapter.id", ({ adapter, fixture }) => { + test("matches the checked-in expected output", async () => { + const events = await runAdapter(adapter, fixture); + const expected = await Bun.file(`${fixture}.expected.json`).json(); + expect(events).toEqual(expected); + }); + + test("every event satisfies the schema", async () => { + for (const event of await runAdapter(adapter, fixture)) { + expect(() => GachaEvent.parse(event)).not.toThrow(); + } + }); + + test("is deterministic across runs", async () => { + const a = await runAdapter(adapter, fixture); + const b = await runAdapter(adapter, fixture); + expect(a).toEqual(b); + }); + + test("never emits an end before its start", async () => { + for (const e of await runAdapter(adapter, fixture)) { + if (e.endsAt !== null) expect(e.endsAt > e.startsAt).toBe(true); + } + }); + + test("no event runs longer than 180 days", async () => { + // Patch cycles are ~6 weeks. A longer span means a misread year, which is + // the failure mode most likely to reach a user as a confident wrong date. + for (const e of await runAdapter(adapter, fixture)) { + if (e.endsAt === null) continue; + const days = + (Date.parse(e.endsAt) - Date.parse(e.startsAt)) / 86_400_000; + expect(days).toBeLessThanOrEqual(180); + } + }); + + test("event IDs are unique and stable in shape", async () => { + const events = await runAdapter(adapter, fixture); + const ids = events.map((e) => e.id); + expect(new Set(ids).size).toBe(ids.length); + for (const e of events) { + expect(e.id).toBe( + `${e.game}:${e.id.split(":")[1]}:${e.startsAt.slice(0, 10)}`, + ); + } + }); + + test("excludes permanent and past sections", async () => { + const titles = (await runAdapter(adapter, fixture)).map((e) => e.title); + // Permanent entries carry no dates; past entries ended before the fixture + // date. Neither belongs on a "what's live / what's next" calendar. + for (const t of titles) expect(t).not.toMatch(/permanent/i); + }); +}); + +describe("genshin fixture specifics", () => { + test("yields the nine dated events on the page", async () => { + const events = await runAdapter( + genshinGame8, + "fixtures/genshin/game8-events-2026-08-14", + ); + expect(events).toHaveLength(9); + + const byTitle = new Map(events.map((e) => [e.title, e])); + const mutual = byTitle.get("Mutual Aid in Bloom: Into the Frostlands"); + expect(mutual?.startsAt).toBe("2026-08-12T00:00:00.000Z"); + expect(mutual?.endsAt).toBe("2026-08-24T00:00:00.000Z"); + expect(mutual?.startPrecision).toBe("day"); + + // Sourced from a one-cell "Availability Period" range rather than + // Start/End rows — the other table shape on the same page. + expect(byTitle.get("Battle Pass - Frostfarer")?.endsAt).toBe( + "2026-09-21T00:00:00.000Z", + ); + }); + + test("year-less summary rows produce no events", async () => { + // The page's summary tables show "08/12 - 08/24" with no year. Those must + // be skipped, not year-guessed — and they must not duplicate the detail + // tables that carry the same events with real years. + const events = await runAdapter( + genshinGame8, + "fixtures/genshin/game8-events-2026-08-14", + ); + const dupes = events.filter( + (e) => e.title === "Mutual Aid in Bloom: Into the Frostlands", + ); + expect(dupes).toHaveLength(1); + }); +}); + +describe("nte fixture specifics", () => { + test("yields current and upcoming events only", async () => { + const events = await runAdapter( + nteGame8, + "fixtures/nte/game8-events-2026-08-14", + ); + expect(events).toHaveLength(13); // 9 current + 4 upcoming + + const titles = events.map((e) => e.title); + expect(titles).toContain("Market Opening Rehearsal"); // current + expect(titles).toContain("Fons Rush"); // upcoming + expect(titles).not.toContain("Login Gift"); // permanent + expect(titles).not.toContain("Tiger Perks"); // previous + }); + + test("carries the summary column through", async () => { + const events = await runAdapter( + nteGame8, + "fixtures/nte/game8-events-2026-08-14", + ); + const circleGift = events.find((e) => e.title === "Circle Gift"); + expect(circleGift?.summary).toContain("Log in"); + }); +}); + +describe("inferType", () => { + const cases: Array<[string, EventType]> = [ + ["Overflowing Abundance Rerun", "rerun"], + ["Stygian Onslaught", "challenge"], + ["Character Test Runs", "challenge"], + ["Gold Clash", "challenge"], + ["Seize the Day Login Bonus", "login"], + ["Epitome Invocation Banner", "banner"], + ["Mutual Aid in Bloom: Into the Frostlands", "other"], + ]; + test.each(cases)("%s → %s", (title, expected) => { + expect(inferType(title)).toBe(expected); + }); +});