brackt/app/services/standings-sync/epl.ts
2026-05-07 11:48:57 -07:00

91 lines
2.5 KiB
TypeScript

import type { FetchedStandingsRecord, StandingsSyncAdapter } from "./types";
const EPL_STANDINGS_URL = "https://api.football-data.org/v4/competitions/PL/standings";
interface FootballDataTeam {
id: number;
name: string;
shortName?: string;
tla?: string;
}
interface FootballDataTableEntry {
position: number;
team: FootballDataTeam;
playedGames: number;
form?: string | null;
won: number;
draw: number;
lost: number;
points: number;
goalsFor: number;
goalsAgainst: number;
goalDifference: number;
}
interface FootballDataStanding {
type: "TOTAL" | "HOME" | "AWAY" | string;
table: FootballDataTableEntry[];
}
interface FootballDataStandingsResponse {
standings: FootballDataStanding[];
}
/**
* English Premier League standings adapter backed by football-data.org.
*
* Endpoint: GET /v4/competitions/PL/standings
* Required env var: FOOTBALL_DATA_API_KEY
*/
export class EplStandingsAdapter implements StandingsSyncAdapter {
constructor(private readonly apiKey = process.env.FOOTBALL_DATA_API_KEY) {}
async fetchStandings(): Promise<FetchedStandingsRecord[]> {
if (!this.apiKey) {
throw new Error(
"FOOTBALL_DATA_API_KEY is required to sync EPL standings from football-data.org."
);
}
const response = await fetch(EPL_STANDINGS_URL, {
headers: {
"X-Auth-Token": this.apiKey,
},
});
if (!response.ok) {
throw new Error(
`football-data.org EPL standings API returned ${response.status}: ${response.statusText}`
);
}
const json = (await response.json()) as FootballDataStandingsResponse;
const total = json.standings?.find((standing) => standing.type === "TOTAL");
const table = total?.table ?? [];
if (table.length === 0) {
throw new Error(
"football-data.org EPL standings API returned no TOTAL table entries — response shape may have changed"
);
}
return table
.toSorted((a, b) => a.position - b.position)
.map((entry): FetchedStandingsRecord => ({
teamName: entry.team.name,
externalTeamId: String(entry.team.id),
wins: entry.won,
losses: entry.lost,
ties: entry.draw,
tablePoints: entry.points,
goalsFor: entry.goalsFor,
goalsAgainst: entry.goalsAgainst,
goalDifference: entry.goalDifference,
winPct: entry.playedGames > 0 ? entry.won / entry.playedGames : 0,
gamesPlayed: entry.playedGames,
leagueRank: entry.position,
lastTen: entry.form ?? undefined,
}));
}
}