goals
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -198,10 +198,9 @@ export default function KoFixtures({ teams, matches }: { teams: Team[]; matches:
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||
{m.goals.map((g, i) => (
|
||||
<span key={i} style={{
|
||||
display: "inline-flex", alignItems: "center", gap: 3,
|
||||
color: g.team === "home" ? "var(--ink)" : "var(--ink)",
|
||||
display: "inline-flex", alignItems: "center", gap: 4,
|
||||
}}>
|
||||
{g.team === "home" ? "⬆" : "⬇"}
|
||||
<Flag team={g.team === "home" ? home : away} size={14} />
|
||||
<span style={{ color: "var(--turf)", fontWeight: 700 }}>
|
||||
{g.scorer}
|
||||
</span>
|
||||
|
||||
88
lib/feeds.ts
88
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<FifaGoalRaw[]> {
|
||||
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<string, string> => {
|
||||
const map = new Map<string, string>();
|
||||
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<number, FifaScores>; fifaIdToAppCode: Map<string, string> },
|
||||
): Promise<Match[]> {
|
||||
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<string, GoalEvent[]>();
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user