4652a92c29
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>
108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
/**
|
|
* Scrape job — fetches 2026 operating hours for all parks from the Six Flags API.
|
|
*
|
|
* npm run scrape — skips months scraped within the last 72h
|
|
* npm run scrape:force — re-scrapes everything
|
|
*/
|
|
|
|
import { openDb, upsertDay, isMonthScraped } from "../lib/db";
|
|
import { PARKS } from "../lib/parks";
|
|
import { scrapeMonth, fetchToday, RateLimitError } from "../lib/scrapers/sixflags";
|
|
|
|
const YEAR = 2026;
|
|
const MONTHS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
const DELAY_MS = 1000;
|
|
const FORCE = process.argv.includes("--rescrape");
|
|
|
|
async function sleep(ms: number) {
|
|
return new Promise<void>((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
async function main() {
|
|
const db = openDb();
|
|
|
|
console.log(`Scraping ${YEAR} — ${PARKS.length} parks\n`);
|
|
|
|
let totalFetched = 0;
|
|
let totalSkipped = 0;
|
|
let totalErrors = 0;
|
|
|
|
for (const park of PARKS) {
|
|
const label = park.shortName.padEnd(22);
|
|
|
|
let openDays = 0;
|
|
let fetched = 0;
|
|
let skipped = 0;
|
|
let errors = 0;
|
|
|
|
process.stdout.write(` ${label} `);
|
|
|
|
for (const month of MONTHS) {
|
|
if (!FORCE && isMonthScraped(db, park.id, YEAR, month)) {
|
|
process.stdout.write("·");
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const days = await scrapeMonth(park.apiId, YEAR, month);
|
|
db.transaction(() => {
|
|
for (const d of days) upsertDay(db, park.id, d.date, d.isOpen, d.hoursLabel, d.specialType);
|
|
})();
|
|
openDays += days.filter((d) => d.isOpen).length;
|
|
fetched++;
|
|
process.stdout.write("█");
|
|
if (fetched + skipped + errors < MONTHS.length) await sleep(DELAY_MS);
|
|
} catch (err) {
|
|
if (err instanceof RateLimitError) {
|
|
process.stdout.write("✗");
|
|
} else {
|
|
process.stdout.write("✗");
|
|
console.error(`\n error: ${err instanceof Error ? err.message : err}`);
|
|
}
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
totalFetched += fetched;
|
|
totalSkipped += skipped;
|
|
totalErrors += errors;
|
|
|
|
if (errors > 0) {
|
|
console.log(` ${errors} error(s)`);
|
|
} else if (skipped === MONTHS.length) {
|
|
console.log(" up to date");
|
|
} else {
|
|
console.log(` ${openDays} open days`);
|
|
}
|
|
}
|
|
|
|
console.log(`\n ${totalFetched} fetched ${totalSkipped} skipped ${totalErrors} errors`);
|
|
if (totalErrors > 0) console.log(" Re-run to retry failed months.");
|
|
|
|
// ── Today scrape (always fresh — dateless endpoint returns current day) ────
|
|
console.log("\n── Today's data ──");
|
|
for (const park of PARKS) {
|
|
process.stdout.write(` ${park.shortName.padEnd(22)} `);
|
|
try {
|
|
const today = await fetchToday(park.apiId);
|
|
if (today) {
|
|
upsertDay(db, park.id, today.date, today.isOpen, today.hoursLabel, today.specialType);
|
|
console.log(today.isOpen ? `open ${today.hoursLabel ?? ""}` : "closed");
|
|
} else {
|
|
console.log("no data");
|
|
}
|
|
} catch {
|
|
console.log("error");
|
|
}
|
|
await sleep(500);
|
|
}
|
|
|
|
db.close();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Fatal:", err);
|
|
process.exit(1);
|
|
});
|