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 === "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;
|
||||
if (href?.includes("play-live.viaplay.com") || (kind === "sport" && startMs != null)) {
|
||||
if (href?.includes("play-live.viaplay.com") || (kind === "sport" && stillLive)) {
|
||||
playMode = "live";
|
||||
} else if (kind === "sport" && endMs != null && endMs <= now) {
|
||||
// Afgelopen hoofduitzending: web speelt byguid/vod-dash, niet live-TV.
|
||||
playMode = "vod";
|
||||
} else if (
|
||||
href?.includes("play.viaplay.com/api/stream/byguid") ||
|
||||
kind === "clip" ||
|
||||
|
|
@ -886,14 +892,25 @@ export function isLiveDashMpd(mpdUrl: string | null | undefined): boolean {
|
|||
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)) {
|
||||
throw new Error(`Viaplay gaf promo-stream (“${played.title}”)`);
|
||||
}
|
||||
if (isDontGoPlaceholder(played.mpdUrl)) {
|
||||
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(
|
||||
"Viaplay gaf geen live-dash stream (Don't go/VOD-fallback — sessie of rechten)"
|
||||
);
|
||||
|
|
@ -930,7 +947,6 @@ export async function playByMediaGuid(
|
|||
}
|
||||
): Promise<ViaplayPlayResult> {
|
||||
void opts.pageUrl;
|
||||
void opts.allowVod;
|
||||
const mediaGuid = toMediaGuid(mediaGuidRaw);
|
||||
const playUrl = buildPlayLiveUrl(creds, mediaGuid, { deviceKey: opts.deviceKey });
|
||||
const res = await fetchJson(playUrl, {
|
||||
|
|
@ -945,7 +961,7 @@ export async function playByMediaGuid(
|
|||
throw new Error(msg);
|
||||
}
|
||||
const played = parsePlayStreamBody(body, mediaGuid);
|
||||
assertPlayNotSoftFail(played);
|
||||
assertPlayNotSoftFail(played, { allowVod: !!opts.allowVod });
|
||||
return played;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,7 +184,10 @@ function isWeekendLocal(ms: number): boolean {
|
|||
}
|
||||
|
||||
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 id =
|
||||
openAs === "event" ? toEventId(toMediaGuid(item.guid)) : `viaplay-content:${item.guid}`;
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ import {
|
|||
forgetStreamHref,
|
||||
isDontGoPlaceholder,
|
||||
isLiveDashMpd,
|
||||
isVodDashMpd,
|
||||
mpdLooksLikeDontGo,
|
||||
mpdHasContentProtection,
|
||||
stripKapWindow,
|
||||
fetchSportRails,
|
||||
VIAPLAY_USER_AGENT,
|
||||
} from "./client";
|
||||
import { inventoryFromNames, type CookieInventory } from "./cookies";
|
||||
|
|
@ -109,10 +111,6 @@ function parseKey(value: string): { kid: string; key: string } | null {
|
|||
return { kid: kid!, key: key! };
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -273,6 +271,35 @@ function channelMpdFromEvents(
|
|||
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> {
|
||||
if (!isReusableLiveMpd(url)) return false;
|
||||
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(
|
||||
sessionSecret: string,
|
||||
event: ViaplayEventSnapshot,
|
||||
|
|
@ -445,6 +608,10 @@ export async function ensureViaplayEventReady(
|
|||
[];
|
||||
|
||||
const ended = Date.now() > event.endMs;
|
||||
if (ended) {
|
||||
return await playEndedEventAsVod(sessionSecret, event);
|
||||
}
|
||||
|
||||
const cachedMpd = channelMpdFromEvents(settings.config.events, event);
|
||||
|
||||
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).
|
||||
if (ended) {
|
||||
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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
// Terugkijken: altijd verse play-live + KAP van DIT programma.
|
||||
// Niet de vpxtra van een ander live-event (anders: Switch Dames → mannenvoetbal).
|
||||
|
||||
// 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 = ended
|
||||
? preferLiveChannelUrl([stored.mpdUrl, event.mpdUrl, cachedMpd]) ||
|
||||
stored.mpdUrl ||
|
||||
event.mpdUrl ||
|
||||
cachedMpd
|
||||
: preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
||||
if (!ended && !opts?.forceRefresh && cachedKeys.length) {
|
||||
let url = preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
||||
if (url && isReusableLiveMpd(url)) {
|
||||
if (!ended) url = stripKapWindow(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 skipLiveTve = !ended && isTveSportMpd(url);
|
||||
const trustStoredKap = ended && hasKapWindow(url);
|
||||
const probed =
|
||||
!skipLiveTve &&
|
||||
(trustStoredKap || (await liveCachedMpdPlayable(url, tweaks.httpTimeoutMs)));
|
||||
const skipLiveTve = isTveSportMpd(url);
|
||||
const probed = !skipLiveTve && (await liveCachedMpdPlayable(url, tweaks.httpTimeoutMs));
|
||||
if (probed && url) {
|
||||
recordLivePlay({
|
||||
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
|
||||
const cacheLiveUrl = preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
||||
if (
|
||||
|
|
@ -770,10 +810,24 @@ export async function ensureViaplayEventReady(
|
|||
const channelUrl = stripKapWindow(play.mpdUrl);
|
||||
const liveUrl =
|
||||
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 =
|
||||
ended && isLiveDashMpd(channelUrl)
|
||||
ended && isLiveDashMpd(kapBase)
|
||||
? await resolveKapMpd(
|
||||
channelUrl,
|
||||
kapBase,
|
||||
event.startMs,
|
||||
event.endMs,
|
||||
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)");
|
||||
}
|
||||
|
||||
let keys = cachedKeys;
|
||||
let keys = ended
|
||||
? await keysForChannelPath(sessionSecret, kap.mpdUrl)
|
||||
: cachedKeys;
|
||||
if (!keys.length) {
|
||||
keys = await keysForChannelPath(sessionSecret, kap.mpdUrl);
|
||||
keys = ended
|
||||
? cachedKeys
|
||||
: await keysForChannelPath(sessionSecret, kap.mpdUrl);
|
||||
}
|
||||
const wantKeys = opts?.fetchKeys ?? tweaks.fetchKeysIfMissing;
|
||||
if (wantKeys && play.licenseUrl && !keys.length) {
|
||||
|
|
@ -826,12 +884,15 @@ export async function ensureViaplayEventReady(
|
|||
await upsertDrmKey(prisma, sessionSecret, {
|
||||
kid: k.kid,
|
||||
key: k.key,
|
||||
mpdUrl: isTveSportMpd(kap.mpdUrl) ? null : kap.mpdUrl,
|
||||
eventName: event.title,
|
||||
// Catch-up mag geen live-vpxtra van een gedeelde KID overschrijven.
|
||||
mpdUrl:
|
||||
ended || isTveSportMpd(kap.mpdUrl) ? undefined : kap.mpdUrl,
|
||||
eventName: ended ? undefined : event.title,
|
||||
channelName: play.channelName,
|
||||
source: "viaplay",
|
||||
externalId: event.guid,
|
||||
mediaGuid,
|
||||
mediaGuid:
|
||||
ended || isTveSportMpd(kap.mpdUrl) ? undefined : mediaGuid,
|
||||
});
|
||||
} catch {
|
||||
/* DRM-db mag play niet breken */
|
||||
|
|
|
|||
Loading…
Reference in a new issue