migration to new feed source

This commit is contained in:
2026-07-01 16:34:01 -05:00
parent cfb778776e
commit 4e4c72da7f
58 changed files with 444 additions and 48 deletions

View File

@@ -1,8 +1,9 @@
import { GroupId, GoalEvent, Match, MatchStatus, Team } from "./types";
import { venueFor } from "./venues";
import { localisedTeamName } from "./team-mappings";
import { localisedTeamName, TEAM_LOCALIZATION } from "./team-mappings";
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
import { computeGroupTables, computeThirdPlaceTable } from "./standings";
import { FIFA_GROUP_MAP, FIFA_STAGE_MAP } from "./fifa-constants";
// ----------------------------------------------------------------------------
// Caching: einfacher In-Memory-Cache mit TTL. Verhindert, dass jede Browser-
@@ -609,6 +610,122 @@ interface FifaScores {
idStage: string | null;
}
// ----------------------------------------------------------------------------
// FIFA-API Calendar-Endpoint: Spiele + Teams parallel zur football-data-Quelle
// ----------------------------------------------------------------------------
interface FifaCalendarTeamBlock {
IdTeam: string;
Abbreviation: string;
TeamName: Array<{ Locale: string; Description: string }>;
}
interface FifaCalendarMatch {
IdMatch: string;
IdStage: string;
IdGroup: string | null;
MatchNumber: number;
Date: string;
Home: FifaCalendarTeamBlock | null;
Away: FifaCalendarTeamBlock | null;
HomeTeamScore: number | null;
AwayTeamScore: number | null;
HomeTeamPenaltyScore: number | null;
AwayTeamPenaltyScore: number | null;
MatchStatus: number;
MatchTime: string | null;
ResultType: number | null;
Winner?: string | null;
Attendance: string | null;
Stadium?: {
Name: Array<{ Locale: string; Description: string }>;
CityName: Array<{ Locale: string; Description: string }>;
} | null;
}
// Holt alle Spiele + Teams von der FIFA-API (parallele Quelle, noch nicht aktiv).
export async function fetchMatchesAndTeamsFifa(locale: string = "de"): Promise<{ matches: Match[]; teams: Team[] }> {
return cached(`fifa:matches:${locale}`, 60_000, async () => {
const lang = locale === "en" ? "en" : "de";
const url = `${FIFA_BASE}/calendar/matches?language=${lang}&count=500&idSeason=${FIFA_SEASON}`;
const res = await fetch(url, {
cache: "no-store",
headers: { "User-Agent": "wm2026-board/1.0" },
});
if (!res.ok) throw new Error(`fifa-calendar ${res.status}`);
const data = (await res.json()) as { Results: FifaCalendarMatch[] };
const teamMap = new Map<string, Team>();
function teamFromBlock(tb: FifaCalendarTeamBlock, idGroup: string | null): Team | null {
const id = tb.IdTeam;
if (teamMap.has(id)) return teamMap.get(id)!;
const code = fifaCodeToAppCode(tb.Abbreviation);
const group = idGroup ? FIFA_GROUP_MAP[idGroup] : null;
if (!group) return null;
const loc = TEAM_LOCALIZATION[code.toUpperCase()];
const name = tb.TeamName?.[0]?.Description ?? "";
const t: Team = {
id,
name,
code,
group,
localisedName: loc ? loc[lang === "en" ? "en" : "de"] : name,
localisedNames: loc ? { de: loc.de, en: loc.en } : { de: name, en: name },
};
teamMap.set(id, t);
return t;
}
function mapFifaStatus(ms: number): MatchStatus {
switch (ms) {
case 0: return "FINISHED";
case 1: return "SCHEDULED";
case 10: return "POSTPONED";
default: return "LIVE";
}
}
const matches: Match[] = (data.Results ?? []).map((fm) => {
const group = fm.IdGroup ? FIFA_GROUP_MAP[fm.IdGroup] ?? null : null;
let homeTeam: Team | null = null;
let awayTeam: Team | null = null;
if (fm.Home) homeTeam = teamFromBlock(fm.Home, fm.IdGroup);
if (fm.Away) awayTeam = teamFromBlock(fm.Away, fm.IdGroup);
const attendance = fm.Attendance ? parseInt(fm.Attendance, 10) || null : null;
const minute = fm.MatchTime ? parseInt(fm.MatchTime, 10) || null : null;
const pref = lang === "en" ? "en-GB" : "de-DE";
const stadiumName = fm.Stadium?.Name?.find(n => n.Locale === pref)?.Description
?? fm.Stadium?.Name?.[0]?.Description ?? null;
const stadiumCity = fm.Stadium?.CityName?.find(n => n.Locale === pref)?.Description
?? fm.Stadium?.CityName?.[0]?.Description ?? null;
return {
id: fm.IdMatch,
group,
stage: FIFA_STAGE_MAP[fm.IdStage] ?? "GROUP",
matchNumber: fm.MatchNumber,
utcDate: fm.Date,
status: mapFifaStatus(fm.MatchStatus),
minute,
homeTeamId: fm.Home?.IdTeam ?? null,
awayTeamId: fm.Away?.IdTeam ?? null,
homeScore: fm.HomeTeamScore,
awayScore: fm.AwayTeamScore,
homePenalty: fm.HomeTeamPenaltyScore,
awayPenalty: fm.AwayTeamPenaltyScore,
winnerTeamId: fm.Winner ?? null,
venue: null,
stadiumName,
stadiumCity,
attendance,
};
});
const teams = [...teamMap.values()];
return { matches, teams };
});
}
// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück.
export async function fetchFifaScores(locale: string = "de"): Promise<{
scores: Map<number, FifaScores>;
@@ -660,17 +777,13 @@ export async function fetchFifaScores(locale: string = "de"): Promise<{
}
// Wendet FIFA-Live-Scores auf Matches an (MatchNumber als exakter Schlüssel).
// Team-IDs sind jetzt FIFA-IDs → winnerTeamId kann direkt gesetzt werden.
export function applyFifaScores(
matches: Match[], teams: Team[],
fifaData: { scores: Map<number, FifaScores>; fifaIdToAppCode: Map<string, string> },
): Match[] {
const { scores: fifaMap, fifaIdToAppCode } = fifaData;
const { scores: fifaMap } = fifaData;
if (fifaMap.size === 0) return matches;
// Baue App-Code → App-Team-ID Map
const appCodeToId = new Map<string, string>();
for (const t of teams) {
if (t.code) appCodeToId.set(t.code.toLowerCase(), t.id);
}
let applied = 0;
const result = matches.map((m) => {
const fs = fifaMap.get(m.matchNumber);
@@ -680,20 +793,7 @@ export function applyFifaScores(
if (fs.awayScore != null) r.awayScore = fs.awayScore;
if (fs.homePenalty != null) r.homePenalty = fs.homePenalty;
if (fs.awayPenalty != null) r.awayPenalty = fs.awayPenalty;
// FIFA-ID → App-Team-ID auflösen
if (fs.winnerTeamId) {
const appCode = fifaIdToAppCode.get(fs.winnerTeamId);
if (appCode) {
const appTeamId = appCodeToId.get(appCode.toLowerCase());
if (appTeamId) {
r.winnerTeamId = appTeamId;
} else {
console.warn("[fifa] Winner-Team-Code nicht in App-Teams:", appCode, "| matchNumber:", m.matchNumber);
}
} else {
console.warn("[fifa] FIFA-Winner-ID nicht in Team-Map:", fs.winnerTeamId, "| matchNumber:", m.matchNumber);
}
}
if (fs.winnerTeamId) r.winnerTeamId = fs.winnerTeamId;
if (fs.matchTime) {
const min = parseInt(fs.matchTime, 10);
if (!isNaN(min)) r.minute = min;

14
lib/fifa-constants.ts Normal file
View File

@@ -0,0 +1,14 @@
import { GroupId, Match } from "./types";
// FIFA IdGroup → App-GroupId (verifiziert, fortlaufend 289275-289286)
export const FIFA_GROUP_MAP: Record<string, GroupId> = {
"289275": "A", "289276": "B", "289277": "C", "289278": "D",
"289279": "E", "289280": "F", "289281": "G", "289282": "H",
"289283": "I", "289284": "J", "289285": "K", "289286": "L",
};
// FIFA IdStage → App-Stage (verifiziert)
export const FIFA_STAGE_MAP: Record<string, Match["stage"]> = {
"289273": "GROUP", "289287": "R32", "289288": "R16",
"289289": "QF", "289290": "SF", "289291": "3RD", "289292": "FINAL",
};

52
lib/flags.ts Normal file
View File

@@ -0,0 +1,52 @@
// TLA (3-Buchstaben, wie in TEAM_LOCALIZATION) → circle-flags ISO-2-Dateiname.
// Vollständig für alle Teams aus TEAM_LOCALIZATION.
export const TLA_TO_ISO2: Record<string, string> = {
ALG: "dz",
ARG: "ar",
AUS: "au",
AUT: "at",
BEL: "be",
BIH: "ba",
BRA: "br",
CAN: "ca",
CHE: "ch",
CIV: "ci",
COD: "cd",
COL: "co",
CPV: "cv",
CZE: "cz",
ECU: "ec",
EGY: "eg",
ENG: "gb-eng",
ESP: "es",
FRA: "fr",
GER: "de",
GHA: "gh",
HAI: "ht",
HRV: "hr",
IRN: "ir",
IRQ: "iq",
JOR: "jo",
JPN: "jp",
KOR: "kr",
KSA: "sa",
MAR: "ma",
MEX: "mx",
NED: "nl",
NOR: "no",
NZL: "nz",
PAN: "pa",
PAR: "py",
PRT: "pt",
QAT: "qa",
RSA: "za",
SCO: "gb-sct",
SEN: "sn",
SWE: "se",
TUN: "tn",
TUR: "tr",
URU: "uy",
USA: "us",
UZB: "uz",
CUW: "cw",
};

View File

@@ -51,8 +51,10 @@ export interface Match {
winnerTeamId?: string | null; // FIFA-Winner (auch bei Elfmeterschießen)
// Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden
prob?: { home: number; draw: number; away: number } | null;
venue?: string | null; // Austragungsort
attendance?: number | null; // Zuschauerzahl, falls verfügbar
venue?: string | null; // Austragungsort (alt: football-data)
stadiumName?: string | null; // FIFA-Stadionname
stadiumCity?: string | null; // FIFA-Stadt
attendance?: number | null; // Zuschauerzahl
goals?: GoalEvent[] | null; // Torereignisse
}