refactor: hardcode API IDs and coaster lists, remove Playwright discovery
Embed Six Flags API IDs directly in the park registry and snapshot coaster lists from park-meta.json into a TypeScript module. This eliminates the Playwright-based discovery script, RCDB scraper, and runtime dependency on park-meta.json — preparing for the backend API transition. - Add apiId field to Park type and all 24 park entries - Create lib/coaster-data.ts with hardcoded coaster lists - Update page components to use park.apiId and new getCoasterSet() - Remove scripts/discover.ts, lib/scrapers/rcdb.ts, lib/park-meta.ts - Remove data/park-meta.json from shared volume - Remove playwright devDependency and discover npm script - Simplify scripts/scrape.ts (no RCDB, no discovery checks) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
import { normalizeForMatch } from "./coaster-match";
|
||||
|
||||
export const COASTER_LISTS: Record<string, string[]> = {
|
||||
greatadventure: [
|
||||
"Superman - Ultimate Flight",
|
||||
"El Toro",
|
||||
"Dark Knight",
|
||||
"Joker",
|
||||
"Jersey Devil Coaster",
|
||||
"Lil' Devil Coaster",
|
||||
"Flash: Vertical Velocity",
|
||||
"Batman The Ride",
|
||||
"Skull Mountain",
|
||||
"Runaway Mine Train",
|
||||
"Medusa",
|
||||
"Harley Quinn Crazy Train",
|
||||
"Nitro",
|
||||
],
|
||||
magicmountain: [
|
||||
"Ninja",
|
||||
"New Revolution",
|
||||
"Batman The Ride",
|
||||
"Viper",
|
||||
"Gold Rusher",
|
||||
"Riddler's Revenge",
|
||||
"Canyon Blaster",
|
||||
"Goliath",
|
||||
"X2",
|
||||
"Scream!",
|
||||
"Tatsu",
|
||||
"Apocalypse the Ride",
|
||||
"Road Runner Express",
|
||||
"Speedy Gonzales Hot Rod Racers",
|
||||
"Full Throttle",
|
||||
"Twisted Colossus",
|
||||
"West Coast Racers",
|
||||
"Wonder Woman Flight of Courage",
|
||||
],
|
||||
greatamerica: [
|
||||
"Demon",
|
||||
"Batman The Ride",
|
||||
"American Eagle",
|
||||
"Viper",
|
||||
"Whizzer",
|
||||
"Sprocket Rockets",
|
||||
"Raging Bull",
|
||||
"Flash: Vertical Velocity",
|
||||
"Superman - Ultimate Flight",
|
||||
"Dark Knight",
|
||||
"Little Dipper",
|
||||
"Goliath",
|
||||
"X-Flight",
|
||||
"Joker",
|
||||
"Maxx Force",
|
||||
"Wrath of Rakshasa",
|
||||
],
|
||||
overgeorgia: [
|
||||
"Blue Hawk",
|
||||
"Great American Scream Machine",
|
||||
"Dahlonega Mine Train",
|
||||
"Batman The Ride",
|
||||
"Georgia Scorcher",
|
||||
"Superman - Ultimate Flight",
|
||||
"Joker Funhouse Coaster",
|
||||
"Goliath",
|
||||
"Dare Devil Dive",
|
||||
"Twisted Cyclone",
|
||||
"Riddler Mindbender",
|
||||
"Georgia Gold Rusher",
|
||||
],
|
||||
overtexas: [
|
||||
"Pandemonium",
|
||||
"New Texas Giant",
|
||||
"Joker",
|
||||
"Aquaman: Power Wave",
|
||||
"Shock Wave",
|
||||
"Judge Roy Scream",
|
||||
"Runaway Mine Train",
|
||||
"Runaway Mountain",
|
||||
"Mini Mine Train",
|
||||
"Mr. Freeze",
|
||||
"Batman The Ride",
|
||||
"Titan",
|
||||
"Wile E. Coyote's Grand Canyon Blaster",
|
||||
],
|
||||
stlouis: [
|
||||
"Ninja",
|
||||
"River King Mine Train",
|
||||
"Mr. Freeze Reverse Blast",
|
||||
"Batman The Ride",
|
||||
"Screamin' Eagle",
|
||||
"Boss",
|
||||
"Pandemonium",
|
||||
"American Thunder",
|
||||
"Boomerang",
|
||||
"Rookie Racer",
|
||||
],
|
||||
fiestatexas: [
|
||||
"Batgirl Coaster Chase",
|
||||
"Road Runner Express",
|
||||
"Poltergeist",
|
||||
"Boomerang Coast to Coaster",
|
||||
"Superman Krypton Coaster",
|
||||
"Pandemonium",
|
||||
"Chupacabra",
|
||||
"Iron Rattler",
|
||||
"Batman The Ride",
|
||||
"Wonder Woman Golden Lasso Coaster",
|
||||
"Dr. Diabolical's Cliffhanger",
|
||||
],
|
||||
newengland: [
|
||||
"Joker",
|
||||
"Thunderbolt",
|
||||
"Great Chase",
|
||||
"Riddler Revenge",
|
||||
"Superman the Ride",
|
||||
"Flashback",
|
||||
"Catwoman's Whip",
|
||||
"Pandemonium",
|
||||
"Batman - The Dark Knight",
|
||||
"Wicked Cyclone",
|
||||
"Gotham City Gauntlet Escape from Arkham Asylum",
|
||||
],
|
||||
discoverykingdom: [
|
||||
"Roadrunner Express",
|
||||
"Medusa",
|
||||
"Cobra",
|
||||
"Flash: Vertical Velocity",
|
||||
"Kong",
|
||||
"Boomerang",
|
||||
"Superman Ultimate Flight",
|
||||
"Joker",
|
||||
"Batman The Ride",
|
||||
"Sidewinder Safari",
|
||||
],
|
||||
mexico: [
|
||||
"Tsunami",
|
||||
"Superman Krypton Coaster",
|
||||
"Batgirl Batarang",
|
||||
"Batman The Ride",
|
||||
"Superman el Último Escape",
|
||||
"Dark Knight",
|
||||
"Joker",
|
||||
"Medusa Steel Coaster",
|
||||
"Wonder Woman",
|
||||
"Speedway Stunt Coaster",
|
||||
],
|
||||
greatescape: [
|
||||
"Comet",
|
||||
"Steamin' Demon",
|
||||
"Flashback",
|
||||
"Canyon Blaster",
|
||||
"Frankie's Mine Train",
|
||||
"Bobcat",
|
||||
],
|
||||
darienlake: [
|
||||
"Predator",
|
||||
"Viper",
|
||||
"Mind Eraser",
|
||||
"Boomerang",
|
||||
"Ride of Steel",
|
||||
"Hoot N Holler",
|
||||
"Moto Coaster",
|
||||
"Tantrum",
|
||||
],
|
||||
cedarpoint: [
|
||||
"Raptor",
|
||||
"Rougarou",
|
||||
"Magnum XL-200",
|
||||
"Blue Streak",
|
||||
"Corkscrew",
|
||||
"Gemini",
|
||||
"Wilderness Run",
|
||||
"Woodstock Express",
|
||||
"Millennium Force",
|
||||
"Iron Dragon",
|
||||
"Cedar Creek Mine Ride",
|
||||
"Maverick",
|
||||
"GateKeeper",
|
||||
"Valravn",
|
||||
"Steel Vengeance",
|
||||
"Top Thrill 2",
|
||||
"Wild Mouse",
|
||||
"Siren's Curse",
|
||||
],
|
||||
knotts: [
|
||||
"Jaguar!",
|
||||
"GhostRider",
|
||||
"Xcelerator",
|
||||
"Silver Bullet",
|
||||
"Sierra Sidewinder",
|
||||
"Pony Express",
|
||||
"Coast Rider",
|
||||
"HangTime",
|
||||
"Snoopy's Tenderpaw Twister Coaster",
|
||||
],
|
||||
canadaswonderland: [
|
||||
"Flight Deck",
|
||||
"Dragon Fyre",
|
||||
"Mighty Canadian Minebuster",
|
||||
"Wilde Beast",
|
||||
"Ghoster Coaster",
|
||||
"Thunder Run",
|
||||
"Bat",
|
||||
"Vortex",
|
||||
"Taxi Jam",
|
||||
"Fly",
|
||||
"Silver Streak",
|
||||
"Backlot Stunt Coaster",
|
||||
"Behemoth",
|
||||
"Leviathan",
|
||||
"Wonder Mountain's Guardian",
|
||||
"Yukon Striker",
|
||||
"Snoopy's Racing Railway",
|
||||
"AlpenFury",
|
||||
],
|
||||
carowinds: [
|
||||
"Carolina Cyclone",
|
||||
"Woodstock Express",
|
||||
"Carolina Goldrusher",
|
||||
"Hurler",
|
||||
"Vortex",
|
||||
"Wilderness Run",
|
||||
"Afterburn",
|
||||
"Flying Cobras",
|
||||
"Thunder Striker",
|
||||
"Fury 325",
|
||||
"Copperhead Strike",
|
||||
"Snoopy's Racing Railway",
|
||||
"Ricochet",
|
||||
"Kiddy Hawk",
|
||||
],
|
||||
kingsdominion: [
|
||||
"Racer 75",
|
||||
"Woodstock Express",
|
||||
"Grizzly",
|
||||
"Flight of Fear",
|
||||
"Reptilian",
|
||||
"Great Pumpkin Coaster",
|
||||
"Apple Zapple",
|
||||
"Backlot Stunt Coaster",
|
||||
"Dominator",
|
||||
"Pantherian",
|
||||
"Twisted Timbers",
|
||||
"Tumbili",
|
||||
"Rapterra",
|
||||
],
|
||||
kingsisland: [
|
||||
"Flight of Fear",
|
||||
"Beast",
|
||||
"Racer",
|
||||
"Adventure Express",
|
||||
"Woodstock Express",
|
||||
"Bat",
|
||||
"Great Pumpkin Coaster",
|
||||
"Invertigo",
|
||||
"Diamondback",
|
||||
"Banshee",
|
||||
"Orion",
|
||||
"Mystic Timbers",
|
||||
"Snoopy's Soap Box Racers",
|
||||
"Woodstock's Air Rail",
|
||||
"Queen City Stunt Coaster",
|
||||
],
|
||||
valleyfair: [
|
||||
"High Roller",
|
||||
"Corkscrew",
|
||||
"Excalibur",
|
||||
"Wild Thing",
|
||||
"Mad Mouse",
|
||||
"Steel Venom",
|
||||
"Renegade",
|
||||
"Cosmic Coaster",
|
||||
],
|
||||
worldsoffun: [
|
||||
"Timber Wolf",
|
||||
"Cosmic Coaster",
|
||||
"Mamba",
|
||||
"Spinning Dragons",
|
||||
"Patriot",
|
||||
"Prowler",
|
||||
"Zambezi Zinger",
|
||||
"Boomerang",
|
||||
],
|
||||
miadventure: [
|
||||
"Corkscrew",
|
||||
"Wolverine Wildcat",
|
||||
"Zach's Zoomer",
|
||||
"Shivering Timbers",
|
||||
"Mad Mouse",
|
||||
"Thunderhawk",
|
||||
"Woodstock Express",
|
||||
],
|
||||
dorneypark: [
|
||||
"Thunderhawk",
|
||||
"Steel Force",
|
||||
"Wild Mouse",
|
||||
"Woodstock Express",
|
||||
"Talon",
|
||||
"Hydra the Revenge",
|
||||
"Possessed",
|
||||
"Iron Menace",
|
||||
],
|
||||
cagreatamerica: [
|
||||
"Demon",
|
||||
"Grizzly",
|
||||
"Woodstock Express",
|
||||
"Patriot",
|
||||
"Flight Deck",
|
||||
"Lucy's Crabbie Cabbies",
|
||||
"Psycho Mouse",
|
||||
"Gold Striker",
|
||||
"RailBlazer",
|
||||
],
|
||||
frontiercity: [
|
||||
"Silver Bullet",
|
||||
"Wildcat",
|
||||
"Diamondback",
|
||||
"Steel Lasso",
|
||||
"Frankie's Mine Train",
|
||||
],
|
||||
};
|
||||
|
||||
export function getCoasterSet(parkId: string): Set<string> | null {
|
||||
const coasters = COASTER_LISTS[parkId];
|
||||
if (!coasters || coasters.length === 0) return null;
|
||||
return new Set(coasters.map(normalizeForMatch));
|
||||
}
|
||||
|
||||
export function hasCoasterData(): boolean {
|
||||
return Object.values(COASTER_LISTS).some((list) => list.length > 0);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* park-meta.json — persisted alongside the SQLite DB in data/
|
||||
*
|
||||
* This file stores per-park metadata that doesn't belong in the schedule DB:
|
||||
* - rcdb_id: user-supplied RCDB park ID (fills into https://rcdb.com/{id}.htm)
|
||||
* - coasters: list of operating roller coaster names scraped from RCDB
|
||||
* - coasters_scraped_at: ISO timestamp of last RCDB scrape
|
||||
*
|
||||
* discover.ts: ensures every park has a skeleton entry (rcdb_id null by default)
|
||||
* scrape.ts: populates coasters[] for parks with a known rcdb_id (30-day staleness)
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const META_PATH = path.join(process.cwd(), "data", "park-meta.json");
|
||||
|
||||
export interface ParkMeta {
|
||||
/** RCDB park page ID — user fills this in manually after discover creates the skeleton */
|
||||
rcdb_id: number | null;
|
||||
/** Operating roller coaster names scraped from RCDB */
|
||||
coasters: string[];
|
||||
/** ISO timestamp of when coasters was last scraped from RCDB */
|
||||
coasters_scraped_at: string | null;
|
||||
}
|
||||
|
||||
export type ParkMetaMap = Record<string, ParkMeta>;
|
||||
|
||||
export function readParkMeta(): ParkMetaMap {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(META_PATH, "utf8")) as ParkMetaMap;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function writeParkMeta(meta: ParkMetaMap): void {
|
||||
fs.mkdirSync(path.dirname(META_PATH), { recursive: true });
|
||||
fs.writeFileSync(META_PATH, JSON.stringify(meta, null, 2) + "\n");
|
||||
}
|
||||
|
||||
/** Default skeleton entry for a park that has never been configured. */
|
||||
export function defaultParkMeta(): ParkMeta {
|
||||
return { rcdb_id: null, coasters: [], coasters_scraped_at: null };
|
||||
}
|
||||
|
||||
const COASTER_STALE_MS = parseStalenessHours(process.env.COASTER_STALENESS_HOURS, 720) * 60 * 60 * 1000;
|
||||
|
||||
/** Returns true when the coaster list needs to be re-scraped from RCDB. */
|
||||
export function areCoastersStale(entry: ParkMeta): boolean {
|
||||
if (!entry.coasters_scraped_at) return true;
|
||||
return Date.now() - new Date(entry.coasters_scraped_at).getTime() > COASTER_STALE_MS;
|
||||
}
|
||||
|
||||
import { normalizeForMatch } from "./coaster-match";
|
||||
export { normalizeForMatch as normalizeRideName } from "./coaster-match";
|
||||
import { parseStalenessHours } from "./env";
|
||||
|
||||
/**
|
||||
* Returns a Set of normalized coaster names for fast membership checks.
|
||||
* Returns null when no coaster data exists for the park.
|
||||
*/
|
||||
export function getCoasterSet(parkId: string, meta: ParkMetaMap): Set<string> | null {
|
||||
const entry = meta[parkId];
|
||||
if (!entry || entry.coasters.length === 0) return null;
|
||||
return new Set(entry.coasters.map(normalizeForMatch));
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export const PARKS: Park[] = [
|
||||
// ── Six Flags branded parks ──────────────────────────────────────────────
|
||||
{
|
||||
id: "greatadventure",
|
||||
apiId: 905,
|
||||
name: "Six Flags Great Adventure",
|
||||
shortName: "Great Adventure",
|
||||
chain: "sixflags",
|
||||
@@ -22,6 +23,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "magicmountain",
|
||||
apiId: 906,
|
||||
name: "Six Flags Magic Mountain",
|
||||
shortName: "Magic Mountain",
|
||||
chain: "sixflags",
|
||||
@@ -33,6 +35,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "greatamerica",
|
||||
apiId: 910,
|
||||
name: "Six Flags Great America",
|
||||
shortName: "Great America",
|
||||
chain: "sixflags",
|
||||
@@ -44,6 +47,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "overgeorgia",
|
||||
apiId: 902,
|
||||
name: "Six Flags Over Georgia",
|
||||
shortName: "Over Georgia",
|
||||
chain: "sixflags",
|
||||
@@ -55,6 +59,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "overtexas",
|
||||
apiId: 901,
|
||||
name: "Six Flags Over Texas",
|
||||
shortName: "Over Texas",
|
||||
chain: "sixflags",
|
||||
@@ -66,6 +71,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "stlouis",
|
||||
apiId: 903,
|
||||
name: "Six Flags St. Louis",
|
||||
shortName: "St. Louis",
|
||||
chain: "sixflags",
|
||||
@@ -77,6 +83,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "fiestatexas",
|
||||
apiId: 914,
|
||||
name: "Six Flags Fiesta Texas",
|
||||
shortName: "Fiesta Texas",
|
||||
chain: "sixflags",
|
||||
@@ -88,6 +95,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "newengland",
|
||||
apiId: 935,
|
||||
name: "Six Flags New England",
|
||||
shortName: "New England",
|
||||
chain: "sixflags",
|
||||
@@ -99,6 +107,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "discoverykingdom",
|
||||
apiId: 936,
|
||||
name: "Six Flags Discovery Kingdom",
|
||||
shortName: "Discovery Kingdom",
|
||||
chain: "sixflags",
|
||||
@@ -110,6 +119,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "mexico",
|
||||
apiId: 960,
|
||||
name: "Six Flags Mexico",
|
||||
shortName: "Mexico",
|
||||
chain: "sixflags",
|
||||
@@ -121,6 +131,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "greatescape",
|
||||
apiId: 924,
|
||||
name: "Six Flags Great Escape",
|
||||
shortName: "Great Escape",
|
||||
chain: "sixflags",
|
||||
@@ -132,6 +143,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "darienlake",
|
||||
apiId: 945,
|
||||
name: "Six Flags Darien Lake",
|
||||
shortName: "Darien Lake",
|
||||
chain: "sixflags",
|
||||
@@ -144,6 +156,7 @@ export const PARKS: Park[] = [
|
||||
// ── Former Cedar Fair theme parks ─────────────────────────────────────────
|
||||
{
|
||||
id: "cedarpoint",
|
||||
apiId: 1,
|
||||
name: "Cedar Point",
|
||||
shortName: "Cedar Point",
|
||||
chain: "sixflags",
|
||||
@@ -155,6 +168,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "knotts",
|
||||
apiId: 4,
|
||||
name: "Knott's Berry Farm",
|
||||
shortName: "Knott's",
|
||||
chain: "sixflags",
|
||||
@@ -166,6 +180,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "canadaswonderland",
|
||||
apiId: 40,
|
||||
name: "Canada's Wonderland",
|
||||
shortName: "Canada's Wonderland",
|
||||
chain: "sixflags",
|
||||
@@ -177,6 +192,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "carowinds",
|
||||
apiId: 30,
|
||||
name: "Carowinds",
|
||||
shortName: "Carowinds",
|
||||
chain: "sixflags",
|
||||
@@ -188,6 +204,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "kingsdominion",
|
||||
apiId: 25,
|
||||
name: "Kings Dominion",
|
||||
shortName: "Kings Dominion",
|
||||
chain: "sixflags",
|
||||
@@ -199,6 +216,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "kingsisland",
|
||||
apiId: 20,
|
||||
name: "Kings Island",
|
||||
shortName: "Kings Island",
|
||||
chain: "sixflags",
|
||||
@@ -210,6 +228,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "valleyfair",
|
||||
apiId: 14,
|
||||
name: "Valleyfair",
|
||||
shortName: "Valleyfair",
|
||||
chain: "sixflags",
|
||||
@@ -221,6 +240,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "worldsoffun",
|
||||
apiId: 6,
|
||||
name: "Worlds of Fun",
|
||||
shortName: "Worlds of Fun",
|
||||
chain: "sixflags",
|
||||
@@ -232,6 +252,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "miadventure",
|
||||
apiId: 12,
|
||||
name: "Michigan's Adventure",
|
||||
shortName: "Michigan's Adventure",
|
||||
chain: "sixflags",
|
||||
@@ -243,6 +264,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "dorneypark",
|
||||
apiId: 8,
|
||||
name: "Dorney Park",
|
||||
shortName: "Dorney Park",
|
||||
chain: "sixflags",
|
||||
@@ -254,6 +276,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "cagreatamerica",
|
||||
apiId: 35,
|
||||
name: "California's Great America",
|
||||
shortName: "CA Great America",
|
||||
chain: "sixflags",
|
||||
@@ -265,6 +288,7 @@ export const PARKS: Park[] = [
|
||||
},
|
||||
{
|
||||
id: "frontiercity",
|
||||
apiId: 943,
|
||||
name: "Frontier City",
|
||||
shortName: "Frontier City",
|
||||
chain: "sixflags",
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* RCDB (Roller Coaster DataBase) scraper.
|
||||
*
|
||||
* Fetches a park's RCDB page (https://rcdb.com/{id}.htm) and extracts the
|
||||
* names of operating roller coasters from the "Operating Roller Coasters"
|
||||
* section.
|
||||
*
|
||||
* RCDB has no public API. This scraper reads the static HTML page.
|
||||
* Please scrape infrequently (30-day staleness window) to be respectful.
|
||||
*/
|
||||
|
||||
const BASE = "https://rcdb.com";
|
||||
|
||||
const HEADERS = {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
Accept: "text/html,application/xhtml+xml",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrape operating roller coaster names for a park.
|
||||
*
|
||||
* Returns an array of coaster names on success, or null when the page
|
||||
* cannot be fetched or contains no operating coasters.
|
||||
*/
|
||||
export async function scrapeRcdbCoasters(rcdbId: number): Promise<string[] | null> {
|
||||
const url = `${BASE}/${rcdbId}.htm`;
|
||||
try {
|
||||
const res = await fetch(url, { headers: HEADERS, signal: AbortSignal.timeout(15_000) });
|
||||
if (!res.ok) {
|
||||
console.error(` RCDB ${rcdbId}: HTTP ${res.status}`);
|
||||
return null;
|
||||
}
|
||||
const html = await res.text();
|
||||
return parseOperatingCoasters(html);
|
||||
} catch (err) {
|
||||
console.error(` RCDB ${rcdbId}: ${err}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse operating roller coaster names from RCDB park page HTML.
|
||||
*
|
||||
* RCDB park pages list coasters in sections bounded by <section> tags.
|
||||
* The operating section heading looks like:
|
||||
* <h4>Operating Roller Coasters: <a href="...">16</a></h4>
|
||||
*
|
||||
* Each coaster is an <a> link to its detail page with an unquoted href:
|
||||
* <td data-sort="Batman The Ride"><a href=/5.htm>Batman The Ride</a>
|
||||
*
|
||||
* We extract only those links (href=/DIGITS.htm) from within the
|
||||
* operating section, stopping at the next <section> tag.
|
||||
*/
|
||||
function parseOperatingCoasters(html: string): string[] {
|
||||
// Find the "Operating Roller Coasters" section heading.
|
||||
const opIdx = html.search(/Operating\s+Roller\s+Coasters/i);
|
||||
if (opIdx === -1) return [];
|
||||
|
||||
// The section ends at the next <section> tag (e.g. "Defunct Roller Coasters").
|
||||
const after = html.slice(opIdx);
|
||||
const nextSection = after.search(/<section\b/i);
|
||||
const sectionHtml = nextSection > 0 ? after.slice(0, nextSection) : after;
|
||||
|
||||
// Extract coaster names from links to RCDB detail pages.
|
||||
// RCDB uses unquoted href attributes: href=/1234.htm
|
||||
// General links (/g.htm, /r.htm, /location.htm, etc.) won't match \d+\.htm.
|
||||
const names: string[] = [];
|
||||
const linkPattern = /<a\s[^>]*href=["']?\/(\d+)\.htm["']?[^>]*>([^<]+)<\/a>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = linkPattern.exec(sectionHtml)) !== null) {
|
||||
const name = decodeHtmlEntities(match[2].trim());
|
||||
if (name) names.push(name);
|
||||
}
|
||||
|
||||
// Deduplicate while preserving order
|
||||
return [...new Set(names)];
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)))
|
||||
.replace(/&[a-z]+;/gi, "");
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
/**
|
||||
* Six Flags scraper — calls the internal CloudFront operating-hours API directly.
|
||||
* Six Flags API client — calls the internal CloudFront operating-hours API.
|
||||
*
|
||||
* API: https://d18car1k0ff81h.cloudfront.net/operating-hours/park/{apiId}?date=YYYYMM
|
||||
* Returns full month data in one request — no browser needed.
|
||||
*
|
||||
* Each park has a numeric API ID that must be discovered first (see scripts/discover.ts).
|
||||
* Once stored in the DB, this scraper never touches a browser again.
|
||||
* Returns full month data in one request.
|
||||
*
|
||||
* Rate limiting: on 429/503, exponential backoff (30s → 60s → 120s), MAX_RETRIES attempts.
|
||||
*/
|
||||
@@ -309,7 +306,6 @@ export async function scrapeRidesForDay(
|
||||
|
||||
/**
|
||||
* Fetch operating hours for an entire month in a single API call.
|
||||
* apiId must be pre-discovered via scripts/discover.ts.
|
||||
*/
|
||||
export async function scrapeMonth(
|
||||
apiId: number,
|
||||
@@ -325,8 +321,7 @@ export async function scrapeMonth(
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch park info for a given API ID (used during discovery to identify park type).
|
||||
* Uses the current month so there's always some data.
|
||||
* Fetch park info for a given API ID. Uses the current month so there's always some data.
|
||||
*/
|
||||
export async function fetchParkInfo(
|
||||
apiId: number
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface Park {
|
||||
id: string;
|
||||
apiId: number;
|
||||
name: string;
|
||||
shortName: string;
|
||||
chain: "sixflags" | string;
|
||||
|
||||
Reference in New Issue
Block a user