Stop serving cached Viaplay Don't go MPDs and log every play including cache hits.
This commit is contained in:
parent
8d54e2d8c6
commit
133f51284d
8 changed files with 264 additions and 99 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
"/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");
|
const { getViaplayPlayLog, getLastVodPlay } = await import("../viaplay/play");
|
||||||
return { last: getLastVodPlay(), log: getViaplayPlayLog() };
|
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");
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,16 +270,21 @@ 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),
|
||||||
|
hasLicense: true,
|
||||||
|
fromCache: true,
|
||||||
|
cookies: emptyCookies(),
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
streamUrl: event.mpdUrl,
|
streamUrl: event.mpdUrl,
|
||||||
format: "dash",
|
format: "dash",
|
||||||
|
|
@ -258,15 +294,24 @@ export async function ensureViaplayEventReady(
|
||||||
live: !ended,
|
live: !ended,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
await dropEventMpdCache(
|
||||||
|
mediaGuid,
|
||||||
|
"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,10 +682,18 @@ 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) {
|
||||||
|
keys = cachedKeys;
|
||||||
|
if (!keys.length) {
|
||||||
if (!pssh) {
|
if (!pssh) {
|
||||||
pssh = await fetchViaplayBootstrapPssh(tweaks.httpTimeoutMs);
|
pssh = await fetchViaplayBootstrapPssh(tweaks.httpTimeoutMs);
|
||||||
}
|
}
|
||||||
|
|
@ -651,13 +732,11 @@ async function resolveViaplayGuidPlayInner(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue