Stop serving cached Viaplay Don't go MPDs and log every play including cache hits.

This commit is contained in:
Jos Vooges | STH 2026-09-28 20:15:33 +02:00
parent 8d54e2d8c6
commit 133f51284d
8 changed files with 264 additions and 99 deletions

View file

@ -56,6 +56,7 @@ type PlayLogEntry = {
hasClientId: boolean; hasClientId: boolean;
hasPersistentLogin: boolean; hasPersistentLogin: boolean;
}; };
fromCache?: boolean;
}; };
type ViaplaySettings = { type ViaplaySettings = {
@ -138,6 +139,7 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
const loadPlayLog = useCallback(async () => { const loadPlayLog = useCallback(async () => {
const r = await fetch("/api/v1/admin/scripts/viaplay/play-log", { const r = await fetch("/api/v1/admin/scripts/viaplay/play-log", {
credentials: "include", credentials: "include",
cache: "no-store",
}); });
if (!r.ok) return; if (!r.ok) return;
const d = (await r.json()) as { log?: PlayLogEntry[] }; const d = (await r.json()) as { log?: PlayLogEntry[] };
@ -485,7 +487,7 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
<h3 style={{ marginBottom: "0.5rem" }}>Play-diagnose</h3> <h3 style={{ marginBottom: "0.5rem" }}>Play-diagnose</h3>
<p className="muted" style={{ marginTop: 0, fontSize: "0.85rem" }}> <p className="muted" style={{ marginTop: 0, fontSize: "0.85rem" }}>
Geen cookie-waarden, wel namen + of Viaplay Don't go teruggaf. Speel iets in de Geen cookie-waarden, wel namen + of Viaplay Don't go teruggaf. Speel iets in de
app en kijk hier binnen een paar seconden. app — ook een cache-hit verschijnt hier binnen een paar seconden.
</p> </p>
{playLog.length === 0 ? ( {playLog.length === 0 ? (
<p className="muted">Nog geen plays sinds de laatste API-restart.</p> <p className="muted">Nog geen plays sinds de laatste API-restart.</p>
@ -512,11 +514,12 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
{e.accountLabel ? ` · ${e.accountLabel}` : ""} {e.accountLabel ? ` · ${e.accountLabel}` : ""}
{e.mediaPath ? ` · ${e.mediaPath}` : ""} {e.mediaPath ? ` · ${e.mediaPath}` : ""}
{e.hasLicense ? " · widevine" : " · geen licentie"} {e.hasLicense ? " · widevine" : " · geen licentie"}
{` · cookies ${e.cookies.count}`} {` · cookies ${e.cookies?.count ?? 0}`}
{e.cookies.hasDatadome ? " · datadome" : " · géén datadome"} {e.cookies?.hasDatadome ? " · datadome" : " · géén datadome"}
{e.cookies.hasBoid ? " · boid" : " · géén boid"} {e.cookies?.hasBoid ? " · boid" : " · géén boid"}
{e.cookies.sessionStoredDecoded ? " · session decoded" : ""} {e.cookies?.sessionStoredDecoded ? " · session decoded" : ""}
{e.cookies.sessionStoredEncoded ? " · session encoded" : ""} {e.cookies?.sessionStoredEncoded ? " · session encoded" : ""}
{e.fromCache ? " · cache" : ""}
</div> </div>
{(e.dontGoReason || e.error) && ( {(e.dontGoReason || e.error) && (
<div style={{ marginTop: 4 }}>{e.dontGoReason || e.error}</div> <div style={{ marginTop: 4 }}>{e.dontGoReason || e.error}</div>

View file

@ -1929,7 +1929,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
keyCount: played.keys.length, keyCount: played.keys.length,
promo, promo,
resolver: "viaplay-byguid-v10", resolver: "viaplay-byguid-v10",
lastPlay: (await import("../viaplay/play")).getLastVodPlay(), lastPlay: await (await import("../viaplay/play")).getLastVodPlay(),
}; };
} catch (err) { } catch (err) {
return { return {
@ -1937,17 +1937,22 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
guid, guid,
error: err instanceof Error ? err.message : String(err), error: err instanceof Error ? err.message : String(err),
resolver: "viaplay-byguid-v10", resolver: "viaplay-byguid-v10",
lastPlay: (await import("../viaplay/play")).getLastVodPlay(), lastPlay: await (await import("../viaplay/play")).getLastVodPlay(),
}; };
} }
} }
); );
/** Diagnose: laatste live/VOD plays (geen secrets, wel cookie-namen + Don't go-reden). */ /** Diagnose: laatste live/VOD plays (geen secrets, wel cookie-namen + Don't go-reden). */
app.get("/api/v1/admin/scripts/viaplay/play-log", { preHandler: requireAdmin }, async () => { app.get(
const { getViaplayPlayLog, getLastVodPlay } = await import("../viaplay/play"); "/api/v1/admin/scripts/viaplay/play-log",
return { last: getLastVodPlay(), log: getViaplayPlayLog() }; { preHandler: requireAdmin },
}); async (_request, reply) => {
reply.header("Cache-Control", "no-store, no-cache, must-revalidate");
const { getViaplayPlayLog, getLastVodPlay } = await import("../viaplay/play");
return { last: await getLastVodPlay(), log: await getViaplayPlayLog() };
}
);
app.post("/api/v1/admin/scripts/viaplay/sync", { preHandler: requireAdmin }, async () => { app.post("/api/v1/admin/scripts/viaplay/sync", { preHandler: requireAdmin }, async () => {
const { syncViaplayCatalog } = await import("../viaplay/sync"); const { syncViaplayCatalog } = await import("../viaplay/sync");

View file

@ -232,7 +232,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
keyCount: played.keys.length, keyCount: played.keys.length,
promo, promo,
resolver: "viaplay-byguid-har-v7", resolver: "viaplay-byguid-har-v7",
lastVodPlay: getLastVodPlay(), lastVodPlay: await getLastVodPlay(),
}; };
} catch (err) { } catch (err) {
return { return {
@ -240,7 +240,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
guid, guid,
error: err instanceof Error ? err.message : String(err), error: err instanceof Error ? err.message : String(err),
resolver: "viaplay-byguid-har-v7", resolver: "viaplay-byguid-har-v7",
lastVodPlay: getLastVodPlay(), lastVodPlay: await getLastVodPlay(),
}; };
} }
}); });

View file

@ -103,12 +103,12 @@ async function main() {
}); });
app.get("/health", async () => { app.get("/health", async () => {
const { getLastVodPlay } = await import("./viaplay/play"); const { peekLastViaplayPlay } = await import("./viaplay/play");
return { return {
status: "ok", status: "ok",
service: "master-api", service: "master-api",
version: "1.0.7-ziggo-gracenote", version: "1.0.7-ziggo-gracenote",
lastVodPlay: getLastVodPlay(), lastVodPlay: peekLastViaplayPlay(),
}; };
}); });

View file

@ -584,6 +584,11 @@ function cachedStreamHref(guid: string): string | null {
return hit.href; return hit.href;
} }
export function forgetStreamHref(guid: string): void {
const g = String(guid || "").trim();
if (g) streamHrefCache.delete(g);
}
function streamHref(product: Record<string, unknown>): string | null { function streamHref(product: Record<string, unknown>): string | null {
const links = asRecord(product._links); const links = asRecord(product._links);
const stream = asRecord(links?.["viaplay:stream"]); const stream = asRecord(links?.["viaplay:stream"]);
@ -1318,6 +1323,7 @@ export async function playByGuid(
isDontGoPlaceholder(mpd.url) || isDontGoPlaceholder(mpd.url) ||
mpdLooksLikeDontGo(mpd.xml, expectedMs) mpdLooksLikeDontGo(mpd.xml, expectedMs)
) { ) {
forgetStreamHref(guid);
throw new Error( throw new Error(
"Viaplay MPD is Don't go-placeholder (duur/media mismatch)" "Viaplay MPD is Don't go-placeholder (duur/media mismatch)"
); );

View file

@ -1,3 +1,4 @@
import { prisma } from "../database/client";
import type { CookieInventory } from "./cookies"; import type { CookieInventory } from "./cookies";
export type ViaplayPlayDiag = { export type ViaplayPlayDiag = {
@ -20,25 +21,85 @@ export type ViaplayPlayDiag = {
refreshOk: boolean | null; refreshOk: boolean | null;
error: string | null; error: string | null;
resolver: string; resolver: string;
fromCache?: boolean;
triedAccounts?: number; triedAccounts?: number;
triedLabels?: string[]; triedLabels?: string[];
}; };
const MAX = 25; const MAX = 25;
const PLAY_LOG_ID = "viaplay-play-log";
const log: ViaplayPlayDiag[] = []; const log: ViaplayPlayDiag[] = [];
let hydrating: Promise<void> | null = null;
function entryKey(e: ViaplayPlayDiag): string {
return `${e.at}|${e.kind}|${e.guid}|${e.mediaPath || ""}`;
}
function mergeStored(stored: ViaplayPlayDiag[]) {
const seen = new Set(log.map(entryKey));
for (const e of stored) {
if (!e || typeof e.at !== "string" || typeof e.guid !== "string") continue;
if (seen.has(entryKey(e))) continue;
seen.add(entryKey(e));
log.push(e);
}
log.sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
if (log.length > MAX) log.length = MAX;
}
async function hydrate(): Promise<void> {
if (!hydrating) {
hydrating = (async () => {
try {
const row = await prisma.integrationSetting.findUnique({
where: { id: PLAY_LOG_ID },
});
if (!row?.configJson) return;
const parsed = JSON.parse(row.configJson) as unknown;
if (Array.isArray(parsed)) mergeStored(parsed as ViaplayPlayDiag[]);
} catch {
/* lege log is ok */
}
})();
}
await hydrating;
}
async function persistLog(): Promise<void> {
try {
const snapshot = JSON.stringify(log.slice(0, MAX));
await prisma.integrationSetting.upsert({
where: { id: PLAY_LOG_ID },
create: { id: PLAY_LOG_ID, configJson: snapshot, enabled: true },
update: { configJson: snapshot },
});
} catch {
/* diagnose mag play niet breken */
}
}
export function recordViaplayPlay(entry: Omit<ViaplayPlayDiag, "at">): ViaplayPlayDiag { export function recordViaplayPlay(entry: Omit<ViaplayPlayDiag, "at">): ViaplayPlayDiag {
const full: ViaplayPlayDiag = { ...entry, at: new Date().toISOString() }; const full: ViaplayPlayDiag = { ...entry, at: new Date().toISOString() };
log.unshift(full); log.unshift(full);
if (log.length > MAX) log.length = MAX; if (log.length > MAX) log.length = MAX;
void hydrate()
.then(() => persistLog())
.catch(() => undefined);
return full; return full;
} }
export function getViaplayPlayLog(): ViaplayPlayDiag[] { export function peekLastViaplayPlay(): ViaplayPlayDiag | null {
return log[0] ?? null;
}
export async function getViaplayPlayLog(): Promise<ViaplayPlayDiag[]> {
await hydrate();
return log.slice(); return log.slice();
} }
export function getLastViaplayPlay(): ViaplayPlayDiag | null { export async function getLastViaplayPlay(): Promise<ViaplayPlayDiag | null> {
await hydrate();
return log[0] ?? null; return log[0] ?? null;
} }

View file

@ -15,9 +15,11 @@ import {
extractPsshFromMpd, extractPsshFromMpd,
fetchViaplayBootstrapPssh, fetchViaplayBootstrapPssh,
cookieInventory, cookieInventory,
forgetStreamHref,
isDontGoPlaceholder, isDontGoPlaceholder,
isLiveDashMpd, isLiveDashMpd,
mpdLooksLikeDontGo, mpdLooksLikeDontGo,
stripKapWindow,
VIAPLAY_USER_AGENT, VIAPLAY_USER_AGENT,
} from "./client"; } from "./client";
import { inventoryFromNames, type CookieInventory } from "./cookies"; import { inventoryFromNames, type CookieInventory } from "./cookies";
@ -27,6 +29,7 @@ import {
recordViaplayPlay, recordViaplayPlay,
getLastViaplayPlay, getLastViaplayPlay,
getViaplayPlayLog, getViaplayPlayLog,
peekLastViaplayPlay,
type ViaplayPlayDiag, type ViaplayPlayDiag,
} from "./diag"; } from "./diag";
import { import {
@ -39,11 +42,11 @@ import { VIAPLAY_CATCHUP_MS } from "./events";
export type LastVodPlayDiag = ViaplayPlayDiag; export type LastVodPlayDiag = ViaplayPlayDiag;
export function getLastVodPlay() { export async function getLastVodPlay() {
return getLastViaplayPlay(); return getLastViaplayPlay();
} }
export { getViaplayPlayLog }; export { getViaplayPlayLog, peekLastViaplayPlay };
function emptyCookies(): CookieInventory { function emptyCookies(): CookieInventory {
return inventoryFromNames([], null); return inventoryFromNames([], null);
@ -198,23 +201,51 @@ function accountOrder(
return ordered; return ordered;
} }
function isReusableLiveMpd(url: string | null | undefined): url is string {
const u = url?.trim();
return !!u && isLiveDashMpd(u) && !isDontGoPlaceholder(u);
}
function channelMpdFromEvents( function channelMpdFromEvents(
events: ViaplayEventSnapshot[], events: ViaplayEventSnapshot[],
event: ViaplayEventSnapshot event: ViaplayEventSnapshot
): string | null { ): string | null {
if (event.mpdUrl?.trim()) return event.mpdUrl.trim(); if (isReusableLiveMpd(event.mpdUrl)) return event.mpdUrl.trim();
const channel = event.channelName?.trim().toLowerCase(); const channel = event.channelName?.trim().toLowerCase();
const station = event.stationId?.trim(); const station = event.stationId?.trim();
if (!channel && !station) return null; if (!channel && !station) return null;
for (const other of events) { for (const other of events) {
const url = other.mpdUrl?.trim(); const url = other.mpdUrl?.trim();
if (!url) continue; if (!isReusableLiveMpd(url)) continue;
if (station && other.stationId?.trim() === station) return url; if (station && other.stationId?.trim() === station) return stripKapWindow(url);
if (channel && other.channelName?.trim().toLowerCase() === channel) return url; if (channel && other.channelName?.trim().toLowerCase() === channel) {
return stripKapWindow(url);
}
} }
return null; return null;
} }
async function liveCachedMpdPlayable(url: string, timeoutMs: number): Promise<boolean> {
if (!isReusableLiveMpd(url)) return false;
try {
const mpd = await fetchMpdXml(url, Math.min(timeoutMs, 8_000));
if (mpd.status !== 200 || !/<MPD[\s>]/i.test(mpd.xml)) return false;
if (isDontGoPlaceholder(mpd.url) || mpdLooksLikeDontGo(mpd.xml)) return false;
return true;
} catch {
return false;
}
}
async function dropEventMpdCache(mediaGuid: string, reason: string) {
await patchViaplayEventCache(mediaGuid, {
mpdUrl: null,
mpdCachedAt: null,
lastPlayError: reason,
lastPlayAt: new Date().toISOString(),
});
}
export async function ensureViaplayEventReady( export async function ensureViaplayEventReady(
sessionSecret: string, sessionSecret: string,
event: ViaplayEventSnapshot, event: ViaplayEventSnapshot,
@ -239,34 +270,48 @@ export async function ensureViaplayEventReady(
const cacheFresh = const cacheFresh =
!opts?.forceRefresh && !opts?.forceRefresh &&
!ended && !ended &&
!!event.mpdUrl && isReusableLiveMpd(event.mpdUrl) &&
isMpdCacheFresh(event.mpdCachedAt, tweaks.mpdCacheTtlMinutes); isMpdCacheFresh(event.mpdCachedAt, tweaks.mpdCacheTtlMinutes);
if ( if (cacheFresh && cachedKeys.length && event.mpdUrl) {
cacheFresh && if (await liveCachedMpdPlayable(event.mpdUrl, tweaks.httpTimeoutMs)) {
cachedKeys.length && recordLivePlay({
event.mpdUrl && guid: mediaGuid,
isLiveDashMpd(event.mpdUrl) && ok: true,
!isDontGoPlaceholder(event.mpdUrl) title: event.title,
) { mediaPath: mediaPathOf(event.mpdUrl),
return { hasLicense: true,
streamUrl: event.mpdUrl, fromCache: true,
format: "dash", cookies: emptyCookies(),
keys: cachedKeys, error: null,
fromCache: true, });
return {
streamUrl: event.mpdUrl,
format: "dash",
keys: cachedKeys,
fromCache: true,
mediaGuid,
live: !ended,
};
}
await dropEventMpdCache(
mediaGuid, mediaGuid,
live: !ended, "Gecachte MPD ongeldig (Don't go of verlopen) — verse play-live"
}; );
recordLivePlay({
guid: mediaGuid,
ok: false,
title: event.title,
mediaPath: mediaPathOf(event.mpdUrl),
fromCache: true,
dontGo: true,
cookies: emptyCookies(),
error: "Gecachte MPD ongeldig (Don't go of verlopen) — verse play-live",
});
} }
// Terugkijken: KAP alleen op echte live-dash zender-MPD (niet op VOD/Don't go). // Terugkijken: KAP alleen op echte live-dash zender-MPD (niet op VOD/Don't go).
if ( if (ended && cachedMpd && !opts?.forceRefresh && isReusableLiveMpd(cachedMpd)) {
ended &&
cachedMpd &&
!opts?.forceRefresh &&
isLiveDashMpd(cachedMpd) &&
!isDontGoPlaceholder(cachedMpd)
) {
try { try {
const kap = await resolveKapMpd( const kap = await resolveKapMpd(
cachedMpd, cachedMpd,
@ -274,6 +319,9 @@ export async function ensureViaplayEventReady(
event.endMs, event.endMs,
Math.min(tweaks.httpTimeoutMs, 12_000) Math.min(tweaks.httpTimeoutMs, 12_000)
); );
if (isDontGoPlaceholder(kap.mpdUrl) || !(await liveCachedMpdPlayable(kap.mpdUrl, tweaks.httpTimeoutMs))) {
throw new Error("KAP-MPD is Don't go of ongeldig");
}
if (!cachedKeys.length) { if (!cachedKeys.length) {
cachedKeys = await keysForChannelPath(sessionSecret, kap.mpdUrl); cachedKeys = await keysForChannelPath(sessionSecret, kap.mpdUrl);
} }
@ -286,6 +334,16 @@ export async function ensureViaplayEventReady(
lastPlayError: null, lastPlayError: null,
lastPlayAt: new Date().toISOString(), lastPlayAt: new Date().toISOString(),
}); });
recordLivePlay({
guid: mediaGuid,
ok: true,
title: event.title,
mediaPath: mediaPathOf(kap.mpdUrl),
hasLicense: true,
fromCache: true,
cookies: emptyCookies(),
error: null,
});
return { return {
streamUrl: kap.mpdUrl, streamUrl: kap.mpdUrl,
format: "dash", format: "dash",
@ -304,10 +362,20 @@ export async function ensureViaplayEventReady(
if ( if (
cacheFresh && cacheFresh &&
event.mpdUrl && event.mpdUrl &&
isLiveDashMpd(event.mpdUrl) && isReusableLiveMpd(event.mpdUrl) &&
!isDontGoPlaceholder(event.mpdUrl) && !(opts?.fetchKeys ?? tweaks.fetchKeysIfMissing) &&
!(opts?.fetchKeys ?? tweaks.fetchKeysIfMissing) (await liveCachedMpdPlayable(event.mpdUrl, tweaks.httpTimeoutMs))
) { ) {
recordLivePlay({
guid: mediaGuid,
ok: true,
title: event.title,
mediaPath: mediaPathOf(event.mpdUrl),
hasLicense: cachedKeys.length > 0,
fromCache: true,
cookies: emptyCookies(),
error: null,
});
return { return {
streamUrl: event.mpdUrl, streamUrl: event.mpdUrl,
format: "dash", format: "dash",
@ -365,6 +433,16 @@ export async function ensureViaplayEventReady(
pssh: null as string | null, pssh: null as string | null,
}; };
if (isDontGoPlaceholder(play.mpdUrl) || isDontGoPlaceholder(kap.mpdUrl)) {
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de wedstrijdstream");
}
if (!ended && !isLiveDashMpd(play.mpdUrl)) {
throw new Error("Viaplay gaf geen live-dash stream (Don't go/VOD-fallback)");
}
if (kap.xml && mpdLooksLikeDontGo(kap.xml)) {
throw new Error("Viaplay MPD is Don't go-placeholder (duur/media mismatch)");
}
let keys = cachedKeys; let keys = cachedKeys;
if (!keys.length) { if (!keys.length) {
keys = await keysForChannelPath(sessionSecret, play.mpdUrl); keys = await keysForChannelPath(sessionSecret, play.mpdUrl);
@ -410,12 +488,6 @@ export async function ensureViaplayEventReady(
if (play.licenseUrl && !keys.length) { if (play.licenseUrl && !keys.length) {
throw new Error("Viaplay-licentie aanwezig maar geen keys"); throw new Error("Viaplay-licentie aanwezig maar geen keys");
} }
if (isDontGoPlaceholder(play.mpdUrl) || isDontGoPlaceholder(kap.mpdUrl)) {
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de wedstrijdstream");
}
if (!ended && !isLiveDashMpd(play.mpdUrl)) {
throw new Error("Viaplay gaf geen live-dash stream (Don't go/VOD-fallback)");
}
await patchViaplayEventCache(mediaGuid, { await patchViaplayEventCache(mediaGuid, {
mpdUrl: kap.mpdUrl, mpdUrl: kap.mpdUrl,
@ -437,9 +509,10 @@ export async function ensureViaplayEventReady(
accountLabel: account.label, accountLabel: account.label,
mediaPath: mediaPathOf(kap.mpdUrl), mediaPath: mediaPathOf(kap.mpdUrl),
hasLicense: !!play.licenseUrl, hasLicense: !!play.licenseUrl,
dontGo: isDontGoPlaceholder(kap.mpdUrl), dontGo: false,
cookies: cookieInventory(creds), cookies: cookieInventory(creds),
error: null, error: null,
fromCache: false,
}); });
return { return {
@ -587,7 +660,6 @@ async function resolveViaplayGuidPlayInner(
sectionPath: "/sport", sectionPath: "/sport",
}); });
let keys = cachedKeys;
const wantKeys = tweaks.fetchKeysIfMissing; const wantKeys = tweaks.fetchKeysIfMissing;
let pssh: string | null = null; let pssh: string | null = null;
let mpdUrl = play.mpdUrl; let mpdUrl = play.mpdUrl;
@ -598,6 +670,7 @@ async function resolveViaplayGuidPlayInner(
isDontGoPlaceholder(mpd.url) || isDontGoPlaceholder(mpd.url) ||
mpdLooksLikeDontGo(mpd.xml, play.durationMs) mpdLooksLikeDontGo(mpd.xml, play.durationMs)
) { ) {
forgetStreamHref(guid);
throw new Error( throw new Error(
"Viaplay MPD is Don't go-placeholder (duur/media mismatch)" "Viaplay MPD is Don't go-placeholder (duur/media mismatch)"
); );
@ -609,45 +682,54 @@ async function resolveViaplayGuidPlayInner(
if (err instanceof Error && /Don't go/i.test(err.message)) throw err; if (err instanceof Error && /Don't go/i.test(err.message)) throw err;
} }
// Clear VOD (HAR: geen license) — nooit stale ClearKey van eerdere plays plakken. if (isDontGoPlaceholder(mpdUrl)) {
forgetStreamHref(guid);
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de gevraagde stream");
}
// Nooit stale ClearKey van een eerdere (Don't go-)play op een verse MPD plakken.
let keys: Array<{ kid: string; key: string }> = [];
if (!play.licenseUrl) { if (!play.licenseUrl) {
keys = []; keys = [];
} else if (wantKeys && !keys.length) { } else if (wantKeys) {
if (!pssh) { keys = cachedKeys;
pssh = await fetchViaplayBootstrapPssh(tweaks.httpTimeoutMs); if (!keys.length) {
} if (!pssh) {
if (!pssh) { pssh = await fetchViaplayBootstrapPssh(tweaks.httpTimeoutMs);
throw new Error("Geen PSSH (MPD + bootstrap) — VOD-keys niet op te halen"); }
} if (!pssh) {
const extracted = await extractViaplayKeys({ throw new Error("Geen PSSH (MPD + bootstrap) — VOD-keys niet op te halen");
wvdPath: settings.wvdPath, }
pssh, const extracted = await extractViaplayKeys({
licUrl: play.licenseUrl, wvdPath: settings.wvdPath,
licAuth: play.licenseAuth, pssh,
userAgent: VIAPLAY_USER_AGENT, licUrl: play.licenseUrl,
timeoutMs: tweaks.cdmTimeoutMs, licAuth: play.licenseAuth,
}); userAgent: VIAPLAY_USER_AGENT,
const parsed = extracted timeoutMs: tweaks.cdmTimeoutMs,
.map(parseKey) });
.filter((k): k is { kid: string; key: string } => !!k); const parsed = extracted
if (parsed.length) { .map(parseKey)
keys = parsed; .filter((k): k is { kid: string; key: string } => !!k);
cachedKeys = parsed; if (parsed.length) {
const { upsertDrmKey } = await import("../drm/keys"); keys = parsed;
for (const k of parsed) { cachedKeys = parsed;
try { const { upsertDrmKey } = await import("../drm/keys");
await upsertDrmKey(prisma, sessionSecret, { for (const k of parsed) {
kid: k.kid, try {
key: k.key, await upsertDrmKey(prisma, sessionSecret, {
mpdUrl, kid: k.kid,
eventName: play.title || guid, key: k.key,
channelName: play.channelName, mpdUrl,
source: "viaplay", eventName: play.title || guid,
externalId: guid, channelName: play.channelName,
mediaGuid: guid, source: "viaplay",
}); externalId: guid,
} catch { mediaGuid: guid,
/* ignore */ });
} catch {
/* ignore */
}
} }
} }
} }
@ -655,9 +737,6 @@ async function resolveViaplayGuidPlayInner(
throw new Error("Viaplay-licentie aanwezig maar geen VOD-keys"); throw new Error("Viaplay-licentie aanwezig maar geen VOD-keys");
} }
} }
if (isDontGoPlaceholder(mpdUrl)) {
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de gevraagde stream");
}
recordVodPlay({ recordVodPlay({
guid, guid,
@ -667,7 +746,7 @@ async function resolveViaplayGuidPlayInner(
accountLabel: label, accountLabel: label,
mediaPath: mediaPathOf(mpdUrl), mediaPath: mediaPathOf(mpdUrl),
hasLicense: !!play.licenseUrl, hasLicense: !!play.licenseUrl,
dontGo: isDontGoPlaceholder(mpdUrl), dontGo: false,
productDurationMs: play.durationMs, productDurationMs: play.durationMs,
cookies: cookieInventory(creds), cookies: cookieInventory(creds),
error: null, error: null,
@ -688,6 +767,17 @@ async function resolveViaplayGuidPlayInner(
} catch (err) { } catch (err) {
lastError = err instanceof Error ? err : new Error(String(err)); lastError = err instanceof Error ? err : new Error(String(err));
attemptErrors.push(`${label}: ${lastError.message}`); attemptErrors.push(`${label}: ${lastError.message}`);
if (isDontGoError(lastError.message)) forgetStreamHref(guid);
recordVodPlay({
guid,
ok: false,
accountId: account.id,
accountLabel: label,
cookies: cookieInventory(account),
error: lastError.message,
triedAccounts: triedLabels.length,
triedLabels,
});
} }
} }

View file

@ -108,7 +108,7 @@ export const VIAPLAY_TWEAK_META: Array<{
{ {
key: "mpdCacheTtlMinutes", key: "mpdCacheTtlMinutes",
label: "Manifest-cache TTL", label: "Manifest-cache TTL",
hint: "Hergebruik gecached manifest tot TTL verloopt.", hint: "Hergebruik live-manifest tot TTL. Elke play her-checkt de MPD (Don't go wordt nooit uit cache gespeeld).",
kind: "number", kind: "number",
unit: "min", unit: "min",
}, },