import type { FetchedStandingsRecord, StandingsSyncAdapter } from "./types"; import { statsMap, type EspnStat } from "./espn"; // Jolpica is the maintained successor to the deprecated Ergast API — same JSON shape, no key needed. const JOLPICA_DRIVER_STANDINGS_URL = "https://api.jolpi.ca/ergast/f1/current/driverStandings.json"; // ESPN IndyCar (IRL) standings endpoint const ESPN_INDYCAR_STANDINGS_URL = "https://site.api.espn.com/apis/v2/sports/racing/irl/standings"; interface JolpicaDriverStanding { position: string; points: string; wins: string; Driver: { driverId: string; givenName: string; familyName: string; }; } interface JolpicaStandingsResponse { MRData: { StandingsTable: { StandingsLists: Array<{ DriverStandings: JolpicaDriverStanding[]; }>; }; }; } export class F1StandingsAdapter implements StandingsSyncAdapter { async fetchStandings(): Promise { const standingsRes = await fetch(JOLPICA_DRIVER_STANDINGS_URL); if (!standingsRes.ok) { throw new Error( `F1 standings API returned ${standingsRes.status}: ${standingsRes.statusText}` ); } const json = (await standingsRes.json()) as JolpicaStandingsResponse; const driverStandings = json.MRData?.StandingsTable?.StandingsLists?.[0]?.DriverStandings ?? []; if (driverStandings.length === 0) { throw new Error( "F1 standings API returned no entries — season may not have started or response shape changed" ); } return driverStandings.map((d): FetchedStandingsRecord => { const position = parseInt(d.position, 10); const points = parseFloat(d.points); const wins = parseInt(d.wins, 10); return { teamName: `${d.Driver.givenName} ${d.Driver.familyName}`, externalTeamId: d.Driver.driverId, leagueRank: position, wins, losses: 0, gamesPlayed: 0, winPct: 0, currentPoints: points, }; }); } } // ESPN racing standings shape (flat list, no conference/division nesting) interface EspnRacingAthlete { id: string; displayName: string; } interface EspnRacingEntry { athlete: EspnRacingAthlete; stats: EspnStat[]; } interface EspnRacingStandingsResponse { standings?: { entries?: EspnRacingEntry[]; }; // Some ESPN racing endpoints wrap entries under children[0].standings children?: Array<{ standings?: { entries?: EspnRacingEntry[] }; }>; } export class IndyCarStandingsAdapter implements StandingsSyncAdapter { async fetchStandings(): Promise { const res = await fetch(ESPN_INDYCAR_STANDINGS_URL); if (!res.ok) { throw new Error( `IndyCar standings API returned ${res.status}: ${res.statusText}` ); } const json = (await res.json()) as EspnRacingStandingsResponse; // ESPN racing standings can be flat or nested under children[0] const entries: EspnRacingEntry[] = json.standings?.entries ?? json.children?.[0]?.standings?.entries ?? []; if (entries.length === 0) { throw new Error( "IndyCar standings API returned no entries — season may not have started or response shape changed" ); } return entries.map((entry, idx): FetchedStandingsRecord => { const sm = statsMap(entry.stats); const points = sm.get("championshipPts")?.value ?? sm.get("points")?.value ?? sm.get("pts")?.value ?? 0; const wins = sm.get("wins")?.value ?? 0; const starts = sm.get("starts")?.value ?? sm.get("racesStarted")?.value ?? 0; const rank = sm.get("rank")?.value ?? sm.get("position")?.value ?? idx + 1; const winPct = starts > 0 ? wins / starts : 0; return { teamName: entry.athlete.displayName, externalTeamId: entry.athlete.id, leagueRank: Math.round(rank), wins, losses: 0, gamesPlayed: starts, winPct, currentPoints: points, }; }); } }