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.
This commit is contained in:
Jos Vooges | STH 2026-09-28 03:36:08 +02:00
parent b783ba137f
commit 7cdff2bde4
4 changed files with 52 additions and 5 deletions

View file

@ -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", 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", "Content-Type": "application/octet-stream",
} }
lic_url = data.get("licUrl") or ""
auth = (data.get("licAuth") or "").strip() 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}" headers["Authorization"] = f"Basic {auth}"
response = requests.post( response = requests.post(
data["licUrl"], data["licUrl"],

View file

@ -1242,6 +1242,32 @@ export async function fetchMpdXml(
return { status: response.status, xml, url }; 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<string | null> {
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 { export function extractPsshFromMpd(xml: string): string | null {
const widevine = xml.match( const widevine = xml.match(
/<ContentProtection\b[^>]*schemeIdUri=["']urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed["'][\s\S]*?<\/ContentProtection>/i /<ContentProtection\b[^>]*schemeIdUri=["']urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed["'][\s\S]*?<\/ContentProtection>/i

View file

@ -13,6 +13,7 @@ import {
resolveKapMpd, resolveKapMpd,
fetchMpdXml, fetchMpdXml,
extractPsshFromMpd, extractPsshFromMpd,
fetchViaplayBootstrapPssh,
VIAPLAY_USER_AGENT, VIAPLAY_USER_AGENT,
} from "./client"; } from "./client";
import { import {
@ -243,10 +244,15 @@ export async function ensureViaplayEventReady(
let keys = cachedKeys; let keys = cachedKeys;
const wantKeys = opts?.fetchKeys ?? tweaks.fetchKeysIfMissing; 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({ const extracted = await extractViaplayKeys({
wvdPath: settings.wvdPath, wvdPath: settings.wvdPath,
pssh: kap.pssh, pssh,
licUrl: play.licenseUrl, licUrl: play.licenseUrl,
licAuth: play.licenseAuth, licAuth: play.licenseAuth,
userAgent: VIAPLAY_USER_AGENT, 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, { await patchViaplayEventCache(mediaGuid, {
mpdUrl: kap.mpdUrl, mpdUrl: kap.mpdUrl,
mpdCachedAt: new Date().toISOString(), mpdCachedAt: new Date().toISOString(),
@ -409,7 +419,13 @@ export async function resolveViaplayGuidPlay(
// Clear VOD (HAR: geen license) — nooit stale ClearKey van eerdere plays plakken. // Clear VOD (HAR: geen license) — nooit stale ClearKey van eerdere plays plakken.
if (!play.licenseUrl) { if (!play.licenseUrl) {
keys = []; 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({ const extracted = await extractViaplayKeys({
wvdPath: settings.wvdPath, wvdPath: settings.wvdPath,
pssh, pssh,
@ -442,6 +458,9 @@ export async function resolveViaplayGuidPlay(
} }
} }
} }
if (!keys.length) {
throw new Error("Viaplay-licentie aanwezig maar geen VOD-keys");
}
} }
recordVodPlay({ recordVodPlay({

View file

@ -1116,7 +1116,7 @@ export async function getScheduleEventPlay(
if (!providerAllowed(pub.provider, disabledGlobal, access?.allowedProviders ?? null)) { if (!providerAllowed(pub.provider, disabledGlobal, access?.allowedProviders ?? null)) {
throw new AppError("NOT_FOUND", "Event niet gevonden", 404); 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); throw new AppError("GONE", "Dit event is afgelopen", 410);
} }
if (pub.phase === "upcoming") { if (pub.phase === "upcoming") {