From 2a57be6e2f6d394329e59302b1d843bf5b021dea Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 30 Jun 2026 11:13:19 -0500 Subject: [PATCH] goals --- app/api/matches/route.ts | 7 ++- app/components/KoFixtures.tsx | 5 +- lib/feeds.ts | 88 ++++++++++++++++++++++++++++++++++- lib/types.ts | 2 +- 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/app/api/matches/route.ts b/app/api/matches/route.ts index 62c0801..fd2c530 100644 --- a/app/api/matches/route.ts +++ b/app/api/matches/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, assignKONumbersBySlots } from "@/lib/feeds"; +import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, assignKONumbersBySlots, attachFifaGoals } from "@/lib/feeds"; import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings"; import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket"; import { securelyQualifiedThirdTeams } from "@/lib/third-place-security"; @@ -32,6 +32,11 @@ export async function GET() { try { const fifaData = await fetchFifaScores(); matches = applyFifaScores(matches, teams, fifaData); + try { + matches = await attachFifaGoals(matches, fifaData); + } catch (err) { + console.warn("[fifa] Goals fehlgeschlagen:", err instanceof Error ? err.message : err); + } } catch (err) { console.warn("[fifa] Scores fehlgeschlagen:", err instanceof Error ? err.message : err); } diff --git a/app/components/KoFixtures.tsx b/app/components/KoFixtures.tsx index 4e6711c..485b76f 100644 --- a/app/components/KoFixtures.tsx +++ b/app/components/KoFixtures.tsx @@ -198,10 +198,9 @@ export default function KoFixtures({ teams, matches }: { teams: Team[]; matches:
{m.goals.map((g, i) => ( - {g.team === "home" ? "⬆" : "⬇"} + {g.scorer} diff --git a/lib/feeds.ts b/lib/feeds.ts index 1e95460..21ae85b 100644 --- a/lib/feeds.ts +++ b/lib/feeds.ts @@ -1,4 +1,4 @@ -import { GroupId, Match, MatchStatus, Team } from "./types"; +import { GroupId, GoalEvent, Match, MatchStatus, Team } from "./types"; import { venueFor } from "./venues"; import { localisedTeamName } from "./team-mappings"; import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket"; @@ -578,6 +578,8 @@ interface FifaTeamBlock { interface FifaMatch { MatchNumber: number; + IdMatch?: string | null; + IdStage?: string | null; Home: FifaTeamBlock | null; Away: FifaTeamBlock | null; HomeTeamScore: number | null; @@ -599,6 +601,8 @@ interface FifaScores { status: MatchStatus; matchTime: string | null; winnerTeamId: string | null; + idMatch: string | null; + idStage: string | null; } // Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück. @@ -641,6 +645,8 @@ export async function fetchFifaScores(): Promise<{ status, matchTime: fm.MatchTime, winnerTeamId: fm.Winner ?? null, + idMatch: fm.IdMatch ?? null, + idStage: fm.IdStage ?? null, }); } return { scores, fifaIdToAppCode }; @@ -692,4 +698,84 @@ export function applyFifaScores( }); if (applied > 0) console.log("[fifa] scores angewandt:", applied); return result; +} + +// Holt Tor-Details pro Spiel vom FIFA-Detail-Endpoint. +interface FifaGoalRaw { scorer: string; minute: string; team: "home" | "away"; type: number | null; } + +function locName(arr: Array<{ Locale: string; Description: string }> | undefined): string { + if (!arr || arr.length === 0) return ""; + return (arr.find(x => x.Locale === "de-DE") + ?? arr.find(x => x.Locale === "en-GB") + ?? arr[0])?.Description ?? ""; +} + +function normMinuteStr(min: string | null | undefined): string { + return (min ?? "").replace(/'/g, "").replace(/\s+/g, "").trim(); +} + +async function fetchFifaGoals(idStage: string, idMatch: string): Promise { + return cached(`fifa:detail:${idMatch}`, 60_000, async () => { + const url = `${FIFA_BASE}/live/football/17/${FIFA_SEASON}/${idStage}/${idMatch}?language=de`; + const res = await fetch(url, { + cache: "no-store", + headers: { "User-Agent": "wm2026-board/1.0" }, + signal: AbortSignal.timeout(5000), + }); + if (!res.ok) { console.warn("[fifa-detail] status", res.status, idMatch); return []; } + const dj: any = await res.json(); + + const buildPlayerMap = (teamBlock: any): Map => { + const map = new Map(); + for (const p of teamBlock?.Players ?? []) { + map.set(String(p.IdPlayer), locName(p.PlayerName) || locName(p.ShortName)); + } + return map; + }; + const homeBlock = dj.HomeTeam, awayBlock = dj.AwayTeam; + const homePlayers = buildPlayerMap(homeBlock); + const awayPlayers = buildPlayerMap(awayBlock); + + const goals: FifaGoalRaw[] = []; + for (const [block, players, side] of [ + [homeBlock, homePlayers, "home"] as const, + [awayBlock, awayPlayers, "away"] as const, + ]) { + for (const g of block?.Goals ?? []) { + const min = normMinuteStr(g.Minute); + if (min === "" || isNaN(parseInt(min, 10))) continue; + goals.push({ + scorer: players.get(String(g.IdPlayer)) || "?", + minute: normMinuteStr(g.Minute), + team: side, + type: g.Type ?? null, + }); + } + } + goals.sort((a, b) => parseInt(a.minute, 10) - parseInt(b.minute, 10)); + return goals; + }); +} + +// Lädt Tor-Details für beendete/laufende Spiele mit Toren und hängt sie an die Matches an. +export async function attachFifaGoals( + matches: Match[], + fifaData: { scores: Map; fifaIdToAppCode: Map }, +): Promise { + const targets = matches.filter(m => + (m.status === "FINISHED" || m.status === "LIVE" || m.status === "IN_PLAY") && + ((m.homeScore ?? 0) + (m.awayScore ?? 0)) > 0, + ); + const goalsByMatchId = new Map(); + await Promise.all(targets.map(async (m) => { + const fs = fifaData.scores.get(m.matchNumber); + if (!fs?.idMatch || !fs?.idStage) return; + const goals = await fetchFifaGoals(fs.idStage, fs.idMatch); + if (goals.length) goalsByMatchId.set(m.id, goals); + })); + if (goalsByMatchId.size === 0) return matches; + return matches.map(m => { + const g = goalsByMatchId.get(m.id); + return g ? { ...m, goals: g.map(({ scorer, minute, team }) => ({ scorer, minute, team })) } : m; + }); } \ No newline at end of file diff --git a/lib/types.ts b/lib/types.ts index c966dd7..a975289 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -23,7 +23,7 @@ export type MatchStatus = export interface GoalEvent { scorer: string; // Torschützen-Name - minute: number; // Spielminute + minute: string; // Spielminute (z.B. "72", "90+1") team: "home" | "away"; }