Stop serving Viaplay Don't go: encode cookies like the live script, require live-dash, and reject promo MPDs by duration.
This commit is contained in:
parent
379fbdab6d
commit
ebb986f6c1
10 changed files with 184 additions and 129 deletions
|
|
@ -1928,14 +1928,14 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
||||||
streamUrl: played.streamUrl,
|
streamUrl: played.streamUrl,
|
||||||
keyCount: played.keys.length,
|
keyCount: played.keys.length,
|
||||||
promo,
|
promo,
|
||||||
resolver: "viaplay-byguid-har-v7",
|
resolver: "viaplay-byguid-v9",
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
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-v9",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
|
boid?: string;
|
||||||
};
|
};
|
||||||
const { upsertViaplayAccountByEmail } = await import("../viaplay/settings");
|
const { upsertViaplayAccountByEmail } = await import("../viaplay/settings");
|
||||||
return upsertViaplayAccountByEmail(config.SESSION_SECRET, {
|
return upsertViaplayAccountByEmail(config.SESSION_SECRET, {
|
||||||
|
|
@ -172,6 +173,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
||||||
accessToken: String(body.accessToken ?? ""),
|
accessToken: String(body.accessToken ?? ""),
|
||||||
profileId: body.profileId,
|
profileId: body.profileId,
|
||||||
persistentLogin: body.persistentLogin,
|
persistentLogin: body.persistentLogin,
|
||||||
|
boid: body.boid,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export type ViaplayAccountStored = {
|
||||||
sessionEnc?: string;
|
sessionEnc?: string;
|
||||||
accessTokenEnc?: string;
|
accessTokenEnc?: string;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
|
/** Chrome __Secure-Viaplay-Boid — livescript stuurt hem mee. */
|
||||||
|
boid?: string;
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
/** Optioneel — voor latere tray/login. */
|
/** Optioneel — voor latere tray/login. */
|
||||||
passwordEnc?: string;
|
passwordEnc?: string;
|
||||||
|
|
@ -52,6 +54,7 @@ export type ViaplayAccountCreds = {
|
||||||
session: string | null;
|
session: string | null;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
profileId: string | null;
|
profileId: string | null;
|
||||||
|
boid: string | null;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
|
|
@ -106,6 +109,7 @@ export function decryptAccount(
|
||||||
? decryptSecret(account.accessTokenEnc, sessionSecret)
|
? decryptSecret(account.accessTokenEnc, sessionSecret)
|
||||||
: null,
|
: null,
|
||||||
profileId: account.profileId?.trim() || null,
|
profileId: account.profileId?.trim() || null,
|
||||||
|
boid: account.boid?.trim() || null,
|
||||||
deviceId: account.deviceId?.trim() || randomUUID(),
|
deviceId: account.deviceId?.trim() || randomUUID(),
|
||||||
password: account.passwordEnc
|
password: account.passwordEnc
|
||||||
? decryptSecret(account.passwordEnc, sessionSecret)
|
? decryptSecret(account.passwordEnc, sessionSecret)
|
||||||
|
|
@ -127,6 +131,7 @@ export function createAccountStored(
|
||||||
session?: string;
|
session?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
|
boid?: string;
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
|
|
@ -152,6 +157,7 @@ export function createAccountStored(
|
||||||
? encryptSecret(accessToken, sessionSecret)
|
? encryptSecret(accessToken, sessionSecret)
|
||||||
: undefined,
|
: undefined,
|
||||||
profileId: input.profileId?.trim() || undefined,
|
profileId: input.profileId?.trim() || undefined,
|
||||||
|
boid: input.boid?.trim() || undefined,
|
||||||
deviceId: input.deviceId?.trim() || randomUUID(),
|
deviceId: input.deviceId?.trim() || randomUUID(),
|
||||||
passwordEnc: password ? encryptSecret(password, sessionSecret) : undefined,
|
passwordEnc: password ? encryptSecret(password, sessionSecret) : undefined,
|
||||||
persistentLoginEnc: input.persistentLogin?.trim()
|
persistentLoginEnc: input.persistentLogin?.trim()
|
||||||
|
|
@ -178,6 +184,7 @@ export function parseAccountsJson(raw: unknown): ViaplayAccountStored[] {
|
||||||
sessionEnc: String(row.sessionEnc ?? "").trim() || undefined,
|
sessionEnc: String(row.sessionEnc ?? "").trim() || undefined,
|
||||||
accessTokenEnc: String(row.accessTokenEnc ?? "").trim() || undefined,
|
accessTokenEnc: String(row.accessTokenEnc ?? "").trim() || undefined,
|
||||||
profileId: String(row.profileId ?? "").trim() || undefined,
|
profileId: String(row.profileId ?? "").trim() || undefined,
|
||||||
|
boid: String(row.boid ?? "").trim() || undefined,
|
||||||
deviceId: String(row.deviceId ?? "").trim() || undefined,
|
deviceId: String(row.deviceId ?? "").trim() || undefined,
|
||||||
passwordEnc: String(row.passwordEnc ?? "").trim() || undefined,
|
passwordEnc: String(row.passwordEnc ?? "").trim() || undefined,
|
||||||
persistentLoginEnc: String(row.persistentLoginEnc ?? "").trim() || undefined,
|
persistentLoginEnc: String(row.persistentLoginEnc ?? "").trim() || undefined,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ import { randomUUID } from "node:crypto";
|
||||||
import type { ViaplayAccountCreds } from "./accounts";
|
import type { ViaplayAccountCreds } from "./accounts";
|
||||||
import { toMediaGuid } from "./accounts";
|
import { toMediaGuid } from "./accounts";
|
||||||
|
|
||||||
|
/** Zelfde UA als het werkende livescript (viaplay3.py). Edge/153 + sec-ch-ua gaf Don't go. */
|
||||||
export const VIAPLAY_USER_AGENT =
|
export const VIAPLAY_USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0";
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
export type ViaplayPlayResult = {
|
export type ViaplayPlayResult = {
|
||||||
mediaGuid: string;
|
mediaGuid: string;
|
||||||
|
|
@ -13,6 +14,8 @@ export type ViaplayPlayResult = {
|
||||||
channelName: string | null;
|
channelName: string | null;
|
||||||
stationId: string | null;
|
stationId: string | null;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
|
/** Product-duration uit play-JSON (niet de MPD — Don't go liegt hier). */
|
||||||
|
durationMs: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function asRecord(v: unknown): Record<string, unknown> | null {
|
function asRecord(v: unknown): Record<string, unknown> | null {
|
||||||
|
|
@ -21,20 +24,44 @@ function asRecord(v: unknown): Record<string, unknown> | null {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Netscape/browser-cookies blijven encoded; decoded JSON in Cookie-header = Don't go. */
|
/**
|
||||||
|
* Electron/Playwright geeft cookies decoded (`s:j:{…}`).
|
||||||
|
* In de Cookie-header moeten die percent-encoded — anders parst Viaplay
|
||||||
|
* de session niet en geeft 200 + Don't go.
|
||||||
|
* Al encoded (`s%3Aj%3A%7B`) laten we met rust.
|
||||||
|
*/
|
||||||
function cookieValue(raw: string): string {
|
function cookieValue(raw: string): string {
|
||||||
const v = raw.trim();
|
const v = raw.trim();
|
||||||
if (!v) return v;
|
if (!v) return v;
|
||||||
if (/%[0-9A-Fa-f]{2}/.test(v)) return v;
|
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) {
|
||||||
if (/[{}"\s]/.test(v)) return encodeURIComponent(v);
|
return cookieValue(v.slice(1, -1));
|
||||||
|
}
|
||||||
|
if (/[{}"\s,;\\]/.test(v)) return encodeURIComponent(v);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSetCookieJar(setCookies: string[]): Record<string, string> {
|
||||||
|
const jar: Record<string, string> = {};
|
||||||
|
for (const sc of setCookies) {
|
||||||
|
const m = sc.match(/^([^=]+)=([^;]*)/);
|
||||||
|
if (!m) continue;
|
||||||
|
let v = m[2] ?? "";
|
||||||
|
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) {
|
||||||
|
v = v.slice(1, -1);
|
||||||
|
}
|
||||||
|
jar[m[1]!] = v;
|
||||||
|
}
|
||||||
|
return jar;
|
||||||
|
}
|
||||||
|
|
||||||
function cookieHeader(creds: ViaplayAccountCreds): string {
|
function cookieHeader(creds: ViaplayAccountCreds): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (creds.session) parts.push(`session=${cookieValue(creds.session)}`);
|
if (creds.session) parts.push(`session=${cookieValue(creds.session)}`);
|
||||||
if (creds.accessToken) parts.push(`accessToken=${creds.accessToken.trim()}`);
|
if (creds.accessToken) parts.push(`accessToken=${creds.accessToken.trim()}`);
|
||||||
if (creds.profileId) parts.push(`viaplay_profileId=${creds.profileId.trim()}`);
|
if (creds.profileId) parts.push(`viaplay_profileId=${creds.profileId.trim()}`);
|
||||||
|
if (creds.boid) {
|
||||||
|
parts.push(`__Secure-Viaplay-Boid=${creds.boid.trim()}`);
|
||||||
|
}
|
||||||
if (creds.persistentLogin) {
|
if (creds.persistentLogin) {
|
||||||
parts.push(`persistentLogin=${cookieValue(creds.persistentLogin)}`);
|
parts.push(`persistentLogin=${cookieValue(creds.persistentLogin)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -62,18 +89,9 @@ export function authHeaders(creds: ViaplayAccountCreds): Record<string, string>
|
||||||
export function playHeaders(creds: ViaplayAccountCreds): Record<string, string> {
|
export function playHeaders(creds: ViaplayAccountCreds): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
Accept: "*/*",
|
Accept: "*/*",
|
||||||
"Accept-Language":
|
|
||||||
"nl,en;q=0.9,en-GB;q=0.8,en-US;q=0.7,es-ES;q=0.6,es;q=0.5,nl-NL;q=0.4",
|
|
||||||
Origin: "https://viaplay.com",
|
Origin: "https://viaplay.com",
|
||||||
Referer: "https://viaplay.com/",
|
Referer: "https://viaplay.com/",
|
||||||
"User-Agent": VIAPLAY_USER_AGENT,
|
"User-Agent": VIAPLAY_USER_AGENT,
|
||||||
"Sec-Fetch-Dest": "empty",
|
|
||||||
"Sec-Fetch-Mode": "cors",
|
|
||||||
"Sec-Fetch-Site": "same-site",
|
|
||||||
"sec-ch-ua":
|
|
||||||
'"Microsoft Edge";v="153", "Not_A Brand";v="8", "Chromium";v="153"',
|
|
||||||
"sec-ch-ua-mobile": "?0",
|
|
||||||
"sec-ch-ua-platform": '"Windows"',
|
|
||||||
Cookie: cookieHeader(creds),
|
Cookie: cookieHeader(creds),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -107,20 +125,16 @@ export async function refreshViaplaySession(
|
||||||
Origin: "https://viaplay.com",
|
Origin: "https://viaplay.com",
|
||||||
Referer: "https://viaplay.com/",
|
Referer: "https://viaplay.com/",
|
||||||
"User-Agent": VIAPLAY_USER_AGENT,
|
"User-Agent": VIAPLAY_USER_AGENT,
|
||||||
Cookie: `persistentLogin=${pl}`,
|
Cookie: `persistentLogin=${cookieValue(pl)}`,
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(opts.timeoutMs),
|
signal: AbortSignal.timeout(opts.timeoutMs),
|
||||||
});
|
});
|
||||||
|
|
||||||
const jar: Record<string, string> = {};
|
|
||||||
const rawSet =
|
const rawSet =
|
||||||
typeof res.headers.getSetCookie === "function"
|
typeof res.headers.getSetCookie === "function"
|
||||||
? res.headers.getSetCookie()
|
? res.headers.getSetCookie()
|
||||||
: [];
|
: [];
|
||||||
for (const sc of rawSet) {
|
const jar = parseSetCookieJar(rawSet);
|
||||||
const m = sc.match(/^([^=]+)=([^;]*)/);
|
|
||||||
if (m) jar[m[1]!] = m[2]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body: Record<string, unknown> | null = null;
|
let body: Record<string, unknown> | null = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -147,6 +161,7 @@ export async function refreshViaplaySession(
|
||||||
session,
|
session,
|
||||||
accessToken,
|
accessToken,
|
||||||
persistentLogin: nextPl,
|
persistentLogin: nextPl,
|
||||||
|
boid: jar["__Secure-Viaplay-Boid"] || creds.boid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -793,73 +808,78 @@ function extractProducts(json: unknown): ViaplayEpgProduct[] {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't go is Viaplay's soft-fail (200 + promo-VOD) i.p.v. 403.
|
* Don't go is Viaplay's soft-fail (200 + promo-VOD) i.p.v. 403.
|
||||||
* Titel/guid van het event kloppen vaak wél; alleen de media is fout.
|
* Titel/guid/body.duration van het gevraagde product kloppen wél;
|
||||||
|
* alleen de media (en de echte MPD-duur) is de promo-docu (~PT43M36S).
|
||||||
*/
|
*/
|
||||||
const VIAPLAY_DONT_GO_MEDIA_RE =
|
const VIAPLAY_DONT_GO_MEDIA_RE =
|
||||||
/88000100-1778484912807|\b7c4cd64\b|\/dont[-_]?go/i;
|
/88000100-1778484912807|\b7c4cd64\b|\/dont[-_]?go/i;
|
||||||
|
/** Bekende mediaPresentationDuration van de Don't go-docu. */
|
||||||
|
const DONT_GO_MPD_DURATION_MS = 2_616_000;
|
||||||
|
|
||||||
export function isDontGoPlaceholder(mpdUrl: string | null | undefined): boolean {
|
export function isDontGoPlaceholder(mpdUrl: string | null | undefined): boolean {
|
||||||
return !!mpdUrl && VIAPLAY_DONT_GO_MEDIA_RE.test(mpdUrl);
|
return !!mpdUrl && VIAPLAY_DONT_GO_MEDIA_RE.test(mpdUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseIsoDurationMs(iso: string): number | null {
|
||||||
|
const t = String(iso || "").trim();
|
||||||
|
if (!t) return null;
|
||||||
|
const m = t.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?$/i);
|
||||||
|
if (!m) return null;
|
||||||
|
const h = Number(m[1] || 0);
|
||||||
|
const min = Number(m[2] || 0);
|
||||||
|
const s = Number(m[3] || 0);
|
||||||
|
if (![h, min, s].some((n) => Number.isFinite(n) && n > 0)) return null;
|
||||||
|
return Math.round(((h * 60 + min) * 60 + s) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMpdDurationMs(xml: string): number | null {
|
||||||
|
const raw =
|
||||||
|
xml.match(/\bmediaPresentationDuration=["']([^"']+)["']/i)?.[1] ||
|
||||||
|
xml.match(/<Period\b[^>]*\bduration=["']([^"']+)["']/i)?.[1];
|
||||||
|
return raw ? parseIsoDurationMs(raw) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mpdLooksLikeDontGo(
|
||||||
|
xml: string,
|
||||||
|
expectedDurationMs?: number | null
|
||||||
|
): boolean {
|
||||||
|
if (/don'?t\s*go|viaplay\s*presents/i.test(xml)) return true;
|
||||||
|
const mpdMs = parseMpdDurationMs(xml);
|
||||||
|
if (mpdMs == null) return false;
|
||||||
|
if (Math.abs(mpdMs - DONT_GO_MPD_DURATION_MS) <= 4_000) return true;
|
||||||
|
const expected = Number(expectedDurationMs);
|
||||||
|
if (Number.isFinite(expected) && expected > 30_000) {
|
||||||
|
const ratio = mpdMs / expected;
|
||||||
|
if (ratio < 0.55 || ratio > 1.8) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function isLiveDashMpd(mpdUrl: string | null | undefined): boolean {
|
export function isLiveDashMpd(mpdUrl: string | null | undefined): boolean {
|
||||||
return !!mpdUrl && /live-dash/i.test(mpdUrl);
|
return !!mpdUrl && /live-dash/i.test(mpdUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertPlayNotSoftFail(
|
function assertPlayNotSoftFail(played: ViaplayPlayResult): void {
|
||||||
played: ViaplayPlayResult,
|
|
||||||
opts?: { requireLiveDash?: 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 (/vod-dash/i.test(played.mpdUrl) && !played.licenseUrl) {
|
if (!isLiveDashMpd(played.mpdUrl)) {
|
||||||
throw new Error("Viaplay gaf Don't go-VOD zonder licentie");
|
|
||||||
}
|
|
||||||
if (!played.licenseUrl) {
|
|
||||||
throw new Error("Viaplay play-response mist Widevine-licentie (Don't go-fallback)");
|
|
||||||
}
|
|
||||||
if (opts?.requireLiveDash && !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)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
if (!played.licenseUrl) {
|
||||||
|
throw new Error("Viaplay play-response mist Widevine-licentie (Don't go-fallback)");
|
||||||
function sportContentUrls(
|
|
||||||
pageUrl: string | null | undefined,
|
|
||||||
deviceKey: string,
|
|
||||||
mediaGuid: string
|
|
||||||
): { productUrl: string; contextualUrl: string; contextualv2Url: string } {
|
|
||||||
const guidPath = mediaGuid.replace(/-NL$/i, "").toLowerCase();
|
|
||||||
let clean = "";
|
|
||||||
const raw = (pageUrl || "").trim();
|
|
||||||
if (raw) {
|
|
||||||
try {
|
|
||||||
const u = new URL(raw);
|
|
||||||
u.search = "";
|
|
||||||
clean = u.toString().replace(/\/$/, "");
|
|
||||||
} catch {
|
|
||||||
clean = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!clean) {
|
|
||||||
clean = `https://content.viaplay.com/${deviceKey}/sport/${guidPath}`;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
productUrl: `${clean}?partial=true`,
|
|
||||||
contextualUrl: `${clean}?contextual=true`,
|
|
||||||
contextualv2Url: `${clean}?contextualV2=true&productType=sport`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPlayLiveUrl(
|
function buildPlayLiveUrl(
|
||||||
creds: ViaplayAccountCreds,
|
creds: ViaplayAccountCreds,
|
||||||
mediaGuid: string,
|
mediaGuid: string,
|
||||||
opts: { deviceKey: string; pageUrl?: string | null; harStyle: boolean }
|
opts: { deviceKey: string }
|
||||||
): string {
|
): string {
|
||||||
const u = new URL("https://play-live.viaplay.com/api/stream/bymediaguid");
|
const u = new URL("https://play-live.viaplay.com/api/stream/bymediaguid");
|
||||||
u.searchParams.set("deviceId", creds.deviceId || randomUUID());
|
u.searchParams.set("deviceId", creds.deviceId || randomUUID());
|
||||||
|
|
@ -869,26 +889,6 @@ function buildPlayLiveUrl(
|
||||||
u.searchParams.set("deviceKey", opts.deviceKey);
|
u.searchParams.set("deviceKey", opts.deviceKey);
|
||||||
u.searchParams.set("mediaGuid", mediaGuid);
|
u.searchParams.set("mediaGuid", mediaGuid);
|
||||||
u.searchParams.set("nocc", "autoplayer");
|
u.searchParams.set("nocc", "autoplayer");
|
||||||
if (creds.profileId) u.searchParams.set("profileId", creds.profileId);
|
|
||||||
if (!opts.harStyle) return u.toString();
|
|
||||||
|
|
||||||
const pages = sportContentUrls(opts.pageUrl, opts.deviceKey, mediaGuid);
|
|
||||||
const returnUrl = new URL(`https://content.viaplay.com/${opts.deviceKey}`);
|
|
||||||
returnUrl.searchParams.set("language", "nl");
|
|
||||||
if (creds.profileId) returnUrl.searchParams.set("profileId", creds.profileId);
|
|
||||||
u.searchParams.set("cse", "true");
|
|
||||||
u.searchParams.set(
|
|
||||||
"splitIOFlagIds",
|
|
||||||
"all_sports_beyondLiveBeacon_on,web_player_pause_ads_on"
|
|
||||||
);
|
|
||||||
u.searchParams.set("tcString", VIAPLAY_TC_STRING);
|
|
||||||
u.searchParams.set("returnurl", returnUrl.toString());
|
|
||||||
u.searchParams.set("producturl", pages.productUrl);
|
|
||||||
u.searchParams.set("contextualurl", pages.contextualUrl);
|
|
||||||
u.searchParams.set("contextualv2url", pages.contextualv2Url);
|
|
||||||
u.searchParams.set("sectionPath", "/sport");
|
|
||||||
u.searchParams.set("defaultAvailabilityContext", "svod");
|
|
||||||
u.searchParams.set("win10edge", "true");
|
|
||||||
return u.toString();
|
return u.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -899,45 +899,27 @@ export async function playByMediaGuid(
|
||||||
deviceKey: string;
|
deviceKey: string;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
pageUrl?: string | null;
|
pageUrl?: string | null;
|
||||||
/** Terugkijken: echte VOD-MPD toestaan, Don't go niet. */
|
|
||||||
allowVod?: boolean;
|
allowVod?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<ViaplayPlayResult> {
|
): Promise<ViaplayPlayResult> {
|
||||||
|
void opts.pageUrl;
|
||||||
|
void opts.allowVod;
|
||||||
const mediaGuid = toMediaGuid(mediaGuidRaw);
|
const mediaGuid = toMediaGuid(mediaGuidRaw);
|
||||||
const urls = [
|
const playUrl = buildPlayLiveUrl(creds, mediaGuid, { deviceKey: opts.deviceKey });
|
||||||
buildPlayLiveUrl(creds, mediaGuid, {
|
const res = await fetchJson(playUrl, {
|
||||||
deviceKey: opts.deviceKey,
|
headers: playHeaders(creds),
|
||||||
harStyle: false,
|
timeoutMs: opts.timeoutMs,
|
||||||
}),
|
});
|
||||||
buildPlayLiveUrl(creds, mediaGuid, {
|
const body = asRecord(res.json);
|
||||||
deviceKey: opts.deviceKey,
|
if (res.status !== 200 || !body) {
|
||||||
pageUrl: opts.pageUrl,
|
const msg =
|
||||||
harStyle: true,
|
(typeof body?.message === "string" && body.message) ||
|
||||||
}),
|
`play-live mislukt (${res.status})`;
|
||||||
];
|
throw new Error(msg);
|
||||||
|
|
||||||
let lastError: Error | null = null;
|
|
||||||
for (const playUrl of urls) {
|
|
||||||
try {
|
|
||||||
const res = await fetchJson(playUrl, {
|
|
||||||
headers: playHeaders(creds),
|
|
||||||
timeoutMs: opts.timeoutMs,
|
|
||||||
});
|
|
||||||
const body = asRecord(res.json);
|
|
||||||
if (res.status !== 200 || !body) {
|
|
||||||
const msg =
|
|
||||||
(typeof body?.message === "string" && body.message) ||
|
|
||||||
`play-live mislukt (${res.status})`;
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
const played = parsePlayStreamBody(body, mediaGuid);
|
|
||||||
assertPlayNotSoftFail(played, { requireLiveDash: !opts.allowVod });
|
|
||||||
return played;
|
|
||||||
} catch (err) {
|
|
||||||
lastError = err instanceof Error ? err : new Error(String(err));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw lastError || new Error("Viaplay play-live mislukt");
|
const played = parsePlayStreamBody(body, mediaGuid);
|
||||||
|
assertPlayNotSoftFail(played);
|
||||||
|
return played;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePlayStreamBody(
|
function parsePlayStreamBody(
|
||||||
|
|
@ -952,6 +934,7 @@ function parsePlayStreamBody(
|
||||||
if (!mpdUrl) throw new Error("Geen viaplay:media in play-response");
|
if (!mpdUrl) throw new Error("Geen viaplay:media in play-response");
|
||||||
const product = asRecord(body.product);
|
const product = asRecord(body.product);
|
||||||
const channel = asRecord(body.channel) || asRecord(asRecord(body.streamingSession)?.channel);
|
const channel = asRecord(body.channel) || asRecord(asRecord(body.streamingSession)?.channel);
|
||||||
|
const durationRaw = Number(body.duration);
|
||||||
return {
|
return {
|
||||||
mediaGuid: id,
|
mediaGuid: id,
|
||||||
mpdUrl,
|
mpdUrl,
|
||||||
|
|
@ -963,6 +946,8 @@ function parsePlayStreamBody(
|
||||||
typeof asRecord(product?.content)?.title === "string"
|
typeof asRecord(product?.content)?.title === "string"
|
||||||
? String(asRecord(product!.content)!.title)
|
? String(asRecord(product!.content)!.title)
|
||||||
: null,
|
: null,
|
||||||
|
durationMs:
|
||||||
|
Number.isFinite(durationRaw) && durationRaw > 0 ? durationRaw : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1080,9 +1065,6 @@ function assertVodPlayMatchesGuid(
|
||||||
"Viaplay gaf Don't go-placeholder i.p.v. de gevraagde stream"
|
"Viaplay gaf Don't go-placeholder i.p.v. de gevraagde stream"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (/vod-dash/i.test(played.mpdUrl) && !played.licenseUrl) {
|
|
||||||
throw new Error("Viaplay gaf Don't go-VOD zonder licentie");
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = asRecord(body._links);
|
const links = asRecord(body._links);
|
||||||
const productHref = asRecord(links?.["viaplay:product"])?.href;
|
const productHref = asRecord(links?.["viaplay:product"])?.href;
|
||||||
|
|
@ -1254,7 +1236,6 @@ export async function playByGuid(
|
||||||
contextualv2FromStreamHref(listingHref) ||
|
contextualv2FromStreamHref(listingHref) ||
|
||||||
contextualv2FromStreamHref(productHref);
|
contextualv2FromStreamHref(productHref);
|
||||||
|
|
||||||
// Eerst exacte HAR-query (betrouwbaar), daarna eventuele stream-hrefs.
|
|
||||||
const playUrls: string[] = [];
|
const playUrls: string[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const pushUrl = (url: string) => {
|
const pushUrl = (url: string) => {
|
||||||
|
|
@ -1262,6 +1243,14 @@ export async function playByGuid(
|
||||||
seen.add(url);
|
seen.add(url);
|
||||||
playUrls.push(url);
|
playUrls.push(url);
|
||||||
};
|
};
|
||||||
|
const simple = new URL("https://play.viaplay.com/api/stream/byguid");
|
||||||
|
simple.searchParams.set("deviceId", creds.deviceId || randomUUID());
|
||||||
|
simple.searchParams.set("deviceName", "web");
|
||||||
|
simple.searchParams.set("deviceType", "pc");
|
||||||
|
simple.searchParams.set("userAgent", VIAPLAY_USER_AGENT);
|
||||||
|
simple.searchParams.set("deviceKey", opts.deviceKey);
|
||||||
|
simple.searchParams.set("guid", guid);
|
||||||
|
pushUrl(simple.toString());
|
||||||
pushUrl(
|
pushUrl(
|
||||||
buildHarByguidUrl(creds, guid, {
|
buildHarByguidUrl(creds, guid, {
|
||||||
deviceKey: opts.deviceKey,
|
deviceKey: opts.deviceKey,
|
||||||
|
|
@ -1297,8 +1286,29 @@ export async function playByGuid(
|
||||||
durationMs: pageDurationMs,
|
durationMs: pageDurationMs,
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
});
|
});
|
||||||
// Alleen aanvullen als play al een echte (niet-promo) titel heeft — nooit
|
const expectedMs = pageDurationMs || played.durationMs;
|
||||||
// pageTitle over een lege promo-response plakken (dat verborgt Don't go).
|
let mpdChecked = false;
|
||||||
|
try {
|
||||||
|
const mpd = await fetchMpdXml(played.mpdUrl, opts.timeoutMs);
|
||||||
|
if (mpd.status === 200 && /<MPD[\s>]/i.test(mpd.xml)) {
|
||||||
|
mpdChecked = true;
|
||||||
|
if (
|
||||||
|
isDontGoPlaceholder(mpd.url) ||
|
||||||
|
mpdLooksLikeDontGo(mpd.xml, expectedMs)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Viaplay MPD is Don't go-placeholder (duur/media mismatch)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && /Don't go/i.test(err.message)) throw err;
|
||||||
|
}
|
||||||
|
if (!mpdChecked && /vod-dash/i.test(played.mpdUrl) && !played.licenseUrl) {
|
||||||
|
throw new Error(
|
||||||
|
"VOD zonder licentie en MPD niet te valideren — Don't go-risico"
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!played.title?.trim() && pageTitle && !isPromoVodTitle(pageTitle)) {
|
if (!played.title?.trim() && pageTitle && !isPromoVodTitle(pageTitle)) {
|
||||||
played.title = pageTitle;
|
played.title = pageTitle;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
fetchViaplayBootstrapPssh,
|
fetchViaplayBootstrapPssh,
|
||||||
isDontGoPlaceholder,
|
isDontGoPlaceholder,
|
||||||
isLiveDashMpd,
|
isLiveDashMpd,
|
||||||
|
mpdLooksLikeDontGo,
|
||||||
VIAPLAY_USER_AGENT,
|
VIAPLAY_USER_AGENT,
|
||||||
} from "./client";
|
} from "./client";
|
||||||
import {
|
import {
|
||||||
|
|
@ -51,7 +52,7 @@ function recordVodPlay( partial: Omit<LastVodPlayDiag, "at" | "resolver"> ) {
|
||||||
lastVodPlay = {
|
lastVodPlay = {
|
||||||
...partial,
|
...partial,
|
||||||
at: new Date().toISOString(),
|
at: new Date().toISOString(),
|
||||||
resolver: "viaplay-byguid-har-v8",
|
resolver: "viaplay-byguid-v9",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,9 +210,15 @@ export async function ensureViaplayEventReady(
|
||||||
!!event.mpdUrl &&
|
!!event.mpdUrl &&
|
||||||
isMpdCacheFresh(event.mpdCachedAt, tweaks.mpdCacheTtlMinutes);
|
isMpdCacheFresh(event.mpdCachedAt, tweaks.mpdCacheTtlMinutes);
|
||||||
|
|
||||||
if (cacheFresh && cachedKeys.length && !isDontGoPlaceholder(event.mpdUrl)) {
|
if (
|
||||||
|
cacheFresh &&
|
||||||
|
cachedKeys.length &&
|
||||||
|
event.mpdUrl &&
|
||||||
|
isLiveDashMpd(event.mpdUrl) &&
|
||||||
|
!isDontGoPlaceholder(event.mpdUrl)
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
streamUrl: event.mpdUrl!,
|
streamUrl: event.mpdUrl,
|
||||||
format: "dash",
|
format: "dash",
|
||||||
keys: cachedKeys,
|
keys: cachedKeys,
|
||||||
fromCache: true,
|
fromCache: true,
|
||||||
|
|
@ -265,6 +272,7 @@ export async function ensureViaplayEventReady(
|
||||||
if (
|
if (
|
||||||
cacheFresh &&
|
cacheFresh &&
|
||||||
event.mpdUrl &&
|
event.mpdUrl &&
|
||||||
|
isLiveDashMpd(event.mpdUrl) &&
|
||||||
!isDontGoPlaceholder(event.mpdUrl) &&
|
!isDontGoPlaceholder(event.mpdUrl) &&
|
||||||
!(opts?.fetchKeys ?? tweaks.fetchKeysIfMissing)
|
!(opts?.fetchKeys ?? tweaks.fetchKeysIfMissing)
|
||||||
) {
|
) {
|
||||||
|
|
@ -297,6 +305,7 @@ export async function ensureViaplayEventReady(
|
||||||
session: refreshed.session || undefined,
|
session: refreshed.session || undefined,
|
||||||
accessToken: refreshed.accessToken || undefined,
|
accessToken: refreshed.accessToken || undefined,
|
||||||
persistentLogin: refreshed.persistentLogin || undefined,
|
persistentLogin: refreshed.persistentLogin || undefined,
|
||||||
|
boid: refreshed.boid || undefined,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* continue with in-memory refresh */
|
/* continue with in-memory refresh */
|
||||||
|
|
@ -308,7 +317,6 @@ export async function ensureViaplayEventReady(
|
||||||
deviceKey: tweaks.deviceKey,
|
deviceKey: tweaks.deviceKey,
|
||||||
timeoutMs: tweaks.httpTimeoutMs,
|
timeoutMs: tweaks.httpTimeoutMs,
|
||||||
pageUrl: event.pageUrl,
|
pageUrl: event.pageUrl,
|
||||||
allowVod: ended,
|
|
||||||
});
|
});
|
||||||
const kap = isLiveDashMpd(play.mpdUrl)
|
const kap = isLiveDashMpd(play.mpdUrl)
|
||||||
? await resolveKapMpd(
|
? await resolveKapMpd(
|
||||||
|
|
@ -507,6 +515,7 @@ async function resolveViaplayGuidPlayInner(
|
||||||
session: refreshed.session || undefined,
|
session: refreshed.session || undefined,
|
||||||
accessToken: refreshed.accessToken || undefined,
|
accessToken: refreshed.accessToken || undefined,
|
||||||
persistentLogin: refreshed.persistentLogin || undefined,
|
persistentLogin: refreshed.persistentLogin || undefined,
|
||||||
|
boid: refreshed.boid || undefined,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* play mag doorgaan met in-memory refresh */
|
/* play mag doorgaan met in-memory refresh */
|
||||||
|
|
@ -530,11 +539,19 @@ async function resolveViaplayGuidPlayInner(
|
||||||
try {
|
try {
|
||||||
const mpd = await fetchMpdXml(play.mpdUrl, tweaks.httpTimeoutMs);
|
const mpd = await fetchMpdXml(play.mpdUrl, tweaks.httpTimeoutMs);
|
||||||
if (mpd.status === 200 && /<MPD[\s>]/i.test(mpd.xml)) {
|
if (mpd.status === 200 && /<MPD[\s>]/i.test(mpd.xml)) {
|
||||||
|
if (
|
||||||
|
isDontGoPlaceholder(mpd.url) ||
|
||||||
|
mpdLooksLikeDontGo(mpd.xml, play.durationMs)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Viaplay MPD is Don't go-placeholder (duur/media mismatch)"
|
||||||
|
);
|
||||||
|
}
|
||||||
pssh = extractPsshFromMpd(mpd.xml);
|
pssh = extractPsshFromMpd(mpd.xml);
|
||||||
mpdUrl = mpd.url;
|
mpdUrl = mpd.url;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
/* speel base MPD */
|
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.
|
// Clear VOD (HAR: geen license) — nooit stale ClearKey van eerdere plays plakken.
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@ export async function addViaplayAccount(
|
||||||
session?: string;
|
session?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
|
boid?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -233,6 +234,7 @@ export async function upsertViaplayAccountByEmail(
|
||||||
session: string;
|
session: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
|
boid?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -252,6 +254,7 @@ export async function upsertViaplayAccountByEmail(
|
||||||
session: input.session,
|
session: input.session,
|
||||||
accessToken: input.accessToken,
|
accessToken: input.accessToken,
|
||||||
profileId: input.profileId,
|
profileId: input.profileId,
|
||||||
|
boid: input.boid,
|
||||||
persistentLogin: input.persistentLogin,
|
persistentLogin: input.persistentLogin,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
@ -262,6 +265,7 @@ export async function upsertViaplayAccountByEmail(
|
||||||
session: input.session,
|
session: input.session,
|
||||||
accessToken: input.accessToken,
|
accessToken: input.accessToken,
|
||||||
profileId: input.profileId,
|
profileId: input.profileId,
|
||||||
|
boid: input.boid,
|
||||||
persistentLogin: input.persistentLogin,
|
persistentLogin: input.persistentLogin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -276,6 +280,7 @@ export async function updateViaplayAccount(
|
||||||
session?: string;
|
session?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
|
boid?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
clearCookies?: boolean;
|
clearCookies?: boolean;
|
||||||
|
|
@ -296,11 +301,13 @@ export async function updateViaplayAccount(
|
||||||
let sessionEnc = current.sessionEnc;
|
let sessionEnc = current.sessionEnc;
|
||||||
let accessTokenEnc = current.accessTokenEnc;
|
let accessTokenEnc = current.accessTokenEnc;
|
||||||
let profileId = current.profileId;
|
let profileId = current.profileId;
|
||||||
|
let boid = current.boid;
|
||||||
let persistentLoginEnc = current.persistentLoginEnc;
|
let persistentLoginEnc = current.persistentLoginEnc;
|
||||||
if (input.clearCookies) {
|
if (input.clearCookies) {
|
||||||
sessionEnc = undefined;
|
sessionEnc = undefined;
|
||||||
accessTokenEnc = undefined;
|
accessTokenEnc = undefined;
|
||||||
profileId = undefined;
|
profileId = undefined;
|
||||||
|
boid = undefined;
|
||||||
persistentLoginEnc = undefined;
|
persistentLoginEnc = undefined;
|
||||||
}
|
}
|
||||||
if (input.session?.trim()) {
|
if (input.session?.trim()) {
|
||||||
|
|
@ -316,6 +323,9 @@ export async function updateViaplayAccount(
|
||||||
if (input.profileId !== undefined) {
|
if (input.profileId !== undefined) {
|
||||||
profileId = input.profileId.trim() || undefined;
|
profileId = input.profileId.trim() || undefined;
|
||||||
}
|
}
|
||||||
|
if (input.boid !== undefined) {
|
||||||
|
boid = input.boid.trim() || undefined;
|
||||||
|
}
|
||||||
if (input.persistentLogin?.trim()) {
|
if (input.persistentLogin?.trim()) {
|
||||||
persistentLoginEnc = encryptSecret(input.persistentLogin.trim(), sessionSecret);
|
persistentLoginEnc = encryptSecret(input.persistentLogin.trim(), sessionSecret);
|
||||||
}
|
}
|
||||||
|
|
@ -339,6 +349,7 @@ export async function updateViaplayAccount(
|
||||||
sessionEnc,
|
sessionEnc,
|
||||||
accessTokenEnc,
|
accessTokenEnc,
|
||||||
profileId,
|
profileId,
|
||||||
|
boid,
|
||||||
persistentLoginEnc,
|
persistentLoginEnc,
|
||||||
passwordEnc: input.password?.trim()
|
passwordEnc: input.password?.trim()
|
||||||
? encryptSecret(input.password.trim(), sessionSecret)
|
? encryptSecret(input.password.trim(), sessionSecret)
|
||||||
|
|
|
||||||
|
|
@ -1096,7 +1096,7 @@ export function registerViewerRoutes(
|
||||||
fallbackFormat: null,
|
fallbackFormat: null,
|
||||||
live: false,
|
live: false,
|
||||||
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
||||||
resolver: "viaplay-byguid-har-v8",
|
resolver: "viaplay-byguid-v9",
|
||||||
mediaPath: (() => {
|
mediaPath: (() => {
|
||||||
try {
|
try {
|
||||||
const u = new URL(played.streamUrl);
|
const u = new URL(played.streamUrl);
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ async function pushViaplay(
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
profileId: string | null;
|
profileId: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
|
boid?: string | null;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, {
|
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, {
|
||||||
|
|
@ -68,6 +69,7 @@ async function pushViaplay(
|
||||||
accessToken: tokens.accessToken,
|
accessToken: tokens.accessToken,
|
||||||
profileId: tokens.profileId || undefined,
|
profileId: tokens.profileId || undefined,
|
||||||
persistentLogin: tokens.persistentLogin || undefined,
|
persistentLogin: tokens.persistentLogin || undefined,
|
||||||
|
boid: tokens.boid || undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
|
const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export type ViaplayLoginResult = {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
profileId: string | null;
|
profileId: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
|
boid: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function profileDir(accountId: string): string {
|
function profileDir(accountId: string): string {
|
||||||
|
|
@ -96,8 +97,9 @@ async function readCookies(context: BrowserContext): Promise<ViaplayLoginResult
|
||||||
.replace(/^(VIAPLAY-AT|MTG-AT|Bearer)\s+/i, "");
|
.replace(/^(VIAPLAY-AT|MTG-AT|Bearer)\s+/i, "");
|
||||||
const profileId = (byName.get("viaplay_profileId") || "").trim() || null;
|
const profileId = (byName.get("viaplay_profileId") || "").trim() || null;
|
||||||
const persistentLogin = (byName.get("persistentLogin") || "").trim() || null;
|
const persistentLogin = (byName.get("persistentLogin") || "").trim() || null;
|
||||||
|
const boid = (byName.get("__Secure-Viaplay-Boid") || "").trim() || null;
|
||||||
if (!session || !accessToken) return null;
|
if (!session || !accessToken) return null;
|
||||||
return { session, accessToken, profileId, persistentLogin };
|
return { session, accessToken, profileId, persistentLogin, boid };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchProfile(userDataDir: string, headless: boolean) {
|
async function launchProfile(userDataDir: string, headless: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export type CapturedTokens = {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
profileId: string | null;
|
profileId: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
|
boid: string | null;
|
||||||
accessTokenExpMs: number | null;
|
accessTokenExpMs: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -49,7 +50,7 @@ export async function captureViaplayTokens(partition: string): Promise<CapturedT
|
||||||
if (!c.name || !c.value) continue;
|
if (!c.name || !c.value) continue;
|
||||||
if (
|
if (
|
||||||
/viaplay|mtg-api|login\.viaplay/i.test(c.domain || "") ||
|
/viaplay|mtg-api|login\.viaplay/i.test(c.domain || "") ||
|
||||||
["session", "accessToken", "viaplay_profileId", "persistentLogin"].includes(c.name)
|
["session", "accessToken", "viaplay_profileId", "persistentLogin", "__Secure-Viaplay-Boid"].includes(c.name)
|
||||||
) {
|
) {
|
||||||
byName.set(c.name, c.value);
|
byName.set(c.name, c.value);
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +65,7 @@ export async function captureViaplayTokens(partition: string): Promise<CapturedT
|
||||||
.replace(/^(VIAPLAY-AT|MTG-AT|Bearer)\s+/i, "");
|
.replace(/^(VIAPLAY-AT|MTG-AT|Bearer)\s+/i, "");
|
||||||
const profileId = byName.get("viaplay_profileId")?.trim() || null;
|
const profileId = byName.get("viaplay_profileId")?.trim() || null;
|
||||||
const persistentLogin = byName.get("persistentLogin")?.trim() || null;
|
const persistentLogin = byName.get("persistentLogin")?.trim() || null;
|
||||||
|
const boid = byName.get("__Secure-Viaplay-Boid")?.trim() || null;
|
||||||
|
|
||||||
if (!sessionVal || !accessToken) return null;
|
if (!sessionVal || !accessToken) return null;
|
||||||
return {
|
return {
|
||||||
|
|
@ -71,6 +73,7 @@ export async function captureViaplayTokens(partition: string): Promise<CapturedT
|
||||||
accessToken,
|
accessToken,
|
||||||
profileId,
|
profileId,
|
||||||
persistentLogin,
|
persistentLogin,
|
||||||
|
boid,
|
||||||
accessTokenExpMs: jwtExpMs(accessToken),
|
accessTokenExpMs: jwtExpMs(accessToken),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +96,7 @@ export async function pushViaplayTokens(
|
||||||
accessToken: tokens.accessToken,
|
accessToken: tokens.accessToken,
|
||||||
profileId: tokens.profileId || undefined,
|
profileId: tokens.profileId || undefined,
|
||||||
persistentLogin: tokens.persistentLogin || undefined,
|
persistentLogin: tokens.persistentLogin || undefined,
|
||||||
|
boid: tokens.boid || undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json().catch(() => ({}))) as {
|
const data = (await res.json().catch(() => ({}))) as {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue