stremio/apps/master-api/src/f1tv/events.ts
Jos Vooges | STH 20a3cfb056 Fix F1TV cover URLs: resolve relative pictureUrl via image-resizer.
F1 metadata returns paths like 1000…/id, not full HTTPS URLs — hub cards and hero stayed blank without this.
2026-09-22 04:15:13 +02:00

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 };
}