Treat finished LIVE_EVENTs as hub replays, select meetings near now (not season tail), enrich meeting metadata, and retry VOD play without channelId.
126 lines
3.9 KiB
TypeScript
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;
|
|
}
|