Treat finished LIVE_EVENTs as hub replays, select meetings near now (not season tail), enrich meeting metadata, and retry VOD play without channelId.
234 lines
7.5 KiB
TypeScript
234 lines
7.5 KiB
TypeScript
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<ScheduleEventPublic[]> {
|
|
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<ScheduleEventPublic | null> {
|
|
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<F1tvHomePayload> {
|
|
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<string, ScheduleEventPublic[]>();
|
|
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 };
|
|
}
|