Play Viaplay catch-up like the website: cookies only, no Authorization.
This commit is contained in:
parent
98c38826e1
commit
65c144d8b6
2 changed files with 135 additions and 25 deletions
|
|
@ -44,6 +44,33 @@ export function authHeaders(creds: ViaplayAccountCreds): Record<string, string>
|
|||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* HAR afspelen: play.viaplay.com / play-live krijgen GEEN Authorization.
|
||||
* Alleen cookies + browser-UA. VIAPLAY-AT op play geeft Don't go-fallback.
|
||||
*/
|
||||
export function playHeaders(creds: ViaplayAccountCreds): Record<string, string> {
|
||||
return {
|
||||
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",
|
||||
Referer: "https://viaplay.com/",
|
||||
"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),
|
||||
};
|
||||
}
|
||||
|
||||
/** TCF-string uit web-HAR (afspelen.har). */
|
||||
const VIAPLAY_TC_STRING =
|
||||
"CQq8bIAQq8bIAAcABBNLCxFsAP_gAEPgAAYgKaQQYAFAAYABAACsAFwAZAA4AB4AEAAJAAWgAyABoAD-AIoAjQBMAE4AKAAVAAtgBfgDCAMQAaAA3ABzAD8AICAQYBCACGAEYANEAcYA7wB4gD9AIQARMAi0BHAEdAJ2AVkAuoBigDOAGmAPUAe0A_gCAAETAIpARqAmKBOIE5AKTAWKAtkBeYC9gGMgMkAZYA1cBrgD-4IzAjOBO4CmgJ3QIQAFgAVAAuABwADwAIAASAAyABoAEUAJgAYgA0AB-AEIAIYAd4A_QCEQEcAR0AxQBnwD2gJiAUmAsUBeYDJAGWANXAf2BGYCdwAYSACAH4dADAD8ANEAixSAGAH4AaIBFgAA.f_wACHwAAAAA.IKaQQoAFAAYABAACsAFwAZAA4AB4AEAAJAAWgAyABoAD-AIoAjQBMAE4AKAAVAAtgBfgDCAMQAaAA3ABzAD8AICAQYBCACGAEYANEAcYA7wB4gD9AIQARMAi0BHAEdAJ2AVkAuoBigDOAGfANMAeoA9oB_AEAAImARSAjUBMUCcQJyAUmAsUBbIC8wF7AMZAZIAywBq4DXAH9wRmBGcCdwFNAAAA";
|
||||
|
||||
/**
|
||||
* Vernieuw session/accessToken via persistentLogin (zoals de webapp na cookie-expiry).
|
||||
* Zonder verse session-cookie geeft play.viaplay.com 403 of rare fallbacks.
|
||||
|
|
@ -781,28 +808,55 @@ export function isLiveDashMpd(mpdUrl: string | null | undefined): boolean {
|
|||
return !!mpdUrl && /live-dash/i.test(mpdUrl);
|
||||
}
|
||||
|
||||
function assertLivePlayNotSoftFail(played: ViaplayPlayResult): void {
|
||||
function assertPlayNotSoftFail(
|
||||
played: ViaplayPlayResult,
|
||||
opts?: { requireLiveDash?: 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)) {
|
||||
if (opts?.requireLiveDash && !isLiveDashMpd(played.mpdUrl)) {
|
||||
throw new Error(
|
||||
"Viaplay gaf geen live-dash stream (Don't go/VOD-fallback — sessie of rechten)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function playByMediaGuid(
|
||||
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(
|
||||
creds: ViaplayAccountCreds,
|
||||
mediaGuidRaw: string,
|
||||
opts: { deviceKey: string; timeoutMs: number; pageUrl?: string | null }
|
||||
): Promise<ViaplayPlayResult> {
|
||||
const mediaGuid = toMediaGuid(mediaGuidRaw);
|
||||
// Zelfde query als het werkende live-script. Extra HAR-svod/producturl
|
||||
// triggert bij terugkijken de Don't go-VOD-fallback.
|
||||
mediaGuid: string,
|
||||
opts: { deviceKey: string; pageUrl?: string | null; harStyle: boolean }
|
||||
): string {
|
||||
const u = new URL("https://play-live.viaplay.com/api/stream/bymediaguid");
|
||||
u.searchParams.set("deviceId", creds.deviceId || randomUUID());
|
||||
u.searchParams.set("deviceName", "web");
|
||||
|
|
@ -812,9 +866,57 @@ export async function playByMediaGuid(
|
|||
u.searchParams.set("mediaGuid", mediaGuid);
|
||||
u.searchParams.set("nocc", "autoplayer");
|
||||
if (creds.profileId) u.searchParams.set("profileId", creds.profileId);
|
||||
if (!opts.harStyle) return u.toString();
|
||||
|
||||
const res = await fetchJson(u.toString(), {
|
||||
headers: authHeaders(creds),
|
||||
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();
|
||||
}
|
||||
|
||||
export async function playByMediaGuid(
|
||||
creds: ViaplayAccountCreds,
|
||||
mediaGuidRaw: string,
|
||||
opts: {
|
||||
deviceKey: string;
|
||||
timeoutMs: number;
|
||||
pageUrl?: string | null;
|
||||
/** Terugkijken: echte VOD-MPD toestaan, Don't go niet. */
|
||||
allowVod?: boolean;
|
||||
}
|
||||
): Promise<ViaplayPlayResult> {
|
||||
const mediaGuid = toMediaGuid(mediaGuidRaw);
|
||||
const urls = [
|
||||
buildPlayLiveUrl(creds, mediaGuid, {
|
||||
deviceKey: opts.deviceKey,
|
||||
pageUrl: opts.pageUrl,
|
||||
harStyle: true,
|
||||
}),
|
||||
buildPlayLiveUrl(creds, mediaGuid, {
|
||||
deviceKey: opts.deviceKey,
|
||||
harStyle: false,
|
||||
}),
|
||||
];
|
||||
|
||||
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);
|
||||
|
|
@ -825,8 +927,13 @@ export async function playByMediaGuid(
|
|||
throw new Error(msg);
|
||||
}
|
||||
const played = parsePlayStreamBody(body, mediaGuid);
|
||||
assertLivePlayNotSoftFail(played);
|
||||
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");
|
||||
}
|
||||
|
||||
function parsePlayStreamBody(
|
||||
|
|
@ -875,6 +982,7 @@ export function expandViaplayStreamHref(
|
|||
u.searchParams.set("userAgent", VIAPLAY_USER_AGENT);
|
||||
u.searchParams.set("deviceKey", opts.deviceKey);
|
||||
u.searchParams.set("cse", "true");
|
||||
u.searchParams.set("tcString", VIAPLAY_TC_STRING);
|
||||
u.searchParams.set("win10edge", "true");
|
||||
if (!u.searchParams.get("defaultAvailabilityContext")) {
|
||||
u.searchParams.set("defaultAvailabilityContext", "svod");
|
||||
|
|
@ -915,6 +1023,7 @@ function buildHarByguidUrl(
|
|||
"splitIOFlagIds",
|
||||
"all_sports_beyondLiveBeacon_on,web_player_pause_ads_on"
|
||||
);
|
||||
u.searchParams.set("tcString", VIAPLAY_TC_STRING);
|
||||
u.searchParams.set("guid", guid);
|
||||
u.searchParams.set("returnurl", opts.returnUrl);
|
||||
u.searchParams.set("producturl", productUrlFinal);
|
||||
|
|
@ -1055,7 +1164,7 @@ async function fetchProductStreamMeta(
|
|||
/* keep */
|
||||
}
|
||||
const res = await fetchJson(url, {
|
||||
headers: { ...authHeaders(creds), Accept: "application/json" },
|
||||
headers: { ...playHeaders(creds), Accept: "application/json" },
|
||||
timeoutMs,
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
|
|
@ -1166,7 +1275,7 @@ export async function playByGuid(
|
|||
for (const playUrl of playUrls) {
|
||||
try {
|
||||
const res = await fetchJson(playUrl, {
|
||||
headers: authHeaders(creds),
|
||||
headers: playHeaders(creds),
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
const body = asRecord(res.json);
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ export async function ensureViaplayEventReady(
|
|||
deviceKey: tweaks.deviceKey,
|
||||
timeoutMs: tweaks.httpTimeoutMs,
|
||||
pageUrl: event.pageUrl,
|
||||
allowVod: ended,
|
||||
});
|
||||
const kap = isLiveDashMpd(play.mpdUrl)
|
||||
? await resolveKapMpd(
|
||||
|
|
@ -371,7 +372,7 @@ export async function ensureViaplayEventReady(
|
|||
if (isDontGoPlaceholder(play.mpdUrl) || isDontGoPlaceholder(kap.mpdUrl)) {
|
||||
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de wedstrijdstream");
|
||||
}
|
||||
if (!isLiveDashMpd(play.mpdUrl)) {
|
||||
if (!ended && !isLiveDashMpd(play.mpdUrl)) {
|
||||
throw new Error("Viaplay gaf geen live-dash stream (Don't go/VOD-fallback)");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue