From d1c3176ffe8277ad2e65df268a9f4357fc287d5f Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Mon, 28 Sep 2026 03:45:54 +0200 Subject: [PATCH] Fix Viaplay catch-up without live replay. Reuse the channel MPD and keys with a KAP window instead of play-live plus CDM per account. --- apps/master-api/src/viaplay/client.ts | 14 ++++-- apps/master-api/src/viaplay/play.ts | 66 ++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts index 49494b3..ba2b7d3 100644 --- a/apps/master-api/src/viaplay/client.ts +++ b/apps/master-api/src/viaplay/client.ts @@ -1204,25 +1204,31 @@ export async function playByGuid( throw lastError || new Error("Viaplay VOD-play mislukt"); } +/** Haal een eventuele /start/N/stop/N window uit de MPD-URL. */ +export function stripKapWindow(mpdUrl: string): string { + return mpdUrl.replace(/\/start\/\d+\/stop\/\d+/g, ""); +} + /** Bouw MPD-URL met EPG start/stop kap (seconden). */ export function kapMpdUrl( mpdUrl: string, startMs: number, endMs: number ): string[] { + const base = stripKapWindow(mpdUrl); const startSec = Math.floor(startMs / 1000); const stopSec = Math.floor(endMs / 1000); try { - const u = new URL(mpdUrl); + const u = new URL(base); const file = u.pathname.split("/").pop() || "dash.mpd"; - const dir = mpdUrl.replace(/\/[^/]+$/, ""); + const dir = base.replace(/\/[^/]+$/, ""); return [ `${dir}/start/${startSec}/stop/${stopSec}/${file}`, `${dir}/start/${startSec}/stop/${stopSec}/index.mpd`, - mpdUrl, + base, ]; } catch { - return [mpdUrl]; + return [base]; } } diff --git a/apps/master-api/src/viaplay/play.ts b/apps/master-api/src/viaplay/play.ts index e1210c2..182ffa2 100644 --- a/apps/master-api/src/viaplay/play.ts +++ b/apps/master-api/src/viaplay/play.ts @@ -161,6 +161,23 @@ function accountOrder( return ordered; } +function channelMpdFromEvents( + events: ViaplayEventSnapshot[], + event: ViaplayEventSnapshot +): string | null { + if (event.mpdUrl?.trim()) return event.mpdUrl.trim(); + const channel = event.channelName?.trim().toLowerCase(); + const station = event.stationId?.trim(); + if (!channel && !station) return null; + for (const other of events) { + const url = other.mpdUrl?.trim(); + if (!url) continue; + if (station && other.stationId?.trim() === station) return url; + if (channel && other.channelName?.trim().toLowerCase() === channel) return url; + } + return null; +} + export async function ensureViaplayEventReady( sessionSecret: string, event: ViaplayEventSnapshot, @@ -174,13 +191,17 @@ export async function ensureViaplayEventReady( event.accountIds || []; + const ended = Date.now() > event.endMs; + const cachedMpd = channelMpdFromEvents(settings.config.events, event); + let cachedKeys = await keysForMediaGuid(sessionSecret, mediaGuid); if (!cachedKeys.length) { - cachedKeys = await keysForChannelPath(sessionSecret, event.mpdUrl); + cachedKeys = await keysForChannelPath(sessionSecret, event.mpdUrl || cachedMpd); } const cacheFresh = !opts?.forceRefresh && + !ended && !!event.mpdUrl && isMpdCacheFresh(event.mpdCachedAt, tweaks.mpdCacheTtlMinutes); @@ -194,6 +215,40 @@ export async function ensureViaplayEventReady( }; } + // Terugkijken: zelfde pad als het werkende script — KAP op zender-MPD, geen play-live. + if (ended && cachedMpd && !opts?.forceRefresh) { + try { + const kap = await resolveKapMpd( + cachedMpd, + event.startMs, + event.endMs, + tweaks.httpTimeoutMs + ); + if (!cachedKeys.length) { + cachedKeys = await keysForChannelPath(sessionSecret, kap.mpdUrl); + } + if (cachedKeys.length) { + await patchViaplayEventCache(mediaGuid, { + mpdUrl: kap.mpdUrl, + mpdCachedAt: new Date().toISOString(), + hasPssh: kap.hasPssh, + hasKeys: true, + lastPlayError: null, + lastPlayAt: new Date().toISOString(), + }); + return { + streamUrl: kap.mpdUrl, + format: "dash", + keys: cachedKeys, + fromCache: true, + mediaGuid, + }; + } + } catch { + /* play-live hieronder */ + } + } + // Cache MPD zonder keys is ok als keys later komen — of clear feed if (cacheFresh && event.mpdUrl && !(opts?.fetchKeys ?? tweaks.fetchKeysIfMissing)) { return { @@ -243,6 +298,9 @@ export async function ensureViaplayEventReady( ); let keys = cachedKeys; + if (!keys.length) { + keys = await keysForChannelPath(sessionSecret, play.mpdUrl); + } const wantKeys = opts?.fetchKeys ?? tweaks.fetchKeysIfMissing; if (wantKeys && play.licenseUrl && !keys.length) { const pssh = @@ -314,7 +372,11 @@ export async function ensureViaplayEventReady( lastPlayError: lastError?.message || "Play mislukt", lastPlayAt: new Date().toISOString(), }); - throw lastError || new Error("Viaplay play mislukt"); + throw new AppError( + "UPSTREAM", + lastError?.message || "Viaplay play mislukt", + 502 + ); } export async function resolveViaplayPlay(