From 75969d87ec205306a0ec48a9948d5287a65d90f5 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Mon, 28 Sep 2026 20:27:08 +0200 Subject: [PATCH] Serve stored Viaplay DRM MPD+keys to clients before play-live, matching the admin key table. --- apps/master-api/src/viaplay/play.ts | 91 +++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/apps/master-api/src/viaplay/play.ts b/apps/master-api/src/viaplay/play.ts index bbdd224..c15c9d1 100644 --- a/apps/master-api/src/viaplay/play.ts +++ b/apps/master-api/src/viaplay/play.ts @@ -108,10 +108,19 @@ function parseKey(value: string): { kid: string; key: string } | null { return { kid: kid!, key: key! }; } -async function keysForMediaGuid( +function hasKapWindow(url: string | null | undefined): boolean { + return !!url && /\/start\/\d+\/stop\/\d+/.test(url); +} + +type StoredDrmPlay = { + keys: Array<{ kid: string; key: string }>; + mpdUrl: string | null; +}; + +async function storedDrmPlay( sessionSecret: string, mediaGuid: string -): Promise> { +): Promise { const rows = await prisma.drmKey.findMany({ where: { mediaGuid, source: "viaplay" }, orderBy: { lastSeenAt: "desc" }, @@ -126,18 +135,30 @@ async function keysForMediaGuid( rows.push(...alt); } const { decryptSecret } = await import("../security/crypto"); - const out: Array<{ kid: string; key: string }> = []; + const keys: Array<{ kid: string; key: string }> = []; const seen = new Set(); + let mpdUrl: string | null = null; for (const row of rows) { if (seen.has(row.kid)) continue; seen.add(row.kid); try { - out.push({ kid: row.kid, key: decryptSecret(row.keyEnc, sessionSecret) }); + keys.push({ kid: row.kid, key: decryptSecret(row.keyEnc, sessionSecret) }); } catch { - /* skip */ + continue; + } + const url = row.mpdUrl?.trim() || null; + if (!mpdUrl && url && /^https?:\/\//i.test(url) && !isDontGoPlaceholder(url)) { + mpdUrl = url; } } - return out; + return { keys, mpdUrl }; +} + +async function keysForMediaGuid( + sessionSecret: string, + mediaGuid: string +): Promise> { + return (await storedDrmPlay(sessionSecret, mediaGuid)).keys; } async function keysForChannelPath( @@ -262,9 +283,57 @@ export async function ensureViaplayEventReady( const ended = Date.now() > event.endMs; const cachedMpd = channelMpdFromEvents(settings.config.events, event); - let cachedKeys = await keysForMediaGuid(sessionSecret, mediaGuid); + const stored = await storedDrmPlay(sessionSecret, mediaGuid); + let cachedKeys = stored.keys; if (!cachedKeys.length) { - cachedKeys = await keysForChannelPath(sessionSecret, event.mpdUrl || cachedMpd); + cachedKeys = await keysForChannelPath( + sessionSecret, + stored.mpdUrl || event.mpdUrl || cachedMpd + ); + } + + // Zelfde bron als de DRM-tabel in admin: opgeslagen MPD + KID/KEY. + // Alleen opnieuw play-live als die set ontbreekt of Don't go is. + if (!opts?.forceRefresh && cachedKeys.length) { + let url = stored.mpdUrl || event.mpdUrl || cachedMpd; + if (url && isReusableLiveMpd(url)) { + if (ended && !hasKapWindow(url)) { + try { + const kap = await resolveKapMpd( + url, + event.startMs, + event.endMs, + Math.min(tweaks.httpTimeoutMs, 12_000) + ); + if (isReusableLiveMpd(kap.mpdUrl)) url = kap.mpdUrl; + } catch { + /* gebruik de drm-MPD zoals de losse speler */ + } + } + const trustStoredKap = ended && hasKapWindow(url); + const probed = + trustStoredKap || (await liveCachedMpdPlayable(url, tweaks.httpTimeoutMs)); + if (probed && url) { + recordLivePlay({ + guid: mediaGuid, + ok: true, + title: event.title, + mediaPath: mediaPathOf(url), + hasLicense: true, + fromCache: true, + cookies: emptyCookies(), + error: null, + }); + return { + streamUrl: url, + format: "dash", + keys: cachedKeys, + fromCache: true, + mediaGuid, + live: !ended && !hasKapWindow(url), + }; + } + } } const cacheFresh = @@ -291,7 +360,7 @@ export async function ensureViaplayEventReady( keys: cachedKeys, fromCache: true, mediaGuid, - live: !ended, + live: !ended && !hasKapWindow(event.mpdUrl), }; } await dropEventMpdCache( @@ -382,7 +451,7 @@ export async function ensureViaplayEventReady( keys: cachedKeys, fromCache: true, mediaGuid, - live: !ended, + live: !ended && !hasKapWindow(event.mpdUrl), }; } @@ -522,7 +591,7 @@ export async function ensureViaplayEventReady( fromCache: false, mediaGuid, accountId: account.id, - live: !ended, + live: !ended && !hasKapWindow(kap.mpdUrl), }; } catch (err) { lastError = err instanceof Error ? err : new Error(String(err));