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 { 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, }; } }