Serve stored Viaplay DRM MPD+keys to clients before play-live, matching the admin key table.

This commit is contained in:
Jos Vooges | STH 2026-09-28 20:27:08 +02:00
parent 133f51284d
commit 75969d87ec

View file

@ -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<Array<{ kid: string; key: string }>> {
): Promise<StoredDrmPlay> {
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<string>();
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<Array<{ kid: string; key: string }>> {
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));