Korrekturen bei KO Spiele
This commit is contained in:
266
lib/feeds.ts
266
lib/feeds.ts
@@ -1,4 +1,4 @@
|
||||
import { GroupId, GoalEvent, Match, MatchStatus, Team } from "./types";
|
||||
import { GroupId, Match, MatchStatus, Team } from "./types";
|
||||
import { venueFor } from "./venues";
|
||||
import { localisedTeamName } from "./team-mappings";
|
||||
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
|
||||
@@ -556,221 +556,85 @@ export function attachKOOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// worldcup26.ir: schnelle Live-Scores (additiv, Fallback auf football-data)
|
||||
// FIFA-API: Live-Scores, Elfmeterschießen, Spielminute
|
||||
// ----------------------------------------------------------------------------
|
||||
const WC26_BASE = "https://worldcup26.ir";
|
||||
const FIFA_BASE = "https://api.fifa.com/api/v3";
|
||||
const FIFA_SEASON = "285023";
|
||||
|
||||
interface Wc26Game {
|
||||
home_team_name_en?: string;
|
||||
away_team_name_en?: string;
|
||||
group?: string;
|
||||
matchday?: string;
|
||||
home_score?: string;
|
||||
away_score?: string;
|
||||
time_elapsed?: string;
|
||||
home_scorers?: string;
|
||||
away_scorers?: string;
|
||||
current_minute?: string;
|
||||
interface FifaMatch {
|
||||
MatchNumber: number;
|
||||
HomeTeamScore: number | null;
|
||||
AwayTeamScore: number | null;
|
||||
HomeTeamPenaltyScore: number | null;
|
||||
AwayTeamPenaltyScore: number | null;
|
||||
MatchStatus: number; // 0=finished, 1=scheduled, else=live
|
||||
MatchTime: string | null; // z.B. "132'"
|
||||
ResultType: number | null; // 1=regular, 2=penalties
|
||||
}
|
||||
|
||||
interface LiveScore {
|
||||
homeName: string;
|
||||
awayName: string;
|
||||
group: string;
|
||||
matchday: string;
|
||||
interface FifaScores {
|
||||
homeScore: number | null;
|
||||
awayScore: number | null;
|
||||
status: string; // "IN_PLAY" | "FINISHED"
|
||||
homePenalty: number | null;
|
||||
awayPenalty: number | null;
|
||||
resultType: number | null;
|
||||
status: MatchStatus;
|
||||
matchTime: string | null;
|
||||
}
|
||||
|
||||
// Ruft alle Spiele von worldcup26.ir ab.
|
||||
export async function fetchLiveScores(): Promise<LiveScore[]> {
|
||||
const res = await fetch(`${WC26_BASE}/get/games`, {
|
||||
cache: "no-store",
|
||||
headers: { "User-Agent": "wm2026-board/1.0" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`worldcup26 ${res.status}`);
|
||||
const data = (await res.json()) as { games: Wc26Game[] };
|
||||
const games = data.games ?? [];
|
||||
|
||||
const scores: LiveScore[] = [];
|
||||
for (const g of games) {
|
||||
if (!g.home_team_name_en || !g.away_team_name_en) continue;
|
||||
const hScore = parseScore(g.home_score);
|
||||
const aScore = parseScore(g.away_score);
|
||||
let status = "";
|
||||
switch (g.time_elapsed) {
|
||||
case "live": status = "IN_PLAY"; break;
|
||||
case "finished": status = "FINISHED"; break;
|
||||
default: continue; // notstarted → überspringen
|
||||
}
|
||||
// Nur anwenden, wenn mindestens ein Score vorhanden ist
|
||||
if (hScore == null && aScore == null) continue;
|
||||
scores.push({
|
||||
homeName: g.home_team_name_en,
|
||||
awayName: g.away_team_name_en,
|
||||
group: g.group ?? "",
|
||||
matchday: String(g.matchday ?? ""),
|
||||
homeScore: hScore,
|
||||
awayScore: aScore,
|
||||
status,
|
||||
});
|
||||
}
|
||||
console.log("[worldcup26] spiele:", scores.length);
|
||||
return scores;
|
||||
}
|
||||
|
||||
function parseScore(s: string | undefined): number | null {
|
||||
if (!s || s === "null") return null;
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
// Wendet worldcup26-Live-Scores auf football-data-Matches an.
|
||||
// Matching über normalisierte Teamnamen + group + matchday.
|
||||
export function applyLiveScores(
|
||||
matches: Match[], teams: Team[], liveScores: LiveScore[],
|
||||
): Match[] {
|
||||
// Build lookup: normName(home)::normName(away)::group::matchday → match
|
||||
// Für Group-Phase ist die Paarung eindeutig (jedes Paar spielt 1x).
|
||||
const byPair = new Map<string, Match>();
|
||||
for (const m of matches) {
|
||||
if (m.group == null) continue; // nur Gruppenphase
|
||||
if (!m.homeTeamId || !m.awayTeamId) continue;
|
||||
const ht = teams.find((t) => t.id === m.homeTeamId);
|
||||
const at = teams.find((t) => t.id === m.awayTeamId);
|
||||
if (!ht || !at) continue;
|
||||
const hn = normName(ht.name);
|
||||
const an = normName(at.name);
|
||||
// Beide Richtungen
|
||||
byPair.set(`${hn}::${an}::${m.group}`, m);
|
||||
byPair.set(`${an}::${hn}::${m.group}`, m);
|
||||
}
|
||||
|
||||
const result = [...matches];
|
||||
let applied = 0;
|
||||
for (const ls of liveScores) {
|
||||
const hn = normName(ls.homeName);
|
||||
const an = normName(ls.awayName);
|
||||
const key = `${hn}::${an}::${ls.group}`;
|
||||
const fdMatch = byPair.get(key);
|
||||
if (!fdMatch) continue;
|
||||
|
||||
const idx = result.findIndex((m) => m.id === fdMatch.id);
|
||||
if (idx < 0) continue;
|
||||
|
||||
result[idx] = {
|
||||
...result[idx],
|
||||
homeScore: ls.homeScore ?? result[idx].homeScore,
|
||||
awayScore: ls.awayScore ?? result[idx].awayScore,
|
||||
status: ls.status as Match["status"],
|
||||
};
|
||||
applied++;
|
||||
}
|
||||
if (applied > 0) console.log("[worldcup26] auf matches angewandt:", applied);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Holt KO-Live-Daten von worldcup26 (Tore, Minute) und hängt sie an die Feed-Matches.
|
||||
export async function fetchKOLiveData(): Promise<Wc26Game[]> {
|
||||
try {
|
||||
const res = await fetch(`${WC26_BASE}/get/games`, {
|
||||
// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores zurück.
|
||||
export async function fetchFifaScores(): Promise<Map<number, FifaScores>> {
|
||||
return cached("fifa:scores", 45_000, async () => {
|
||||
const url = `${FIFA_BASE}/calendar/matches?language=de&count=500&idSeason=${FIFA_SEASON}`;
|
||||
const res = await fetch(url, {
|
||||
cache: "no-store",
|
||||
headers: { "User-Agent": "wm2026-board/1.0" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`worldcup26 ${res.status}`);
|
||||
const data = (await res.json()) as { games: Wc26Game[] };
|
||||
return (data.games ?? []).filter(g => {
|
||||
const grp = (g.group ?? "").toUpperCase();
|
||||
return grp === "" || grp === "R32" || grp === "R16" || grp === "QF" || grp === "SF" || grp === "3RD" || grp === "FINAL" || grp === "FINALIST";
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Hängt worldcup26-KO-Live-Daten an die Matches an (Tore, Minute, Scores).
|
||||
export function attachKOLiveData(matches: Match[], teams: Team[], koGames: Wc26Game[]): Match[] {
|
||||
if (koGames.length === 0) return matches;
|
||||
|
||||
const nameById = new Map<string, string>();
|
||||
for (const t of teams) {
|
||||
const nn = normName(t.name);
|
||||
if (!nameById.has(nn)) nameById.set(nn, t.id);
|
||||
}
|
||||
|
||||
return matches.map((m) => {
|
||||
if (m.group != null) return m; // nur K.o.-Spiele
|
||||
|
||||
const hId = m.homeTeamId;
|
||||
const aId = m.awayTeamId;
|
||||
const hName = hId ? teams.find(t => t.id === hId)?.name : null;
|
||||
const aName = aId ? teams.find(t => t.id === aId)?.name : null;
|
||||
|
||||
// Finde worldcup26-Spiel über Teamnamen
|
||||
const wm = koGames.find(g => {
|
||||
if (!hName || !aName) return false;
|
||||
const gh = normName(g.home_team_name_en ?? "");
|
||||
const ga = normName(g.away_team_name_en ?? "");
|
||||
return (gh === normName(hName) && ga === normName(aName)) ||
|
||||
(gh === normName(aName) && ga === normName(hName));
|
||||
});
|
||||
|
||||
if (!wm) return m;
|
||||
|
||||
const result = { ...m };
|
||||
|
||||
// Live-Score + Status
|
||||
if (wm.time_elapsed === "live" || wm.time_elapsed === "finished") {
|
||||
result.status = wm.time_elapsed === "finished" ? "FINISHED" : "IN_PLAY";
|
||||
const hs = parseScore(wm.home_score);
|
||||
const as = parseScore(wm.away_score);
|
||||
if (hs != null) result.homeScore = hs;
|
||||
if (as != null) result.awayScore = as;
|
||||
if (!res.ok) throw new Error(`fifa ${res.status}`);
|
||||
const data = (await res.json()) as { Results: FifaMatch[] };
|
||||
const map = new Map<number, FifaScores>();
|
||||
for (const fm of data.Results ?? []) {
|
||||
let status: MatchStatus;
|
||||
switch (fm.MatchStatus) {
|
||||
case 0: status = "FINISHED"; break;
|
||||
case 1: status = "SCHEDULED"; break;
|
||||
default: status = "LIVE"; break; // 2,3,... = läuft
|
||||
}
|
||||
map.set(fm.MatchNumber, {
|
||||
homeScore: fm.HomeTeamScore,
|
||||
awayScore: fm.AwayTeamScore,
|
||||
homePenalty: fm.HomeTeamPenaltyScore,
|
||||
awayPenalty: fm.AwayTeamPenaltyScore,
|
||||
resultType: fm.ResultType,
|
||||
status,
|
||||
matchTime: fm.MatchTime,
|
||||
});
|
||||
}
|
||||
|
||||
// Spielminute
|
||||
if (wm.current_minute) {
|
||||
const min = parseInt(wm.current_minute, 10);
|
||||
if (!isNaN(min)) result.minute = min;
|
||||
}
|
||||
|
||||
// Torereignisse aus home_scorers / away_scorers parsen (Format: "{\"Name Minute'\"}")
|
||||
const goals: GoalEvent[] = [];
|
||||
if (wm.home_scorers) parseScorers(wm.home_scorers, "home", goals);
|
||||
if (wm.away_scorers) parseScorers(wm.away_scorers, "away", goals);
|
||||
if (goals.length > 0) {
|
||||
goals.sort((a, b) => a.minute - b.minute);
|
||||
result.goals = goals;
|
||||
}
|
||||
|
||||
return result;
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
// Parst worldcup26-Scorer-String: "{\"Casemiro 56'\"}" oder "{\"Player 12'\",\"Other 34'\"}"
|
||||
function parseScorers(raw: string, team: "home" | "away", out: GoalEvent[]): void {
|
||||
if (!raw || raw === "{}") return;
|
||||
const cleaned = raw.replace(/[{}"\\]/g, "").trim();
|
||||
if (!cleaned) return;
|
||||
const parts = cleaned.split(",");
|
||||
for (const p of parts) {
|
||||
const m = /^(.+?)\s+([0-9+]+)'?(?:\s*\(.*?\))?\s*$/.exec(p.trim());
|
||||
if (m) {
|
||||
const name = m[1].trim();
|
||||
const minStr = m[2];
|
||||
let minute = 0;
|
||||
const plusIdx = minStr.indexOf("+");
|
||||
if (plusIdx >= 0) {
|
||||
minute = parseInt(minStr.slice(0, plusIdx), 10) + parseInt(minStr.slice(plusIdx + 1), 10);
|
||||
} else {
|
||||
minute = parseInt(minStr, 10);
|
||||
}
|
||||
if (!isNaN(minute)) {
|
||||
out.push({ scorer: name, minute, team });
|
||||
}
|
||||
// Wendet FIFA-Live-Scores auf Matches an (MatchNumber als exakter Schlüssel).
|
||||
export function applyFifaScores(matches: Match[], fifaMap: Map<number, FifaScores>): Match[] {
|
||||
if (fifaMap.size === 0) return matches;
|
||||
let applied = 0;
|
||||
const result = matches.map((m) => {
|
||||
const fs = fifaMap.get(m.matchNumber);
|
||||
if (!fs) return m;
|
||||
const r = { ...m };
|
||||
if (fs.homeScore != null) r.homeScore = fs.homeScore;
|
||||
if (fs.awayScore != null) r.awayScore = fs.awayScore;
|
||||
if (fs.homePenalty != null) r.homePenalty = fs.homePenalty;
|
||||
if (fs.awayPenalty != null) r.awayPenalty = fs.awayPenalty;
|
||||
if (fs.matchTime) {
|
||||
const min = parseInt(fs.matchTime, 10);
|
||||
if (!isNaN(min)) r.minute = min;
|
||||
}
|
||||
}
|
||||
r.status = fs.status;
|
||||
applied++;
|
||||
return r;
|
||||
});
|
||||
if (applied > 0) console.log("[fifa] scores angewandt:", applied);
|
||||
return result;
|
||||
}
|
||||
@@ -39,6 +39,8 @@ export interface Match {
|
||||
awayTeamId: string | null;
|
||||
homeScore: number | null;
|
||||
awayScore: number | null;
|
||||
homePenalty?: number | null; // Elfmeterschießen (FIFA-API)
|
||||
awayPenalty?: number | null;
|
||||
// Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden
|
||||
prob?: { home: number; draw: number; away: number } | null;
|
||||
venue?: string | null; // Austragungsort
|
||||
|
||||
Reference in New Issue
Block a user