92 lines
2.5 KiB
TypeScript
92 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,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|