F1 metadata returns paths like 1000…/id, not full HTTPS URLs — hub cards and hero stayed blank without this.
168 lines
5 KiB
TypeScript
168 lines
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 playable = session.kind === "live_event" && phase === "live";
|
|
const image = resolveF1tvImageUrl(session.imageUrl);
|
|
|
|
return {
|
|
id: toEventId(session.contentId),
|
|
name: session.title,
|
|
description: session.meetingName,
|
|
provider: "f1tv",
|
|
category: "racing",
|
|
eventType: session.kind === "replay" ? "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;
|
|
out.push(pub);
|
|
}
|
|
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;
|
|
return sessionToPublic(session, nowMs);
|
|
}
|
|
|
|
export type F1tvHomePayload = {
|
|
generatedAt: string;
|
|
live: ScheduleEventPublic[];
|
|
upcoming: ScheduleEventPublic[];
|
|
recentReplays: ScheduleEventPublic[];
|
|
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 ?? [];
|
|
|
|
const live: ScheduleEventPublic[] = [];
|
|
const upcoming: ScheduleEventPublic[] = [];
|
|
const recentReplays: ScheduleEventPublic[] = [];
|
|
|
|
for (const session of sessions) {
|
|
const pub = sessionToPublic(session, nowMs);
|
|
if (session.kind === "replay") {
|
|
if (pub.phase === "ended" || session.startMs < nowMs) {
|
|
recentReplays.push(pub);
|
|
}
|
|
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));
|
|
|
|
let nextMeeting: F1tvMeetingSnapshot | null = null;
|
|
const upcomingMeetingKeys = new Set(upcoming.map((e) => {
|
|
const s = sessions.find((x) => toEventId(x.contentId) === e.id);
|
|
return s?.meetingKey;
|
|
}));
|
|
for (const meeting of meetings) {
|
|
if (upcomingMeetingKeys.has(meeting.meetingKey)) {
|
|
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, 24),
|
|
meetings,
|
|
nextMeeting,
|
|
};
|
|
}
|
|
|
|
export async function getF1tvMeetingDetail(
|
|
meetingKey: string,
|
|
nowMs = Date.now()
|
|
): Promise<{
|
|
meeting: F1tvMeetingSnapshot | null;
|
|
sessions: 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;
|
|
return { meeting, sessions };
|
|
}
|