Play ended Viaplay events as vod-dash byguid, not an expired live-TV MPD.
This commit is contained in:
parent
f35200e0c3
commit
aa1d733582
3 changed files with 230 additions and 150 deletions
|
|
@ -655,9 +655,15 @@ function productToRailItem(node: Record<string, unknown>): ViaplaySportRailItem
|
||||||
else if (type === "series" || type === "sportseries" || type === "episode") kind = "series";
|
else if (type === "series" || type === "sportseries" || type === "episode") kind = "series";
|
||||||
else if (type === "movie" || type === "film") kind = "series"; // docs/specials → VOD byguid
|
else if (type === "movie" || type === "film") kind = "series"; // docs/specials → VOD byguid
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const stillLive =
|
||||||
|
startMs != null && (endMs == null || endMs > now);
|
||||||
let playMode: ViaplaySportRailItem["playMode"] = null;
|
let playMode: ViaplaySportRailItem["playMode"] = null;
|
||||||
if (href?.includes("play-live.viaplay.com") || (kind === "sport" && startMs != null)) {
|
if (href?.includes("play-live.viaplay.com") || (kind === "sport" && stillLive)) {
|
||||||
playMode = "live";
|
playMode = "live";
|
||||||
|
} else if (kind === "sport" && endMs != null && endMs <= now) {
|
||||||
|
// Afgelopen hoofduitzending: web speelt byguid/vod-dash, niet live-TV.
|
||||||
|
playMode = "vod";
|
||||||
} else if (
|
} else if (
|
||||||
href?.includes("play.viaplay.com/api/stream/byguid") ||
|
href?.includes("play.viaplay.com/api/stream/byguid") ||
|
||||||
kind === "clip" ||
|
kind === "clip" ||
|
||||||
|
|
@ -886,14 +892,25 @@ export function isLiveDashMpd(mpdUrl: string | null | undefined): boolean {
|
||||||
return !!mpdUrl && /live-dash/i.test(mpdUrl);
|
return !!mpdUrl && /live-dash/i.test(mpdUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertPlayNotSoftFail(played: ViaplayPlayResult): void {
|
/** HAR/web-terugkijken: vod-dash + vp-sports-clip-*.ism — niet live-TV. */
|
||||||
|
export function isVodDashMpd(mpdUrl: string | null | undefined): boolean {
|
||||||
|
if (!mpdUrl) return false;
|
||||||
|
if (/vod-dash/i.test(mpdUrl)) return true;
|
||||||
|
return /\/vp-sports-clip-/i.test(mpdUrl) && /\.ism\//i.test(mpdUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPlayNotSoftFail(
|
||||||
|
played: ViaplayPlayResult,
|
||||||
|
opts?: { allowVod?: boolean }
|
||||||
|
): void {
|
||||||
if (isPromoVodTitle(played.title)) {
|
if (isPromoVodTitle(played.title)) {
|
||||||
throw new Error(`Viaplay gaf promo-stream (“${played.title}”)`);
|
throw new Error(`Viaplay gaf promo-stream (“${played.title}”)`);
|
||||||
}
|
}
|
||||||
if (isDontGoPlaceholder(played.mpdUrl)) {
|
if (isDontGoPlaceholder(played.mpdUrl)) {
|
||||||
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de wedstrijdstream");
|
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de wedstrijdstream");
|
||||||
}
|
}
|
||||||
if (!isLiveDashMpd(played.mpdUrl)) {
|
const vodOk = !!opts?.allowVod && isVodDashMpd(played.mpdUrl);
|
||||||
|
if (!vodOk && !isLiveDashMpd(played.mpdUrl)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Viaplay gaf geen live-dash stream (Don't go/VOD-fallback — sessie of rechten)"
|
"Viaplay gaf geen live-dash stream (Don't go/VOD-fallback — sessie of rechten)"
|
||||||
);
|
);
|
||||||
|
|
@ -930,7 +947,6 @@ export async function playByMediaGuid(
|
||||||
}
|
}
|
||||||
): Promise<ViaplayPlayResult> {
|
): Promise<ViaplayPlayResult> {
|
||||||
void opts.pageUrl;
|
void opts.pageUrl;
|
||||||
void opts.allowVod;
|
|
||||||
const mediaGuid = toMediaGuid(mediaGuidRaw);
|
const mediaGuid = toMediaGuid(mediaGuidRaw);
|
||||||
const playUrl = buildPlayLiveUrl(creds, mediaGuid, { deviceKey: opts.deviceKey });
|
const playUrl = buildPlayLiveUrl(creds, mediaGuid, { deviceKey: opts.deviceKey });
|
||||||
const res = await fetchJson(playUrl, {
|
const res = await fetchJson(playUrl, {
|
||||||
|
|
@ -945,7 +961,7 @@ export async function playByMediaGuid(
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
const played = parsePlayStreamBody(body, mediaGuid);
|
const played = parsePlayStreamBody(body, mediaGuid);
|
||||||
assertPlayNotSoftFail(played);
|
assertPlayNotSoftFail(played, { allowVod: !!opts.allowVod });
|
||||||
return played;
|
return played;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,10 @@ function isWeekendLocal(ms: number): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function railItemToHome(item: ViaplaySportRailItem, nowMs: number): ViaplayHomeItem {
|
function railItemToHome(item: ViaplaySportRailItem, nowMs: number): ViaplayHomeItem {
|
||||||
const isEvent = item.kind === "sport" && item.playMode === "live";
|
const isEvent =
|
||||||
|
item.kind === "sport" &&
|
||||||
|
item.playMode === "live" &&
|
||||||
|
(item.endMs == null || item.endMs > nowMs);
|
||||||
const openAs: ViaplayHomeItem["openAs"] = isEvent ? "event" : "content";
|
const openAs: ViaplayHomeItem["openAs"] = isEvent ? "event" : "content";
|
||||||
const id =
|
const id =
|
||||||
openAs === "event" ? toEventId(toMediaGuid(item.guid)) : `viaplay-content:${item.guid}`;
|
openAs === "event" ? toEventId(toMediaGuid(item.guid)) : `viaplay-content:${item.guid}`;
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ import {
|
||||||
forgetStreamHref,
|
forgetStreamHref,
|
||||||
isDontGoPlaceholder,
|
isDontGoPlaceholder,
|
||||||
isLiveDashMpd,
|
isLiveDashMpd,
|
||||||
|
isVodDashMpd,
|
||||||
mpdLooksLikeDontGo,
|
mpdLooksLikeDontGo,
|
||||||
mpdHasContentProtection,
|
mpdHasContentProtection,
|
||||||
stripKapWindow,
|
stripKapWindow,
|
||||||
|
fetchSportRails,
|
||||||
VIAPLAY_USER_AGENT,
|
VIAPLAY_USER_AGENT,
|
||||||
} from "./client";
|
} from "./client";
|
||||||
import { inventoryFromNames, type CookieInventory } from "./cookies";
|
import { inventoryFromNames, type CookieInventory } from "./cookies";
|
||||||
|
|
@ -109,10 +111,6 @@ function parseKey(value: string): { kid: string; key: string } | null {
|
||||||
return { kid: kid!, key: key! };
|
return { kid: kid!, key: key! };
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasKapWindow(url: string | null | undefined): boolean {
|
|
||||||
return !!url && /\/start\/\d+\/stop\/\d+/.test(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
type StoredDrmPlay = {
|
type StoredDrmPlay = {
|
||||||
keys: Array<{ kid: string; key: string }>;
|
keys: Array<{ kid: string; key: string }>;
|
||||||
mpdUrl: string | null;
|
mpdUrl: string | null;
|
||||||
|
|
@ -273,6 +271,35 @@ function channelMpdFromEvents(
|
||||||
return preferLiveChannelUrl(candidates);
|
return preferLiveChannelUrl(candidates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catch-up: alleen vpxtra van dezelfde zender als play-live, nooit de
|
||||||
|
* MPD van dit event zelf (die kan gestolen zijn door een gedeelde KID).
|
||||||
|
*/
|
||||||
|
function xtraMpdForSameChannel(
|
||||||
|
events: ViaplayEventSnapshot[],
|
||||||
|
event: ViaplayEventSnapshot,
|
||||||
|
playChannel: string | null,
|
||||||
|
playStation: string | null
|
||||||
|
): string | null {
|
||||||
|
const channel = (playChannel || event.channelName || "").trim().toLowerCase();
|
||||||
|
const station = (playStation || event.stationId || "").trim();
|
||||||
|
if (!channel && !station) return null;
|
||||||
|
const selfGuid = toMediaGuid(event.mediaGuid || event.guid);
|
||||||
|
const candidates: string[] = [];
|
||||||
|
for (const other of events) {
|
||||||
|
if (toMediaGuid(other.mediaGuid || other.guid) === selfGuid) continue;
|
||||||
|
const url = other.mpdUrl?.trim();
|
||||||
|
if (!url) continue;
|
||||||
|
const clean = stripKapWindow(url);
|
||||||
|
if (!isXtraMpd(clean) || !isReusableLiveMpd(clean)) continue;
|
||||||
|
const st = other.stationId?.trim() || "";
|
||||||
|
const ch = (other.channelName || "").trim().toLowerCase();
|
||||||
|
if (station && st && st === station) candidates.push(clean);
|
||||||
|
else if (channel && ch && ch === channel) candidates.push(clean);
|
||||||
|
}
|
||||||
|
return preferLiveChannelUrl(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
async function liveCachedMpdPlayable(url: string, timeoutMs: number): Promise<boolean> {
|
async function liveCachedMpdPlayable(url: string, timeoutMs: number): Promise<boolean> {
|
||||||
if (!isReusableLiveMpd(url)) return false;
|
if (!isReusableLiveMpd(url)) return false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -431,6 +458,142 @@ async function dropEventMpdCache(mediaGuid: string, reason: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vodGuidCandidates(event: ViaplayEventSnapshot): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const push = (raw: string | null | undefined) => {
|
||||||
|
const g = String(raw || "").trim();
|
||||||
|
if (!g || seen.has(g)) return;
|
||||||
|
seen.add(g);
|
||||||
|
out.push(g);
|
||||||
|
};
|
||||||
|
push(event.guid);
|
||||||
|
push(event.mediaGuid);
|
||||||
|
for (const g of [...out]) {
|
||||||
|
if (/-NL$/i.test(g)) push(g.replace(/-NL$/i, ""));
|
||||||
|
else if (!/vp-sports-clip/i.test(g)) push(`${g}-NL`);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normCatchupTitle(s: string): string {
|
||||||
|
return s
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(
|
||||||
|
/\b(samenvatting|hoogtepunten|terugblik|terugkijken|highlights|race|kwalificatie)\b/g,
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
.replace(/[^a-z0-9]+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function catchupTitleScore(eventTitle: string, clipTitle: string): number {
|
||||||
|
const a = normCatchupTitle(eventTitle);
|
||||||
|
const b = normCatchupTitle(clipTitle);
|
||||||
|
if (!a || !b) return 0;
|
||||||
|
if (a === b) return 100;
|
||||||
|
if (a.includes(b) || b.includes(a)) return Math.min(a.length, b.length) >= 8 ? 80 : 0;
|
||||||
|
const aw = new Set(a.split(" ").filter((w) => w.length > 2));
|
||||||
|
const bw = b.split(" ").filter((w) => w.length > 2);
|
||||||
|
if (!aw.size || !bw.length) return 0;
|
||||||
|
const hit = bw.filter((w) => aw.has(w)).length;
|
||||||
|
if (hit >= 3 && hit / bw.length >= 0.5) return 50;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findCatchupClip(
|
||||||
|
deviceKey: string,
|
||||||
|
timeoutMs: number,
|
||||||
|
event: ViaplayEventSnapshot
|
||||||
|
): Promise<{ guid: string; pageUrl: string | null } | null> {
|
||||||
|
let rails: Awaited<ReturnType<typeof fetchSportRails>> = [];
|
||||||
|
try {
|
||||||
|
rails = await fetchSportRails(deviceKey, Math.min(timeoutMs, 12_000));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let best: { guid: string; pageUrl: string | null; score: number } | null = null;
|
||||||
|
for (const rail of rails) {
|
||||||
|
for (const item of rail.items) {
|
||||||
|
if (item.playMode !== "vod") continue;
|
||||||
|
let score = catchupTitleScore(event.title, item.title);
|
||||||
|
if (item.startMs != null && event.startMs) {
|
||||||
|
const delta = Math.abs(item.startMs - event.startMs);
|
||||||
|
if (delta < 3 * 60 * 60_000) score += 15;
|
||||||
|
}
|
||||||
|
if (score < 80) continue;
|
||||||
|
if (!best || score > best.score) {
|
||||||
|
best = { guid: item.guid, pageUrl: item.pageUrl, score };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terugkijken = VOD (HAR: play.viaplay.com/byguid → vod-dash / vp-sports-clip),
|
||||||
|
* niet KAP op een live-TV-MPD die al is afgelopen.
|
||||||
|
*/
|
||||||
|
async function playEndedEventAsVod(
|
||||||
|
sessionSecret: string,
|
||||||
|
event: ViaplayEventSnapshot
|
||||||
|
): Promise<ResolveViaplayPlayResult> {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const guids = vodGuidCandidates(event);
|
||||||
|
|
||||||
|
for (const guid of guids) {
|
||||||
|
try {
|
||||||
|
const played = await resolveViaplayGuidPlayInner(sessionSecret, guid, {
|
||||||
|
productUrl: event.pageUrl,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
isDontGoPlaceholder(played.streamUrl) ||
|
||||||
|
isLiveDashMpd(played.streamUrl) ||
|
||||||
|
!isVodDashMpd(played.streamUrl)
|
||||||
|
) {
|
||||||
|
errors.push(`${guid}: geen vod-dash (${mediaPathOf(played.streamUrl)})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return { ...played, live: false };
|
||||||
|
} catch (err) {
|
||||||
|
errors.push(
|
||||||
|
`${guid}: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await loadViaplaySettings(sessionSecret);
|
||||||
|
try {
|
||||||
|
const clip = await findCatchupClip(
|
||||||
|
settings.config.deviceKey,
|
||||||
|
settings.config.httpTimeoutMs,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
if (clip && !guids.includes(clip.guid)) {
|
||||||
|
const played = await resolveViaplayGuidPlayInner(sessionSecret, clip.guid, {
|
||||||
|
productUrl: clip.pageUrl,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
isDontGoPlaceholder(played.streamUrl) ||
|
||||||
|
isLiveDashMpd(played.streamUrl) ||
|
||||||
|
!isVodDashMpd(played.streamUrl)
|
||||||
|
) {
|
||||||
|
errors.push(`${clip.guid}: clip was Don't go of live-dash`);
|
||||||
|
} else {
|
||||||
|
return { ...played, live: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errors.push(`clip: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AppError(
|
||||||
|
"UPSTREAM",
|
||||||
|
`Geen VOD-replay (vod-dash/vp-sports-clip). ${errors.slice(-3).join(" · ")}`,
|
||||||
|
502
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureViaplayEventReady(
|
export async function ensureViaplayEventReady(
|
||||||
sessionSecret: string,
|
sessionSecret: string,
|
||||||
event: ViaplayEventSnapshot,
|
event: ViaplayEventSnapshot,
|
||||||
|
|
@ -445,6 +608,10 @@ export async function ensureViaplayEventReady(
|
||||||
[];
|
[];
|
||||||
|
|
||||||
const ended = Date.now() > event.endMs;
|
const ended = Date.now() > event.endMs;
|
||||||
|
if (ended) {
|
||||||
|
return await playEndedEventAsVod(sessionSecret, event);
|
||||||
|
}
|
||||||
|
|
||||||
const cachedMpd = channelMpdFromEvents(settings.config.events, event);
|
const cachedMpd = channelMpdFromEvents(settings.config.events, event);
|
||||||
|
|
||||||
const stored = await storedDrmPlay(sessionSecret, mediaGuid);
|
const stored = await storedDrmPlay(sessionSecret, mediaGuid);
|
||||||
|
|
@ -493,96 +660,17 @@ export async function ensureViaplayEventReady(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terugkijken: KAP op de zender-MPD van dit event (xtra als die bij dit guid hoort).
|
// Terugkijken: altijd verse play-live + KAP van DIT programma.
|
||||||
if (ended) {
|
// Niet de vpxtra van een ander live-event (anders: Switch Dames → mannenvoetbal).
|
||||||
const xtra = await storedXtraChannelPlay(
|
|
||||||
sessionSecret,
|
|
||||||
mediaGuid,
|
|
||||||
event.title,
|
|
||||||
settings.config.events.map((e) => e.mpdUrl)
|
|
||||||
);
|
|
||||||
const baseRaw =
|
|
||||||
(xtra.eventMatch && xtra.mpdUrl) ||
|
|
||||||
preferLiveChannelUrl([stored.mpdUrl, event.mpdUrl, cachedMpd]) ||
|
|
||||||
stored.mpdUrl ||
|
|
||||||
event.mpdUrl ||
|
|
||||||
cachedMpd;
|
|
||||||
const base = baseRaw ? stripKapWindow(baseRaw) : null;
|
|
||||||
const keys = xtra.eventMatch && xtra.keys.length ? xtra.keys : cachedKeys;
|
|
||||||
if (base && keys.length && isReusableLiveMpd(base)) {
|
|
||||||
try {
|
|
||||||
const kap = await resolveKapMpd(
|
|
||||||
base,
|
|
||||||
event.startMs,
|
|
||||||
event.endMs,
|
|
||||||
Math.min(tweaks.httpTimeoutMs, 12_000)
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
!isDontGoPlaceholder(kap.mpdUrl) &&
|
|
||||||
!(kap.xml && mpdLooksLikeDontGo(kap.xml))
|
|
||||||
) {
|
|
||||||
await patchViaplayEventCache(mediaGuid, {
|
|
||||||
mpdUrl: kap.mpdUrl,
|
|
||||||
mpdCachedAt: new Date().toISOString(),
|
|
||||||
hasPssh: kap.hasPssh,
|
|
||||||
hasKeys: true,
|
|
||||||
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",
|
|
||||||
keys,
|
|
||||||
fromCache: true,
|
|
||||||
mediaGuid,
|
|
||||||
live: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* bestaande catch-up-paden hieronder */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zelfde bron als de DRM-tabel in admin: opgeslagen MPD + KID/KEY.
|
// 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.
|
// Alleen opnieuw play-live als die set ontbreekt of Don't go is.
|
||||||
if (!opts?.forceRefresh && cachedKeys.length) {
|
if (!ended && !opts?.forceRefresh && cachedKeys.length) {
|
||||||
let url = ended
|
let url = preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
||||||
? preferLiveChannelUrl([stored.mpdUrl, event.mpdUrl, cachedMpd]) ||
|
|
||||||
stored.mpdUrl ||
|
|
||||||
event.mpdUrl ||
|
|
||||||
cachedMpd
|
|
||||||
: preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
|
||||||
if (url && isReusableLiveMpd(url)) {
|
if (url && isReusableLiveMpd(url)) {
|
||||||
if (!ended) url = stripKapWindow(url);
|
if (!ended) url = stripKapWindow(url);
|
||||||
if (ended && !hasKapWindow(url)) {
|
const skipLiveTve = isTveSportMpd(url);
|
||||||
try {
|
const probed = !skipLiveTve && (await liveCachedMpdPlayable(url, tweaks.httpTimeoutMs));
|
||||||
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 skipLiveTve = !ended && isTveSportMpd(url);
|
|
||||||
const trustStoredKap = ended && hasKapWindow(url);
|
|
||||||
const probed =
|
|
||||||
!skipLiveTve &&
|
|
||||||
(trustStoredKap || (await liveCachedMpdPlayable(url, tweaks.httpTimeoutMs)));
|
|
||||||
if (probed && url) {
|
if (probed && url) {
|
||||||
recordLivePlay({
|
recordLivePlay({
|
||||||
guid: mediaGuid,
|
guid: mediaGuid,
|
||||||
|
|
@ -657,54 +745,6 @@ export async function ensureViaplayEventReady(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terugkijken: KAP alleen op echte live-dash zender-MPD (niet op VOD/Don't go).
|
|
||||||
if (ended && cachedMpd && !opts?.forceRefresh && isReusableLiveMpd(cachedMpd)) {
|
|
||||||
try {
|
|
||||||
const kap = await resolveKapMpd(
|
|
||||||
cachedMpd,
|
|
||||||
event.startMs,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
if (cachedKeys.length) {
|
|
||||||
await patchViaplayEventCache(mediaGuid, {
|
|
||||||
mpdUrl: kap.mpdUrl,
|
|
||||||
mpdCachedAt: new Date().toISOString(),
|
|
||||||
hasPssh: kap.hasPssh,
|
|
||||||
hasKeys: true,
|
|
||||||
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",
|
|
||||||
keys: cachedKeys,
|
|
||||||
fromCache: true,
|
|
||||||
mediaGuid,
|
|
||||||
live: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* play-live hieronder */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache MPD zonder keys is ok als keys later komen — of clear feed
|
// Cache MPD zonder keys is ok als keys later komen — of clear feed
|
||||||
const cacheLiveUrl = preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
const cacheLiveUrl = preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
||||||
if (
|
if (
|
||||||
|
|
@ -770,10 +810,24 @@ export async function ensureViaplayEventReady(
|
||||||
const channelUrl = stripKapWindow(play.mpdUrl);
|
const channelUrl = stripKapWindow(play.mpdUrl);
|
||||||
const liveUrl =
|
const liveUrl =
|
||||||
preferLiveChannelUrl([channelUrl, event.mpdUrl, cachedMpd]) || channelUrl;
|
preferLiveChannelUrl([channelUrl, event.mpdUrl, cachedMpd]) || channelUrl;
|
||||||
|
// Terugkijken: KAP op de play-live van DIT guid. TVE → hooguit xtra
|
||||||
|
// van dezelfde zender, nooit vpxtra van een ander live-event.
|
||||||
|
let kapBase = channelUrl;
|
||||||
|
if (ended) {
|
||||||
|
if (isTveSportMpd(channelUrl) || !isXtraMpd(channelUrl)) {
|
||||||
|
const same = xtraMpdForSameChannel(
|
||||||
|
settings.config.events,
|
||||||
|
event,
|
||||||
|
play.channelName,
|
||||||
|
play.stationId
|
||||||
|
);
|
||||||
|
if (same) kapBase = same;
|
||||||
|
}
|
||||||
|
}
|
||||||
const kap =
|
const kap =
|
||||||
ended && isLiveDashMpd(channelUrl)
|
ended && isLiveDashMpd(kapBase)
|
||||||
? await resolveKapMpd(
|
? await resolveKapMpd(
|
||||||
channelUrl,
|
kapBase,
|
||||||
event.startMs,
|
event.startMs,
|
||||||
event.endMs,
|
event.endMs,
|
||||||
Math.min(tweaks.httpTimeoutMs, 12_000)
|
Math.min(tweaks.httpTimeoutMs, 12_000)
|
||||||
|
|
@ -798,9 +852,13 @@ export async function ensureViaplayEventReady(
|
||||||
throw new Error("Viaplay MPD is Don't go-placeholder (duur/media mismatch)");
|
throw new Error("Viaplay MPD is Don't go-placeholder (duur/media mismatch)");
|
||||||
}
|
}
|
||||||
|
|
||||||
let keys = cachedKeys;
|
let keys = ended
|
||||||
|
? await keysForChannelPath(sessionSecret, kap.mpdUrl)
|
||||||
|
: cachedKeys;
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
keys = await keysForChannelPath(sessionSecret, kap.mpdUrl);
|
keys = ended
|
||||||
|
? cachedKeys
|
||||||
|
: await keysForChannelPath(sessionSecret, kap.mpdUrl);
|
||||||
}
|
}
|
||||||
const wantKeys = opts?.fetchKeys ?? tweaks.fetchKeysIfMissing;
|
const wantKeys = opts?.fetchKeys ?? tweaks.fetchKeysIfMissing;
|
||||||
if (wantKeys && play.licenseUrl && !keys.length) {
|
if (wantKeys && play.licenseUrl && !keys.length) {
|
||||||
|
|
@ -826,12 +884,15 @@ export async function ensureViaplayEventReady(
|
||||||
await upsertDrmKey(prisma, sessionSecret, {
|
await upsertDrmKey(prisma, sessionSecret, {
|
||||||
kid: k.kid,
|
kid: k.kid,
|
||||||
key: k.key,
|
key: k.key,
|
||||||
mpdUrl: isTveSportMpd(kap.mpdUrl) ? null : kap.mpdUrl,
|
// Catch-up mag geen live-vpxtra van een gedeelde KID overschrijven.
|
||||||
eventName: event.title,
|
mpdUrl:
|
||||||
|
ended || isTveSportMpd(kap.mpdUrl) ? undefined : kap.mpdUrl,
|
||||||
|
eventName: ended ? undefined : event.title,
|
||||||
channelName: play.channelName,
|
channelName: play.channelName,
|
||||||
source: "viaplay",
|
source: "viaplay",
|
||||||
externalId: event.guid,
|
externalId: event.guid,
|
||||||
mediaGuid,
|
mediaGuid:
|
||||||
|
ended || isTveSportMpd(kap.mpdUrl) ? undefined : mediaGuid,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* DRM-db mag play niet breken */
|
/* DRM-db mag play niet breken */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue