stremio/apps/master-api/src/f1tv/play.ts
Jos Vooges | STH d8a1c28283 Fix F1TV 2026 terugkijken: ended live sessions + smarter meeting sync.
Treat finished LIVE_EVENTs as hub replays, select meetings near now (not season tail), enrich meeting metadata, and retry VOD play without channelId.
2026-09-22 04:34:24 +02:00

126 lines
3.9 KiB
TypeScript

import { AppError } from "../security/errors";
import { pickAccountIdForContent } from "./accounts";
import { ensureSessionId, fetchContentPlay, pickDefaultFeed } from "./client";
import { loadF1tvSettings } from "./settings";
import type { F1tvSessionSnapshot } from "./tweaks";
function detectFormat(url: string): "hls" | "dash" {
const u = url.toLowerCase();
if (u.includes(".m3u8") || u.includes("m3u8") || u.includes("WEB_HLS")) return "hls";
return "dash";
}
export async function resolveF1tvPlay(
sessionSecret: string,
contentId: number,
opts?: { channelId?: number | null; allowReplay?: boolean }
): Promise<{
contentId: number;
name: string;
streamUrl: string;
format: "hls" | "dash";
channelId: number | null;
feedCount: number;
session: F1tvSessionSnapshot | null;
}> {
const { accounts, config } = await loadF1tvSettings(sessionSecret);
const session =
config.sessions.find((s) => s.contentId === contentId) ?? null;
if (session?.kind === "replay" && opts?.allowReplay === false) {
throw new AppError("GONE", "Replay is niet beschikbaar via Events", 410);
}
const enabledIds = accounts.map((a) => a.id);
const owners = config.contentOwners[String(contentId)] ?? session?.accountIds ?? [];
const accountId = pickAccountIdForContent(String(contentId), owners, enabledIds);
const account = accounts.find((a) => a.id === accountId) ?? accounts[0];
if (!account?.ascendontoken || !account.entitlementtoken) {
throw new AppError("INVALID_REQUEST", "Geen F1TV-tokens voor play", 400);
}
const preferredChannel =
opts?.channelId ?? session?.defaultChannelId ?? 1033;
const isVod =
session?.kind === "replay" ||
(session != null && Date.now() > session.endMs);
// Replay/ended: eerst zonder channel (VOD), dan live-feed channel.
const channelAttempts: Array<number | null> = isVod
? [opts?.channelId ?? null, null, preferredChannel]
: [preferredChannel, null];
const auth = {
ascendontoken: account.ascendontoken,
entitlementtoken: account.entitlementtoken,
sessionId: ensureSessionId(account.sessionId),
};
let lastErr: unknown = null;
let played: Awaited<ReturnType<typeof fetchContentPlay>> | null = null;
let usedChannel: number | null = preferredChannel;
for (const channelId of channelAttempts) {
try {
played = await fetchContentPlay({
contentId,
channelId,
auth,
locale: config.locale,
playPlatform: "WEB_HLS",
timeoutMs: config.syncTimeoutMs,
});
usedChannel = channelId;
if (pickDefaultFeed(played.feeds, channelId)?.url) break;
} catch (err) {
lastErr = err;
played = null;
}
}
if (!played) {
const msg = lastErr instanceof Error ? lastErr.message : String(lastErr ?? "");
if (/HTTP 4\d\d|NOT_FOUND|UNAVAILABLE|FORBIDDEN/i.test(msg)) {
throw new AppError(
"NOT_READY",
session?.kind === "live_event" && session.startMs > Date.now()
? "Dit event is nog niet begonnen"
: "F1TV-stream is nu niet beschikbaar",
409
);
}
throw new AppError(
"UPSTREAM_ERROR",
`F1TV play mislukt: ${msg.slice(0, 160) || "onbekend"}`,
502
);
}
const feed = pickDefaultFeed(played.feeds, usedChannel);
if (!feed?.url) {
throw new AppError(
"NOT_READY",
session?.kind === "live_event" && session.startMs > Date.now()
? "Dit event is nog niet begonnen"
: "Geen F1TV-stream-URL ontvangen",
409
);
}
return {
contentId,
name: session?.title ?? `F1TV ${contentId}`,
streamUrl: feed.url,
format: detectFormat(feed.url),
channelId: feed.channelId,
feedCount: played.feeds.length,
session,
};
}
export function parseF1tvEventId(eventId: string): number | null {
const m = /^f1tv:(\d+)$/i.exec(eventId.trim());
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) ? n : null;
}