goals
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
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 { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
|
||||||
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
||||||
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
|
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
|
||||||
@@ -32,6 +32,11 @@ export async function GET() {
|
|||||||
try {
|
try {
|
||||||
const fifaData = await fetchFifaScores();
|
const fifaData = await fetchFifaScores();
|
||||||
matches = applyFifaScores(matches, teams, fifaData);
|
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) {
|
} catch (err) {
|
||||||
console.warn("[fifa] Scores fehlgeschlagen:", err instanceof Error ? err.message : 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 }}>
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||||
{m.goals.map((g, i) => (
|
{m.goals.map((g, i) => (
|
||||||
<span key={i} style={{
|
<span key={i} style={{
|
||||||
display: "inline-flex", alignItems: "center", gap: 3,
|
display: "inline-flex", alignItems: "center", gap: 4,
|
||||||
color: g.team === "home" ? "var(--ink)" : "var(--ink)",
|
|
||||||
}}>
|
}}>
|
||||||
{g.team === "home" ? "⬆" : "⬇"}
|
<Flag team={g.team === "home" ? home : away} size={14} />
|
||||||
<span style={{ color: "var(--turf)", fontWeight: 700 }}>
|
<span style={{ color: "var(--turf)", fontWeight: 700 }}>
|
||||||
{g.scorer}
|
{g.scorer}
|
||||||
</span>
|
</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 { venueFor } from "./venues";
|
||||||
import { localisedTeamName } from "./team-mappings";
|
import { localisedTeamName } from "./team-mappings";
|
||||||
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
|
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
|
||||||
@@ -578,6 +578,8 @@ interface FifaTeamBlock {
|
|||||||
|
|
||||||
interface FifaMatch {
|
interface FifaMatch {
|
||||||
MatchNumber: number;
|
MatchNumber: number;
|
||||||
|
IdMatch?: string | null;
|
||||||
|
IdStage?: string | null;
|
||||||
Home: FifaTeamBlock | null;
|
Home: FifaTeamBlock | null;
|
||||||
Away: FifaTeamBlock | null;
|
Away: FifaTeamBlock | null;
|
||||||
HomeTeamScore: number | null;
|
HomeTeamScore: number | null;
|
||||||
@@ -599,6 +601,8 @@ interface FifaScores {
|
|||||||
status: MatchStatus;
|
status: MatchStatus;
|
||||||
matchTime: string | null;
|
matchTime: string | null;
|
||||||
winnerTeamId: 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.
|
// 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,
|
status,
|
||||||
matchTime: fm.MatchTime,
|
matchTime: fm.MatchTime,
|
||||||
winnerTeamId: fm.Winner ?? null,
|
winnerTeamId: fm.Winner ?? null,
|
||||||
|
idMatch: fm.IdMatch ?? null,
|
||||||
|
idStage: fm.IdStage ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { scores, fifaIdToAppCode };
|
return { scores, fifaIdToAppCode };
|
||||||
@@ -693,3 +699,83 @@ export function applyFifaScores(
|
|||||||
if (applied > 0) console.log("[fifa] scores angewandt:", applied);
|
if (applied > 0) console.log("[fifa] scores angewandt:", applied);
|
||||||
return result;
|
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 {
|
export interface GoalEvent {
|
||||||
scorer: string; // Torschützen-Name
|
scorer: string; // Torschützen-Name
|
||||||
minute: number; // Spielminute
|
minute: string; // Spielminute (z.B. "72", "90+1")
|
||||||
team: "home" | "away";
|
team: "home" | "away";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user