migration to new feed source
This commit is contained in:
142
lib/feeds.ts
142
lib/feeds.ts
@@ -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
14
lib/fifa-constants.ts
Normal 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
52
lib/flags.ts
Normal 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",
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user