Fix Viaplay overview Forbidden by using Bearer-only auth.

Match manage-account HAR: no cookies on overview, and surface expired accessToken clearly.
This commit is contained in:
Jos Vooges | STH 2026-09-22 22:47:40 +02:00
parent 1c0275c21d
commit f19c173369

View file

@ -73,79 +73,87 @@ export type ViaplayOverview = {
sessionOk?: boolean; sessionOk?: boolean;
}; };
function normalizeAccessToken(raw: string | null | undefined): string | null {
if (!raw) return null;
const trimmed = raw.trim();
if (!trimmed) return null;
return trimmed.replace(/^Bearer\s+/i, "").trim() || null;
}
function accessTokenExpiryInfo(token: string): {
expired: boolean;
expMs: number | null;
} {
try {
const payload = JSON.parse(
Buffer.from(token.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString()
) as { exp?: number };
if (!payload.exp) return { expired: false, expMs: null };
const expMs = payload.exp * 1000;
return { expired: Date.now() >= expMs, expMs };
} catch {
return { expired: false, expMs: null };
}
}
/**
* Pakketinfo komt van account.mtg-api.com/overview.
* Browser stuurt alleen Authorization: Bearer (geen cookies) — cookies geven Forbidden.
*/
export async function fetchAccountOverview( export async function fetchAccountOverview(
creds: ViaplayAccountCreds, creds: ViaplayAccountCreds,
timeoutMs: number timeoutMs: number
): Promise<ViaplayOverview> { ): Promise<ViaplayOverview> {
const baseHeaders: Record<string, string> = { const token = normalizeAccessToken(creds.accessToken);
Accept: "application/json, text/plain, */*", if (!token) {
return {
ok: false,
status: 0,
partnerName: null,
productNames: [],
summary: "",
error: "Geen accessToken",
sessionOk: false,
};
}
const tokenInfo = accessTokenExpiryInfo(token);
// Exact zoals manage-account in de browser (HAR): Bearer-only + Content-Type
const headers: Record<string, string> = {
Accept: "*/*",
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept-Language": "nl-NL,nl;q=0.9,en;q=0.8", "Accept-Language": "nl,en-US;q=0.7,en;q=0.3",
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", Authorization: `Bearer ${token}`,
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
}; };
const cookie = [
creds.session ? `session=${creds.session}` : "",
creds.accessToken ? `accessToken=${creds.accessToken}` : "",
creds.profileId ? `viaplay_profileId=${creds.profileId}` : "",
]
.filter(Boolean)
.join("; ");
const attempts: Array<{ label: string; url: string; headers: Record<string, string> }> = [
{
label: "cookie+bearer",
url: "https://account.mtg-api.com/overview",
headers: {
...baseHeaders,
Cookie: cookie,
...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}),
},
},
{
label: "cookie",
url: "https://account.mtg-api.com/overview",
headers: { ...baseHeaders, Cookie: cookie },
},
{
label: "bearer+deviceKey",
url: "https://account.mtg-api.com/overview?deviceKey=pcdash-nl",
headers: {
...baseHeaders,
...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}),
...(creds.profileId ? { Cookie: `viaplay_profileId=${creds.profileId}` } : {}),
},
},
];
let lastStatus = 0; let lastStatus = 0;
let lastError = "Overview mislukt"; let lastError = "Overview mislukt";
for (const attempt of attempts) { try {
try { const res = await fetchJson("https://account.mtg-api.com/overview", {
const res = await fetchJson(attempt.url, { headers,
headers: attempt.headers, timeoutMs: Math.min(timeoutMs, 15_000),
timeoutMs: Math.min(timeoutMs, 15_000), });
}); lastStatus = res.status;
lastStatus = res.status; const body = asRecord(res.json);
const body = asRecord(res.json); if (res.status === 200 && body) {
if (res.status === 200 && body) { const parsed = parseOverviewBody(body);
const parsed = parseOverviewBody(body); if (parsed.ok && parsed.summary) return parsed;
if (parsed.ok && parsed.summary) return parsed; lastError = "Overview gaf geen pakketinformatie terug";
lastError = "Overview gaf geen pakketinformatie terug"; } else {
continue;
}
lastError = lastError =
(typeof body?.message === "string" && body.message) || (typeof body?.message === "string" && body.message) ||
(typeof body?.userMessage === "string" && body.userMessage) || (typeof body?.userMessage === "string" && body.userMessage) ||
`Overview HTTP ${res.status}`; (res.status === 403
} catch (err) { ? "Forbidden"
lastError = err instanceof Error ? err.message : String(err); : res.status === 401
? "Unauthorized"
: `Overview HTTP ${res.status}`);
} }
} catch (err) {
lastError = err instanceof Error ? err.message : String(err);
} }
let sessionOk = false; let sessionOk = false;
@ -164,6 +172,15 @@ export async function fetchAccountOverview(
/* ignore */ /* ignore */
} }
if (
sessionOk &&
(lastStatus === 401 || lastStatus === 403 || /forbidden|unauthorized/i.test(lastError))
) {
lastError = tokenInfo.expired
? "accessToken verlopen — plak een verse accessToken uit de browser en Check opnieuw"
: "overview Forbidden — plak een verse accessToken (Bearer) uit de browser; sessiecookie is niet genoeg";
}
return { return {
ok: false, ok: false,
status: lastStatus, status: lastStatus,