diff --git a/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx b/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx
index c7a93ec..f895852 100644
--- a/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx
+++ b/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx
@@ -56,6 +56,7 @@ type PlayLogEntry = {
hasClientId: boolean;
hasPersistentLogin: boolean;
};
+ fromCache?: boolean;
};
type ViaplaySettings = {
@@ -138,6 +139,7 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
const loadPlayLog = useCallback(async () => {
const r = await fetch("/api/v1/admin/scripts/viaplay/play-log", {
credentials: "include",
+ cache: "no-store",
});
if (!r.ok) return;
const d = (await r.json()) as { log?: PlayLogEntry[] };
@@ -485,7 +487,7 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
Play-diagnose
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.
{playLog.length === 0 ? (
Nog geen plays sinds de laatste API-restart.
@@ -512,11 +514,12 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
{e.accountLabel ? ` · ${e.accountLabel}` : ""}
{e.mediaPath ? ` · ${e.mediaPath}` : ""}
{e.hasLicense ? " · widevine" : " · geen licentie"}
- {` · cookies ${e.cookies.count}`}
- {e.cookies.hasDatadome ? " · datadome" : " · géén datadome"}
- {e.cookies.hasBoid ? " · boid" : " · géén boid"}
- {e.cookies.sessionStoredDecoded ? " · session decoded" : ""}
- {e.cookies.sessionStoredEncoded ? " · session encoded" : ""}
+ {` · cookies ${e.cookies?.count ?? 0}`}
+ {e.cookies?.hasDatadome ? " · datadome" : " · géén datadome"}
+ {e.cookies?.hasBoid ? " · boid" : " · géén boid"}
+ {e.cookies?.sessionStoredDecoded ? " · session decoded" : ""}
+ {e.cookies?.sessionStoredEncoded ? " · session encoded" : ""}
+ {e.fromCache ? " · cache" : ""}
{(e.dontGoReason || e.error) && (
{e.dontGoReason || e.error}
diff --git a/apps/master-api/src/admin/routes.ts b/apps/master-api/src/admin/routes.ts
index f35e831..d32b5bd 100644
--- a/apps/master-api/src/admin/routes.ts
+++ b/apps/master-api/src/admin/routes.ts
@@ -1929,7 +1929,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
keyCount: played.keys.length,
promo,
resolver: "viaplay-byguid-v10",
- lastPlay: (await import("../viaplay/play")).getLastVodPlay(),
+ lastPlay: await (await import("../viaplay/play")).getLastVodPlay(),
};
} catch (err) {
return {
@@ -1937,17 +1937,22 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
guid,
error: err instanceof Error ? err.message : String(err),
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). */
- app.get("/api/v1/admin/scripts/viaplay/play-log", { preHandler: requireAdmin }, async () => {
- const { getViaplayPlayLog, getLastVodPlay } = await import("../viaplay/play");
- return { last: getLastVodPlay(), log: getViaplayPlayLog() };
- });
+ app.get(
+ "/api/v1/admin/scripts/viaplay/play-log",
+ { 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 () => {
const { syncViaplayCatalog } = await import("../viaplay/sync");
diff --git a/apps/master-api/src/agent/routes.ts b/apps/master-api/src/agent/routes.ts
index 38070c6..cb9463c 100644
--- a/apps/master-api/src/agent/routes.ts
+++ b/apps/master-api/src/agent/routes.ts
@@ -232,7 +232,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
keyCount: played.keys.length,
promo,
resolver: "viaplay-byguid-har-v7",
- lastVodPlay: getLastVodPlay(),
+ lastVodPlay: await getLastVodPlay(),
};
} catch (err) {
return {
@@ -240,7 +240,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
guid,
error: err instanceof Error ? err.message : String(err),
resolver: "viaplay-byguid-har-v7",
- lastVodPlay: getLastVodPlay(),
+ lastVodPlay: await getLastVodPlay(),
};
}
});
diff --git a/apps/master-api/src/app.ts b/apps/master-api/src/app.ts
index a1e460f..379bbf8 100644
--- a/apps/master-api/src/app.ts
+++ b/apps/master-api/src/app.ts
@@ -103,12 +103,12 @@ async function main() {
});
app.get("/health", async () => {
- const { getLastVodPlay } = await import("./viaplay/play");
+ const { peekLastViaplayPlay } = await import("./viaplay/play");
return {
status: "ok",
service: "master-api",
version: "1.0.7-ziggo-gracenote",
- lastVodPlay: getLastVodPlay(),
+ lastVodPlay: peekLastViaplayPlay(),
};
});
diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts
index afe34e2..17b7735 100644
--- a/apps/master-api/src/viaplay/client.ts
+++ b/apps/master-api/src/viaplay/client.ts
@@ -584,6 +584,11 @@ function cachedStreamHref(guid: string): string | null {
return hit.href;
}
+export function forgetStreamHref(guid: string): void {
+ const g = String(guid || "").trim();
+ if (g) streamHrefCache.delete(g);
+}
+
function streamHref(product: Record): string | null {
const links = asRecord(product._links);
const stream = asRecord(links?.["viaplay:stream"]);
@@ -1318,6 +1323,7 @@ export async function playByGuid(
isDontGoPlaceholder(mpd.url) ||
mpdLooksLikeDontGo(mpd.xml, expectedMs)
) {
+ forgetStreamHref(guid);
throw new Error(
"Viaplay MPD is Don't go-placeholder (duur/media mismatch)"
);
diff --git a/apps/master-api/src/viaplay/diag.ts b/apps/master-api/src/viaplay/diag.ts
index 38de71a..d809c46 100644
--- a/apps/master-api/src/viaplay/diag.ts
+++ b/apps/master-api/src/viaplay/diag.ts
@@ -1,3 +1,4 @@
+import { prisma } from "../database/client";
import type { CookieInventory } from "./cookies";
export type ViaplayPlayDiag = {
@@ -20,25 +21,85 @@ export type ViaplayPlayDiag = {
refreshOk: boolean | null;
error: string | null;
resolver: string;
+ fromCache?: boolean;
triedAccounts?: number;
triedLabels?: string[];
};
const MAX = 25;
+const PLAY_LOG_ID = "viaplay-play-log";
const log: ViaplayPlayDiag[] = [];
+let hydrating: Promise | 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 {
+ 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 {
+ 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 {
const full: ViaplayPlayDiag = { ...entry, at: new Date().toISOString() };
log.unshift(full);
if (log.length > MAX) log.length = MAX;
+ void hydrate()
+ .then(() => persistLog())
+ .catch(() => undefined);
return full;
}
-export function getViaplayPlayLog(): ViaplayPlayDiag[] {
+export function peekLastViaplayPlay(): ViaplayPlayDiag | null {
+ return log[0] ?? null;
+}
+
+export async function getViaplayPlayLog(): Promise {
+ await hydrate();
return log.slice();
}
-export function getLastViaplayPlay(): ViaplayPlayDiag | null {
+export async function getLastViaplayPlay(): Promise {
+ await hydrate();
return log[0] ?? null;
}
diff --git a/apps/master-api/src/viaplay/play.ts b/apps/master-api/src/viaplay/play.ts
index 9c3624a..bbdd224 100644
--- a/apps/master-api/src/viaplay/play.ts
+++ b/apps/master-api/src/viaplay/play.ts
@@ -15,9 +15,11 @@ import {
extractPsshFromMpd,
fetchViaplayBootstrapPssh,
cookieInventory,
+ forgetStreamHref,
isDontGoPlaceholder,
isLiveDashMpd,
mpdLooksLikeDontGo,
+ stripKapWindow,
VIAPLAY_USER_AGENT,
} from "./client";
import { inventoryFromNames, type CookieInventory } from "./cookies";
@@ -27,6 +29,7 @@ import {
recordViaplayPlay,
getLastViaplayPlay,
getViaplayPlayLog,
+ peekLastViaplayPlay,
type ViaplayPlayDiag,
} from "./diag";
import {
@@ -39,11 +42,11 @@ import { VIAPLAY_CATCHUP_MS } from "./events";
export type LastVodPlayDiag = ViaplayPlayDiag;
-export function getLastVodPlay() {
+export async function getLastVodPlay() {
return getLastViaplayPlay();
}
-export { getViaplayPlayLog };
+export { getViaplayPlayLog, peekLastViaplayPlay };
function emptyCookies(): CookieInventory {
return inventoryFromNames([], null);
@@ -198,23 +201,51 @@ function accountOrder(
return ordered;
}
+function isReusableLiveMpd(url: string | null | undefined): url is string {
+ const u = url?.trim();
+ return !!u && isLiveDashMpd(u) && !isDontGoPlaceholder(u);
+}
+
function channelMpdFromEvents(
events: ViaplayEventSnapshot[],
event: ViaplayEventSnapshot
): 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 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;
+ if (!isReusableLiveMpd(url)) continue;
+ if (station && other.stationId?.trim() === station) return stripKapWindow(url);
+ if (channel && other.channelName?.trim().toLowerCase() === channel) {
+ return stripKapWindow(url);
+ }
}
return null;
}
+async function liveCachedMpdPlayable(url: string, timeoutMs: number): Promise {
+ if (!isReusableLiveMpd(url)) return false;
+ try {
+ const mpd = await fetchMpdXml(url, Math.min(timeoutMs, 8_000));
+ if (mpd.status !== 200 || !/]/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(
sessionSecret: string,
event: ViaplayEventSnapshot,
@@ -239,34 +270,48 @@ export async function ensureViaplayEventReady(
const cacheFresh =
!opts?.forceRefresh &&
!ended &&
- !!event.mpdUrl &&
+ isReusableLiveMpd(event.mpdUrl) &&
isMpdCacheFresh(event.mpdCachedAt, tweaks.mpdCacheTtlMinutes);
- if (
- cacheFresh &&
- cachedKeys.length &&
- event.mpdUrl &&
- isLiveDashMpd(event.mpdUrl) &&
- !isDontGoPlaceholder(event.mpdUrl)
- ) {
- return {
- streamUrl: event.mpdUrl,
- format: "dash",
- keys: cachedKeys,
- fromCache: true,
+ if (cacheFresh && cachedKeys.length && event.mpdUrl) {
+ if (await liveCachedMpdPlayable(event.mpdUrl, tweaks.httpTimeoutMs)) {
+ recordLivePlay({
+ guid: mediaGuid,
+ ok: true,
+ title: event.title,
+ mediaPath: mediaPathOf(event.mpdUrl),
+ hasLicense: true,
+ fromCache: true,
+ cookies: emptyCookies(),
+ error: null,
+ });
+ return {
+ streamUrl: event.mpdUrl,
+ format: "dash",
+ keys: cachedKeys,
+ fromCache: true,
+ mediaGuid,
+ live: !ended,
+ };
+ }
+ await dropEventMpdCache(
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).
- if (
- ended &&
- cachedMpd &&
- !opts?.forceRefresh &&
- isLiveDashMpd(cachedMpd) &&
- !isDontGoPlaceholder(cachedMpd)
- ) {
+ if (ended && cachedMpd && !opts?.forceRefresh && isReusableLiveMpd(cachedMpd)) {
try {
const kap = await resolveKapMpd(
cachedMpd,
@@ -274,6 +319,9 @@ export async function ensureViaplayEventReady(
event.endMs,
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) {
cachedKeys = await keysForChannelPath(sessionSecret, kap.mpdUrl);
}
@@ -286,6 +334,16 @@ export async function ensureViaplayEventReady(
lastPlayError: null,
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 {
streamUrl: kap.mpdUrl,
format: "dash",
@@ -304,10 +362,20 @@ export async function ensureViaplayEventReady(
if (
cacheFresh &&
event.mpdUrl &&
- isLiveDashMpd(event.mpdUrl) &&
- !isDontGoPlaceholder(event.mpdUrl) &&
- !(opts?.fetchKeys ?? tweaks.fetchKeysIfMissing)
+ isReusableLiveMpd(event.mpdUrl) &&
+ !(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 {
streamUrl: event.mpdUrl,
format: "dash",
@@ -365,6 +433,16 @@ export async function ensureViaplayEventReady(
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;
if (!keys.length) {
keys = await keysForChannelPath(sessionSecret, play.mpdUrl);
@@ -410,12 +488,6 @@ export async function ensureViaplayEventReady(
if (play.licenseUrl && !keys.length) {
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, {
mpdUrl: kap.mpdUrl,
@@ -437,9 +509,10 @@ export async function ensureViaplayEventReady(
accountLabel: account.label,
mediaPath: mediaPathOf(kap.mpdUrl),
hasLicense: !!play.licenseUrl,
- dontGo: isDontGoPlaceholder(kap.mpdUrl),
+ dontGo: false,
cookies: cookieInventory(creds),
error: null,
+ fromCache: false,
});
return {
@@ -587,7 +660,6 @@ async function resolveViaplayGuidPlayInner(
sectionPath: "/sport",
});
- let keys = cachedKeys;
const wantKeys = tweaks.fetchKeysIfMissing;
let pssh: string | null = null;
let mpdUrl = play.mpdUrl;
@@ -598,6 +670,7 @@ async function resolveViaplayGuidPlayInner(
isDontGoPlaceholder(mpd.url) ||
mpdLooksLikeDontGo(mpd.xml, play.durationMs)
) {
+ forgetStreamHref(guid);
throw new Error(
"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;
}
- // 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) {
keys = [];
- } 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({
- wvdPath: settings.wvdPath,
- pssh,
- licUrl: play.licenseUrl,
- licAuth: play.licenseAuth,
- userAgent: VIAPLAY_USER_AGENT,
- timeoutMs: tweaks.cdmTimeoutMs,
- });
- const parsed = extracted
- .map(parseKey)
- .filter((k): k is { kid: string; key: string } => !!k);
- if (parsed.length) {
- keys = parsed;
- cachedKeys = parsed;
- const { upsertDrmKey } = await import("../drm/keys");
- for (const k of parsed) {
- try {
- await upsertDrmKey(prisma, sessionSecret, {
- kid: k.kid,
- key: k.key,
- mpdUrl,
- eventName: play.title || guid,
- channelName: play.channelName,
- source: "viaplay",
- externalId: guid,
- mediaGuid: guid,
- });
- } catch {
- /* ignore */
+ } else if (wantKeys) {
+ keys = cachedKeys;
+ if (!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({
+ wvdPath: settings.wvdPath,
+ pssh,
+ licUrl: play.licenseUrl,
+ licAuth: play.licenseAuth,
+ userAgent: VIAPLAY_USER_AGENT,
+ timeoutMs: tweaks.cdmTimeoutMs,
+ });
+ const parsed = extracted
+ .map(parseKey)
+ .filter((k): k is { kid: string; key: string } => !!k);
+ if (parsed.length) {
+ keys = parsed;
+ cachedKeys = parsed;
+ const { upsertDrmKey } = await import("../drm/keys");
+ for (const k of parsed) {
+ try {
+ await upsertDrmKey(prisma, sessionSecret, {
+ kid: k.kid,
+ key: k.key,
+ mpdUrl,
+ eventName: play.title || guid,
+ channelName: play.channelName,
+ source: "viaplay",
+ externalId: guid,
+ mediaGuid: guid,
+ });
+ } catch {
+ /* ignore */
+ }
}
}
}
@@ -655,9 +737,6 @@ async function resolveViaplayGuidPlayInner(
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({
guid,
@@ -667,7 +746,7 @@ async function resolveViaplayGuidPlayInner(
accountLabel: label,
mediaPath: mediaPathOf(mpdUrl),
hasLicense: !!play.licenseUrl,
- dontGo: isDontGoPlaceholder(mpdUrl),
+ dontGo: false,
productDurationMs: play.durationMs,
cookies: cookieInventory(creds),
error: null,
@@ -688,6 +767,17 @@ async function resolveViaplayGuidPlayInner(
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
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,
+ });
}
}
diff --git a/apps/master-api/src/viaplay/tweaks.ts b/apps/master-api/src/viaplay/tweaks.ts
index 6bfd42a..462d6d1 100644
--- a/apps/master-api/src/viaplay/tweaks.ts
+++ b/apps/master-api/src/viaplay/tweaks.ts
@@ -108,7 +108,7 @@ export const VIAPLAY_TWEAK_META: Array<{
{
key: "mpdCacheTtlMinutes",
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",
unit: "min",
},