brackt/app/services/match-sync/pandascore.ts

138 lines
4.7 KiB
TypeScript
Raw Normal View History

import type { MatchRecord, MatchSyncAdapter, MatchStatusValue, SubGameRecord } from "./types";
// PandaScore API response types
interface PandaScoreOpponent {
opponent: { id: number; name: string };
type: string;
}
interface PandaScoreResult {
team_id: number;
score: number;
}
interface PandaScoreGame {
id: number;
position: number; // map number (1, 2, 3)
map?: { name?: string };
winner?: { id: number } | null;
winner_type?: string;
teams?: Array<{ team: { id: number }; score?: number }>;
status: string; // "not_started" | "running" | "finished"
begin_at?: string;
end_at?: string;
}
interface PandaScoreTournament {
id: number;
name: string;
}
interface PandaScoreMatch {
id: number;
name: string;
status: string; // "not_started" | "running" | "finished" | "canceled" | "postponed"
scheduled_at?: string;
begin_at?: string;
end_at?: string;
opponents?: PandaScoreOpponent[];
results?: PandaScoreResult[];
winner?: { id: number; name: string } | null;
winner_id?: number | null;
number_of_games?: number;
games?: PandaScoreGame[];
tournament?: PandaScoreTournament;
}
function mapStatus(s: string): MatchStatusValue {
switch (s) {
case "running": return "in_progress";
case "finished": return "complete";
case "canceled": return "canceled";
case "postponed": return "postponed";
default: return "scheduled";
}
}
function tournamentNameToStage(name: string): number | null {
const lower = name.toLowerCase();
if (lower.includes("opening")) return 1;
if (lower.includes("challengers") || lower.includes("elimination")) return 2;
if (lower.includes("legends") || lower.includes("decider")) return 3;
return null;
}
export class PandaScoreMatchSyncAdapter implements MatchSyncAdapter {
readonly supportsLiveScores = true;
async fetchMatches(externalSeasonId: string): Promise<MatchRecord[]> {
const apiKey = process.env.PANDASCORE_API_KEY;
if (!apiKey) throw new Error("PANDASCORE_API_KEY is not configured");
const allMatches: PandaScoreMatch[] = [];
let page = 1;
const perPage = 100;
// externalSeasonId is a PandaScore serie_id for CS2 Majors
while (true) {
const url = `https://api.pandascore.co/csgo/matches?filter[serie_id]=${externalSeasonId}&per_page=${perPage}&page=${page}&sort=scheduled_at`;
const resp = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!resp.ok) {
const body = await resp.text();
throw new Error(`PandaScore API error ${resp.status}: ${body}`);
}
const batch = (await resp.json()) as PandaScoreMatch[];
if (batch.length === 0) break;
allMatches.push(...batch);
if (batch.length < perPage) break;
page++;
}
return allMatches.map((m) => this.mapMatch(m));
}
private mapMatch(m: PandaScoreMatch): MatchRecord {
const team1 = m.opponents?.[0]?.opponent ?? null;
const team2 = m.opponents?.[1]?.opponent ?? null;
const r1 = m.results?.find((r) => r.team_id === team1?.id);
const r2 = m.results?.find((r) => r.team_id === team2?.id);
const matchStage = m.tournament ? tournamentNameToStage(m.tournament.name) : null;
const subGames: SubGameRecord[] = (m.games ?? []).map((g): SubGameRecord => {
const t1score = g.teams?.find((t) => t.team.id === team1?.id)?.score ?? null;
const t2score = g.teams?.find((t) => t.team.id === team2?.id)?.score ?? null;
const winnerExternalId = g.winner !== null && g.winner !== undefined ? String(g.winner.id) : null;
return {
externalGameId: String(g.id),
gameNumber: g.position,
gameLabel: g.map?.name ?? undefined,
team1Score: t1score !== undefined ? t1score : null,
team2Score: t2score !== undefined ? t2score : null,
winnerExternalId,
status: g.status === "running" ? "in_progress" : g.status === "finished" ? "complete" : "scheduled",
};
});
return {
externalMatchId: String(m.id),
team1ExternalId: team1 ? String(team1.id) : "",
team1Name: team1?.name ?? "",
team2ExternalId: team2 ? String(team2.id) : "",
team2Name: team2?.name ?? "",
team1Score: r1?.score ?? null,
team2Score: r2?.score ?? null,
winnerExternalId: m.winner_id !== null && m.winner_id !== undefined ? String(m.winner_id) : null,
status: mapStatus(m.status),
scheduledAt: m.scheduled_at ? new Date(m.scheduled_at) : null,
startedAt: m.begin_at ? new Date(m.begin_at) : null,
completedAt: m.end_at ? new Date(m.end_at) : null,
matchStage,
matchRound: null, // rounds within a stage require additional API calls or ordering
isSeries: (m.number_of_games ?? 1) > 1,
subGames: subGames.length > 0 ? subGames : undefined,
};
}
}