Upcoming agenda sessions were hitting CONTENT/PLAY and surfacing a generic 500 to the TV app.
100 lines
3.2 KiB
TypeScript
100 lines
3.2 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;
|
|
|
|
let played: Awaited<ReturnType<typeof fetchContentPlay>>;
|
|
try {
|
|
played = await fetchContentPlay({
|
|
contentId,
|
|
channelId: preferredChannel,
|
|
auth: {
|
|
ascendontoken: account.ascendontoken,
|
|
entitlementtoken: account.entitlementtoken,
|
|
sessionId: ensureSessionId(account.sessionId),
|
|
},
|
|
locale: config.locale,
|
|
playPlatform: "WEB_HLS",
|
|
timeoutMs: config.syncTimeoutMs,
|
|
});
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
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)}`, 502);
|
|
}
|
|
|
|
const feed = pickDefaultFeed(played.feeds, preferredChannel);
|
|
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;
|
|
}
|