import type { ScheduleEventPublic } from "../viewer/schedule-events"; import { resolveF1tvImageUrl } from "./client"; import { loadF1tvConfigOnly } from "./settings"; import type { F1tvMeetingSnapshot, F1tvSessionSnapshot } from "./tweaks"; function phaseFor( startMs: number, endMs: number, state: string | null, nowMs: number ): "live" | "upcoming" | "ended" { const st = (state || "").toLowerCase(); if (st === "live" || (st.includes("live") && !st.includes("pre"))) { if (nowMs <= endMs + 15 * 60_000) return "live"; } if (nowMs < startMs) return "upcoming"; if (nowMs > endMs) return "ended"; return "live"; } function toEventId(contentId: number): string { return `f1tv:${contentId}`; } export function sessionToPublic( session: F1tvSessionSnapshot, nowMs: number ): ScheduleEventPublic { const start = new Date(session.startMs).toISOString(); const stop = new Date(session.endMs).toISOString(); const phase = phaseFor(session.startMs, session.endMs, session.state, nowMs); const isReplayKind = session.kind === "replay" || phase === "ended"; const playable = (session.kind === "live_event" && phase === "live") || isReplayKind; const image = resolveF1tvImageUrl(session.imageUrl); return { id: toEventId(session.contentId), name: session.title, description: session.meetingName, provider: "f1tv", category: "racing", eventType: isReplayKind ? "replay" : "live", subtitle: session.series, channel: "F1 TV", network: "F1 TV", isLive: phase === "live", phase, playable, status: session.state, start, stop, startLocal: null, stopLocal: null, sportBucket: "racing", sportLabel: "Racing", imageLandscape: image, imageBackdrop: image, imagePortrait: null, logoUrl: null, images: image ? [image] : [], audioLanguages: [], }; } /** Upcoming + live F1 sessions for the main Events tab (no replays). */ export async function listF1tvScheduleEvents( nowMs = Date.now() ): Promise { const config = await loadF1tvConfigOnly(); if (!config?.sessions?.length) return []; const out: ScheduleEventPublic[] = []; for (const session of config.sessions) { if (session.kind !== "live_event") continue; const pub = sessionToPublic(session, nowMs); if (pub.phase === "ended") continue; // Events-tab: alleen echte live speelbaar, niet ended-as-replay. out.push({ ...pub, playable: pub.phase === "live", eventType: "live", }); } return out; } export async function getF1tvScheduleEvent( eventId: string, nowMs = Date.now() ): Promise { const m = /^f1tv:(\d+)$/i.exec(eventId.trim()); if (!m) return null; const contentId = Number(m[1]); const config = await loadF1tvConfigOnly(); const session = config?.sessions.find((s) => s.contentId === contentId); if (!session) return null; const pub = sessionToPublic(session, nowMs); // Events detail: geen replay-play via schedule-play pad. if (session.kind === "live_event" && pub.phase !== "ended") { return { ...pub, playable: pub.phase === "live", eventType: "live" }; } if (session.kind === "live_event" && pub.phase === "ended") { return { ...pub, playable: false, eventType: "live" }; } return pub; } export type F1tvReplayGroup = { meetingKey: string; title: string; location: string | null; meetingNumber: number | null; imageUrl: string | null; latestStart: string; sessions: ScheduleEventPublic[]; }; export type F1tvHomePayload = { generatedAt: string; live: ScheduleEventPublic[]; upcoming: ScheduleEventPublic[]; recentReplays: ScheduleEventPublic[]; replayGroups: F1tvReplayGroup[]; meetings: F1tvMeetingSnapshot[]; nextMeeting: F1tvMeetingSnapshot | null; }; export async function getF1tvHome(nowMs = Date.now()): Promise { const config = await loadF1tvConfigOnly(); const sessions = config?.sessions ?? []; const meetings = [...(config?.meetings ?? [])].sort((a, b) => { const am = a.startMs ?? 0; const bm = b.startMs ?? 0; return am - bm; }); const live: ScheduleEventPublic[] = []; const upcoming: ScheduleEventPublic[] = []; const recentReplays: ScheduleEventPublic[] = []; for (const session of sessions) { const pub = sessionToPublic(session, nowMs); if (session.kind === "replay" || pub.phase === "ended") { // Terugkijken: echte REPLAY-VODs én afgelopen LIVE_EVENT (2026-sessies). recentReplays.push({ ...pub, eventType: "replay", playable: true }); continue; } if (pub.phase === "live") live.push(pub); else if (pub.phase === "upcoming") upcoming.push(pub); } recentReplays.sort((a, b) => Date.parse(b.start) - Date.parse(a.start)); upcoming.sort((a, b) => Date.parse(a.start) - Date.parse(b.start)); const byMeeting = new Map(); for (const r of recentReplays) { const session = sessions.find((s) => toEventId(s.contentId) === r.id); const key = session?.meetingKey || r.description || "other"; const list = byMeeting.get(key) ?? []; list.push(r); byMeeting.set(key, list); } const replayGroups: F1tvReplayGroup[] = [...byMeeting.entries()] .map(([meetingKey, groupSessions]) => { const meeting = meetings.find((m) => m.meetingKey === meetingKey); const sorted = [...groupSessions].sort( (a, b) => Date.parse(b.start) - Date.parse(a.start) ); return { meetingKey, title: meeting?.title || sorted[0]?.description || meetingKey, location: meeting?.location ?? null, meetingNumber: meeting?.meetingNumber ?? null, imageUrl: resolveF1tvImageUrl(meeting?.imageUrl) || sorted[0]?.imageLandscape || null, latestStart: sorted[0]?.start || "", sessions: sorted, }; }) .sort((a, b) => Date.parse(b.latestStart) - Date.parse(a.latestStart)); let nextMeeting: F1tvMeetingSnapshot | null = null; for (const meeting of meetings) { const start = meeting.startMs ?? 0; const end = meeting.endMs ?? start; if (end + 86_400_000 >= nowMs) { nextMeeting = meeting; break; } } if (!nextMeeting && meetings.length) { nextMeeting = meetings[meetings.length - 1] ?? null; } return { generatedAt: new Date(nowMs).toISOString(), live, upcoming: upcoming.slice(0, 40), recentReplays: recentReplays.slice(0, 60), replayGroups: replayGroups.slice(0, 20), meetings, nextMeeting, }; } export async function getF1tvMeetingDetail( meetingKey: string, nowMs = Date.now() ): Promise<{ meeting: F1tvMeetingSnapshot | null; sessions: ScheduleEventPublic[]; replays: ScheduleEventPublic[]; upcoming: ScheduleEventPublic[]; live: ScheduleEventPublic[]; } | null> { const config = await loadF1tvConfigOnly(); if (!config) return null; const key = meetingKey.trim(); const meeting = config.meetings.find((m) => m.meetingKey === key) ?? null; const sessions = config.sessions .filter((s) => s.meetingKey === key) .map((s) => sessionToPublic(s, nowMs)) .sort((a, b) => Date.parse(a.start) - Date.parse(b.start)); if (!meeting && !sessions.length) return null; const live = sessions.filter((s) => s.phase === "live"); const upcoming = sessions.filter((s) => s.phase === "upcoming"); const replays = sessions .filter((s) => s.phase === "ended" || s.eventType === "replay") .map((s) => ({ ...s, eventType: "replay", playable: true })) .sort((a, b) => Date.parse(b.start) - Date.parse(a.start)); return { meeting, sessions, replays, upcoming, live }; }