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:
parent
1c0275c21d
commit
f19c173369
1 changed files with 74 additions and 57 deletions
|
|
@ -73,62 +73,67 @@ export type ViaplayOverview = {
|
|||
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(
|
||||
creds: ViaplayAccountCreds,
|
||||
timeoutMs: number
|
||||
): Promise<ViaplayOverview> {
|
||||
const baseHeaders: Record<string, string> = {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
const token = normalizeAccessToken(creds.accessToken);
|
||||
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",
|
||||
"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",
|
||||
Referer: "https://viaplay.com/",
|
||||
"User-Agent": VIAPLAY_USER_AGENT,
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
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 lastError = "Overview mislukt";
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
const res = await fetchJson(attempt.url, {
|
||||
headers: attempt.headers,
|
||||
const res = await fetchJson("https://account.mtg-api.com/overview", {
|
||||
headers,
|
||||
timeoutMs: Math.min(timeoutMs, 15_000),
|
||||
});
|
||||
lastStatus = res.status;
|
||||
|
|
@ -137,16 +142,19 @@ export async function fetchAccountOverview(
|
|||
const parsed = parseOverviewBody(body);
|
||||
if (parsed.ok && parsed.summary) return parsed;
|
||||
lastError = "Overview gaf geen pakketinformatie terug";
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
lastError =
|
||||
(typeof body?.message === "string" && body.message) ||
|
||||
(typeof body?.userMessage === "string" && body.userMessage) ||
|
||||
`Overview HTTP ${res.status}`;
|
||||
(res.status === 403
|
||||
? "Forbidden"
|
||||
: res.status === 401
|
||||
? "Unauthorized"
|
||||
: `Overview HTTP ${res.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
|
||||
let sessionOk = false;
|
||||
try {
|
||||
|
|
@ -164,6 +172,15 @@ export async function fetchAccountOverview(
|
|||
/* 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 {
|
||||
ok: false,
|
||||
status: lastStatus,
|
||||
|
|
|
|||
Loading…
Reference in a new issue