export type ViaplayTweaks = { /** Content deviceKey (NL DASH). */ deviceKey: string; /** Hoeveel dagen vooruit EPG syncen. */ syncDaysAhead: number; /** HTTP-timeout sync/play (ms). */ httpTimeoutMs: number; /** Minuten vóór start MPD/keys ophalen. */ preLiveMinutes: number; /** Interval van de pre-live worker (s). */ preLiveIntervalSeconds: number; /** Max gelijktijdige pre-live jobs (1 = veilig). */ preLiveConcurrency: number; /** Pauze tussen jobs (ms). */ preLiveStaggerMs: number; /** Pre-live worker aan/uit. */ preLiveEnabled: boolean; /** Keys ophalen als PSSH beschikbaar is. */ fetchKeysIfMissing: boolean; /** MPD-cache TTL (minuten) vóór her-play. */ mpdCacheTtlMinutes: number; /** CDM-timeout (ms). */ cdmTimeoutMs: number; /** Deel Odido WVD-pad. */ useOdidoWvd: boolean; }; export const VIAPLAY_TWEAK_DEFAULTS: ViaplayTweaks = { deviceKey: "pcdash-nl", syncDaysAhead: 3, httpTimeoutMs: 30_000, preLiveMinutes: 5, preLiveIntervalSeconds: 60, preLiveConcurrency: 1, preLiveStaggerMs: 12_000, preLiveEnabled: true, fetchKeysIfMissing: true, mpdCacheTtlMinutes: 180, cdmTimeoutMs: 60_000, useOdidoWvd: true, }; export const VIAPLAY_TWEAK_META: Array<{ key: keyof ViaplayTweaks; label: string; hint: string; kind: "number" | "boolean" | "string"; unit?: string; }> = [ { key: "deviceKey", label: "Device key", hint: "Viaplay content/play deviceKey (NL = pcdash-nl).", kind: "string", }, { key: "syncDaysAhead", label: "Agenda-dagen vooruit", hint: "Hoeveel dagen sportagenda syncen/tonen (max 3 aanbevolen).", kind: "number", }, { key: "httpTimeoutMs", label: "HTTP-timeout", hint: "Timeout per Viaplay-request.", kind: "number", unit: "ms", }, { key: "preLiveMinutes", label: "Pre-live venster", hint: "MPD/keys ophalen vanaf N minuten vóór start.", kind: "number", unit: "min", }, { key: "preLiveIntervalSeconds", label: "Worker-interval", hint: "Hoe vaak de pre-live worker draait.", kind: "number", unit: "s", }, { key: "preLiveConcurrency", label: "Pre-live concurrency", hint: "1 = veilig voor account; nooit hoger dan 2 zonder reden.", kind: "number", }, { key: "preLiveStaggerMs", label: "Pre-live stagger", hint: "Pauze tussen play/license-calls.", kind: "number", unit: "ms", }, { key: "preLiveEnabled", label: "Pre-live worker", hint: "Automatisch manifest/licentie ±5 min vóór start.", kind: "boolean", }, { key: "fetchKeysIfMissing", label: "Licenties bij miss", hint: "Licenties ophalen als het manifest dat vereist.", kind: "boolean", }, { key: "mpdCacheTtlMinutes", label: "Manifest-cache TTL", hint: "Hergebruik live-manifest tot TTL. Elke play her-checkt de MPD (Don't go wordt nooit uit cache gespeeld).", kind: "number", unit: "min", }, { key: "cdmTimeoutMs", label: "Licentie-timeout", hint: "Timeout voor licentie-aanvraag.", kind: "number", unit: "ms", }, { key: "useOdidoWvd", label: "Gedeeld device-bestand", hint: "Zelfde Widevine-devicebestand als Odido Scripts.", kind: "boolean", }, ]; export function mergeViaplayTweaks( raw: Partial | Record ): ViaplayTweaks { const d = VIAPLAY_TWEAK_DEFAULTS; const n = (v: unknown, fallback: number) => { const x = typeof v === "number" ? v : Number(v); return Number.isFinite(x) && x > 0 ? Math.floor(x) : fallback; }; const s = (v: unknown, fallback: string) => { const t = String(v ?? "").trim(); return t || fallback; }; return { deviceKey: s(raw.deviceKey, d.deviceKey), syncDaysAhead: Math.min(3, Math.max(1, n(raw.syncDaysAhead, d.syncDaysAhead))), httpTimeoutMs: Math.min(120_000, Math.max(5_000, n(raw.httpTimeoutMs, d.httpTimeoutMs))), preLiveMinutes: Math.min(60, Math.max(1, n(raw.preLiveMinutes, d.preLiveMinutes))), preLiveIntervalSeconds: Math.min( 600, Math.max(30, n(raw.preLiveIntervalSeconds, d.preLiveIntervalSeconds)) ), preLiveConcurrency: Math.min( 2, Math.max(1, n(raw.preLiveConcurrency, d.preLiveConcurrency)) ), preLiveStaggerMs: Math.min( 120_000, Math.max(0, n(raw.preLiveStaggerMs, d.preLiveStaggerMs)) ), preLiveEnabled: raw.preLiveEnabled !== false, fetchKeysIfMissing: raw.fetchKeysIfMissing !== false, mpdCacheTtlMinutes: Math.min( 24 * 60, Math.max(15, n(raw.mpdCacheTtlMinutes, d.mpdCacheTtlMinutes)) ), cdmTimeoutMs: Math.min(180_000, Math.max(10_000, n(raw.cdmTimeoutMs, d.cdmTimeoutMs))), useOdidoWvd: raw.useOdidoWvd !== false, }; } export type ViaplayEventSnapshot = { guid: string; mediaGuid: string; title: string; subtitle: string | null; category: string | null; sportLabel: string | null; sportBucket: string | null; startMs: number; endMs: number; imageUrl: string | null; pageUrl: string | null; channelName: string | null; stationId: string | null; mpdUrl: string | null; mpdCachedAt: string | null; hasPssh: boolean; hasKeys: boolean; accountIds: string[]; lastPlayError: string | null; lastPlayAt: string | null; }; export function isMpdCacheFresh( cachedAt: string | null | undefined, ttlMinutes: number, nowMs = Date.now() ): boolean { if (!cachedAt) return false; const t = Date.parse(cachedAt); if (!Number.isFinite(t)) return false; return nowMs - t < ttlMinutes * 60_000; }