From 7cdff2bde4ba892855f1290ea8978f1de6e14356 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Mon, 28 Sep 2026 03:36:08 +0200 Subject: [PATCH] Fix Viaplay live and catch-up play. Allow 48h catch-up instead of rejecting ended events, and fetch keys via bootstrap PSSH when the live MPD has none. --- apps/master-api/scripts/viaplay_cdm.py | 4 ++- apps/master-api/src/viaplay/client.ts | 26 +++++++++++++++++++ apps/master-api/src/viaplay/play.ts | 25 +++++++++++++++--- apps/master-api/src/viewer/schedule-events.ts | 2 +- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/apps/master-api/scripts/viaplay_cdm.py b/apps/master-api/scripts/viaplay_cdm.py index 01bfd3e..2d4b9bf 100644 --- a/apps/master-api/scripts/viaplay_cdm.py +++ b/apps/master-api/scripts/viaplay_cdm.py @@ -24,8 +24,10 @@ def main(): or "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36", "Content-Type": "application/octet-stream", } + lic_url = data.get("licUrl") or "" auth = (data.get("licAuth") or "").strip() - if auth: + # Token zit al in de URL — extra Authorization/Cookie geeft 431/403 (werkend script). + if auth and "token=" not in lic_url and "auth=" not in lic_url: headers["Authorization"] = f"Basic {auth}" response = requests.post( data["licUrl"], diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts index 190b8c6..49494b3 100644 --- a/apps/master-api/src/viaplay/client.ts +++ b/apps/master-api/src/viaplay/client.ts @@ -1242,6 +1242,32 @@ export async function fetchMpdXml( return { status: response.status, xml, url }; } +/** + * Live MPD's zijn soms zonder PSSH. Het werkende Viaplay-script gebruikt + * dan de Widevine-PSSH van een bekende Xtra-zender voor de license-challenge. + */ +const VIAPLAY_BOOTSTRAP_MPD = + "https://live-dash-cdn7-cf-mp-v2.cdn.viaplay.tv/mediapkg-prod-eu-north-1/vp-xtra16/vp-xtra16-dashwv/index.mpd"; + +let bootstrapPsshCache: string | null = null; + +export async function fetchViaplayBootstrapPssh( + timeoutMs: number +): Promise { + if (bootstrapPsshCache) return bootstrapPsshCache; + try { + const res = await fetchMpdXml(VIAPLAY_BOOTSTRAP_MPD, Math.min(timeoutMs, 15_000)); + const pssh = extractPsshFromMpd(res.xml); + if (pssh) { + bootstrapPsshCache = pssh; + return pssh; + } + } catch { + /* next play mag opnieuw proberen */ + } + return null; +} + export function extractPsshFromMpd(xml: string): string | null { const widevine = xml.match( /]*schemeIdUri=["']urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed["'][\s\S]*?<\/ContentProtection>/i diff --git a/apps/master-api/src/viaplay/play.ts b/apps/master-api/src/viaplay/play.ts index 397f8e3..e1210c2 100644 --- a/apps/master-api/src/viaplay/play.ts +++ b/apps/master-api/src/viaplay/play.ts @@ -13,6 +13,7 @@ import { resolveKapMpd, fetchMpdXml, extractPsshFromMpd, + fetchViaplayBootstrapPssh, VIAPLAY_USER_AGENT, } from "./client"; import { @@ -243,10 +244,15 @@ export async function ensureViaplayEventReady( let keys = cachedKeys; const wantKeys = opts?.fetchKeys ?? tweaks.fetchKeysIfMissing; - if (wantKeys && kap.pssh && play.licenseUrl && !keys.length) { + if (wantKeys && play.licenseUrl && !keys.length) { + const pssh = + kap.pssh || (await fetchViaplayBootstrapPssh(tweaks.httpTimeoutMs)); + if (!pssh) { + throw new Error("Geen PSSH (MPD + bootstrap) — keys niet op te halen"); + } const extracted = await extractViaplayKeys({ wvdPath: settings.wvdPath, - pssh: kap.pssh, + pssh, licUrl: play.licenseUrl, licAuth: play.licenseAuth, userAgent: VIAPLAY_USER_AGENT, @@ -275,6 +281,10 @@ export async function ensureViaplayEventReady( } } + if (play.licenseUrl && !keys.length) { + throw new Error("Viaplay-licentie aanwezig maar geen keys"); + } + await patchViaplayEventCache(mediaGuid, { mpdUrl: kap.mpdUrl, mpdCachedAt: new Date().toISOString(), @@ -409,7 +419,13 @@ export async function resolveViaplayGuidPlay( // Clear VOD (HAR: geen license) — nooit stale ClearKey van eerdere plays plakken. if (!play.licenseUrl) { keys = []; - } else if (wantKeys && pssh && !keys.length) { + } else if (wantKeys && !keys.length) { + if (!pssh) { + pssh = await fetchViaplayBootstrapPssh(tweaks.httpTimeoutMs); + } + if (!pssh) { + throw new Error("Geen PSSH (MPD + bootstrap) — VOD-keys niet op te halen"); + } const extracted = await extractViaplayKeys({ wvdPath: settings.wvdPath, pssh, @@ -442,6 +458,9 @@ export async function resolveViaplayGuidPlay( } } } + if (!keys.length) { + throw new Error("Viaplay-licentie aanwezig maar geen VOD-keys"); + } } recordVodPlay({ diff --git a/apps/master-api/src/viewer/schedule-events.ts b/apps/master-api/src/viewer/schedule-events.ts index b490704..7d2402d 100644 --- a/apps/master-api/src/viewer/schedule-events.ts +++ b/apps/master-api/src/viewer/schedule-events.ts @@ -1116,7 +1116,7 @@ export async function getScheduleEventPlay( if (!providerAllowed(pub.provider, disabledGlobal, access?.allowedProviders ?? null)) { throw new AppError("NOT_FOUND", "Event niet gevonden", 404); } - if (pub.phase === "ended") { + if (pub.phase === "ended" && !pub.playable) { throw new AppError("GONE", "Dit event is afgelopen", 410); } if (pub.phase === "upcoming") {