Richer event context: buildup lines from the full play-by-play feed
'X is shown a red card' alone isn't insight — the story is in the surrounding plays. The ESPN summary carries a ~110-line play-by-play feed (attempts, fouls, corners, VAR checks) we cached but never used: - normalizer keeps a trimmed commentary list (schema v3; the boot backfill re-stores older matches automatically) - previews serve it; expanding a timeline event now shows up to four feed lines from the three minutes leading in, then the event's own description — e.g. Korea's saved attempt and corner right before Czechia's counter-goal header, or the foul behind a booking Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,11 @@ export interface EspnTimelineEvent {
|
||||
scoring?: boolean;
|
||||
shootout?: boolean;
|
||||
}
|
||||
/** Current normalizer schema — bump to make the boot backfill re-store. */
|
||||
export const SUMMARY_V = 3;
|
||||
|
||||
export interface EspnCommentaryLine { minute: number | null; text: string }
|
||||
|
||||
export interface EspnSummary {
|
||||
/** Normalizer schema version — bump to trigger the boot backfill. */
|
||||
v?: number;
|
||||
@@ -88,10 +93,13 @@ export interface EspnSummary {
|
||||
stats: Record<string, Record<string, string>>;
|
||||
/** Live/finished event timeline (goals, cards, subs). */
|
||||
events: EspnTimelineEvent[];
|
||||
/** Full play-by-play feed (attempts, fouls, corners, VAR…), kickoff first. */
|
||||
commentary?: EspnCommentaryLine[];
|
||||
}
|
||||
|
||||
export interface RawSummary {
|
||||
gameInfo?: { venue?: { fullName?: string } };
|
||||
commentary?: { sequence?: number | string; time?: { displayValue?: string }; text?: string }[];
|
||||
header?: { competitions?: { competitors?: { id: string; team?: { displayName?: string }; homeAway: 'home' | 'away' }[] }[] };
|
||||
headToHeadGames?: { events?: { gameDate: string; homeTeamId: string; awayTeamId: string; homeTeamScore: string; awayTeamScore: string }[] }[];
|
||||
boxscore?: {
|
||||
@@ -165,5 +173,14 @@ export function normalizeSummary(raw: RawSummary): EspnSummary {
|
||||
};
|
||||
});
|
||||
|
||||
return { v: 2, fetchedAt: Date.now(), venue: raw.gameInfo?.venue?.fullName ?? null, teams, h2h, form, lineups, stats, events };
|
||||
const commentary: EspnCommentaryLine[] = (raw.commentary ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => Number(a.sequence ?? 0) - Number(b.sequence ?? 0))
|
||||
.map((c) => {
|
||||
const m = /(\d+)/.exec(c.time?.displayValue ?? '');
|
||||
return { minute: m ? Number(m[1]) : null, text: c.text ?? '' };
|
||||
})
|
||||
.filter((c) => c.text);
|
||||
|
||||
return { v: SUMMARY_V, fetchedAt: Date.now(), venue: raw.gameInfo?.venue?.fullName ?? null, teams, h2h, form, lineups, stats, events, commentary };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { fetchFifaCalendar, fetchFifaLive, fetchFifaTimeline } from './fifa';
|
||||
import { fetchMatchWeather } from './weather';
|
||||
import { fetchScorerProps } from './espnProps';
|
||||
import type { EspnSummary } from './espnNormalize';
|
||||
import { SUMMARY_V } from './espnNormalize';
|
||||
import type { Fixture } from '../../../src/lib/types';
|
||||
|
||||
// The ingestion orchestrator. Replaces the old live loop: ESPN is the primary
|
||||
@@ -150,7 +151,7 @@ export function startScheduler(
|
||||
if (stopped) return;
|
||||
if (f.status === 'scheduled') continue;
|
||||
const ext = getMatchExt<EspnSummary>(f.num);
|
||||
if (ext && ext.data.v === 2) continue;
|
||||
if (ext && ext.data.v === SUMMARY_V) continue;
|
||||
const eid = getSourceMap(f.num, 'espn');
|
||||
if (!eid) continue;
|
||||
try {
|
||||
|
||||
@@ -130,6 +130,7 @@ export function buildPreview(num: number, state: TournamentState, model: ModelEn
|
||||
eh && ea && (Object.keys(summary!.stats[eh.id] ?? {}).length || Object.keys(summary!.stats[ea.id] ?? {}).length)
|
||||
? { home: summary!.stats[eh.id] ?? {}, away: summary!.stats[ea.id] ?? {} }
|
||||
: null,
|
||||
commentary: summary?.commentary ?? [],
|
||||
events: (summary?.events ?? []).map((e) => ({
|
||||
minute: e.minute,
|
||||
type: e.type,
|
||||
|
||||
Reference in New Issue
Block a user