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