Stop guessing Viaplay packages; only show overview API pakketinfo.
Remove session-based subscription inventing and surface a clear error when package lookup fails.
This commit is contained in:
parent
8be634f783
commit
1c0275c21d
3 changed files with 78 additions and 145 deletions
|
|
@ -410,7 +410,11 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
{a.subscriptionSummary ? (
|
||||
<strong>{a.subscriptionSummary}</strong>
|
||||
) : (
|
||||
<span className="muted">nog niet opgehaald — druk Check</span>
|
||||
<span className="muted">
|
||||
{a.healthError?.includes("pakket")
|
||||
? "niet opgehaald (zie melding hieronder)"
|
||||
: "nog niet opgehaald — druk Check"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{a.partnerName && a.subscriptionSummary && !a.subscriptionSummary.includes(a.partnerName) ? (
|
||||
|
|
|
|||
|
|
@ -69,37 +69,56 @@ export type ViaplayOverview = {
|
|||
productNames: string[];
|
||||
summary: string;
|
||||
error?: string;
|
||||
/** Sessie werkt, maar pakketinfo ontbreekt/mislukt. */
|
||||
sessionOk?: boolean;
|
||||
};
|
||||
|
||||
export async function fetchAccountOverview(
|
||||
creds: ViaplayAccountCreds,
|
||||
timeoutMs: number
|
||||
): Promise<ViaplayOverview> {
|
||||
const attempts: Array<{ label: string; headers: Record<string, string> }> = [
|
||||
const baseHeaders: Record<string, string> = {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Language": "nl-NL,nl;q=0.9,en;q=0.8",
|
||||
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",
|
||||
};
|
||||
|
||||
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: {
|
||||
...authHeaders(creds),
|
||||
Accept: "application/json",
|
||||
...(creds.accessToken
|
||||
? { Authorization: `Bearer ${creds.accessToken}` }
|
||||
: {}),
|
||||
...baseHeaders,
|
||||
Cookie: cookie,
|
||||
...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "cookie",
|
||||
headers: { ...authHeaders(creds), Accept: "application/json" },
|
||||
url: "https://account.mtg-api.com/overview",
|
||||
headers: { ...baseHeaders, Cookie: cookie },
|
||||
},
|
||||
{
|
||||
label: "bearer",
|
||||
label: "bearer+deviceKey",
|
||||
url: "https://account.mtg-api.com/overview?deviceKey=pcdash-nl",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Origin: "https://viaplay.com",
|
||||
Referer: "https://viaplay.com/",
|
||||
"User-Agent": VIAPLAY_USER_AGENT,
|
||||
...(creds.accessToken
|
||||
? { Authorization: `Bearer ${creds.accessToken}` }
|
||||
: {}),
|
||||
...baseHeaders,
|
||||
...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}),
|
||||
...(creds.profileId ? { Cookie: `viaplay_profileId=${creds.profileId}` } : {}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -108,7 +127,7 @@ export async function fetchAccountOverview(
|
|||
let lastError = "Overview mislukt";
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
const res = await fetchJson("https://account.mtg-api.com/overview", {
|
||||
const res = await fetchJson(attempt.url, {
|
||||
headers: attempt.headers,
|
||||
timeoutMs: Math.min(timeoutMs, 15_000),
|
||||
});
|
||||
|
|
@ -116,21 +135,9 @@ export async function fetchAccountOverview(
|
|||
const body = asRecord(res.json);
|
||||
if (res.status === 200 && body) {
|
||||
const parsed = parseOverviewBody(body);
|
||||
if (!parsed.productNames.length || !parsed.partnerName) {
|
||||
const fromSession = subscriptionFromSession(creds.session);
|
||||
const partnerName = parsed.partnerName || fromSession.partnerName;
|
||||
const productNames = parsed.productNames.length
|
||||
? parsed.productNames
|
||||
: fromSession.productNames;
|
||||
const parts = [partnerName, productNames.join(", ")].filter(Boolean);
|
||||
return {
|
||||
...parsed,
|
||||
partnerName,
|
||||
productNames,
|
||||
summary: parts.join(" · ") || parsed.summary,
|
||||
};
|
||||
}
|
||||
return parsed;
|
||||
if (parsed.ok && parsed.summary) return parsed;
|
||||
lastError = "Overview gaf geen pakketinformatie terug";
|
||||
continue;
|
||||
}
|
||||
lastError =
|
||||
(typeof body?.message === "string" && body.message) ||
|
||||
|
|
@ -141,138 +148,51 @@ export async function fetchAccountOverview(
|
|||
}
|
||||
}
|
||||
|
||||
// Fallback: sessie geldig? (light content call)
|
||||
let sessionOk = false;
|
||||
try {
|
||||
const ping = await fetchJson(
|
||||
"https://content.viaplay.com/pcdash-nl?language=nl",
|
||||
{
|
||||
const ping = await fetchJson("https://content.viaplay.com/pcdash-nl?language=nl", {
|
||||
headers: { ...authHeaders(creds), Accept: "application/json" },
|
||||
timeoutMs: Math.min(timeoutMs, 12_000),
|
||||
}
|
||||
);
|
||||
});
|
||||
const root = asRecord(ping.json);
|
||||
const hasUser = Boolean(
|
||||
asRecord(root?._links)?.["viaplay:userData"] ||
|
||||
asRecord(root?._links)?.["viaplay:logout"]
|
||||
sessionOk = Boolean(
|
||||
ping.status === 200 &&
|
||||
(asRecord(root?._links)?.["viaplay:userData"] ||
|
||||
asRecord(root?._links)?.["viaplay:logout"])
|
||||
);
|
||||
if (ping.status === 200 && hasUser) {
|
||||
const fromSession = subscriptionFromSession(creds.session);
|
||||
const exp = accessTokenExpiry(creds.accessToken);
|
||||
const summary =
|
||||
fromSession.summary ||
|
||||
(exp
|
||||
? `Sessie actief · token tot ${exp}`
|
||||
: "Sessie actief (abonnementdetails niet beschikbaar)");
|
||||
return {
|
||||
ok: true,
|
||||
status: ping.status,
|
||||
partnerName: fromSession.partnerName,
|
||||
productNames: fromSession.productNames,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
// Laatste redmiddel: abonnement uit session-cookie (lokaal, geen extra call)
|
||||
const fromSession = subscriptionFromSession(creds.session);
|
||||
if (fromSession.summary) {
|
||||
return {
|
||||
ok: true,
|
||||
status: lastStatus || 200,
|
||||
partnerName: fromSession.partnerName,
|
||||
productNames: fromSession.productNames,
|
||||
summary: fromSession.summary,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: lastStatus,
|
||||
partnerName: null,
|
||||
productNames: [],
|
||||
summary: "",
|
||||
error: lastError,
|
||||
error: sessionOk
|
||||
? `Sessie geldig, pakketinfo niet opgehaald (${lastError})`
|
||||
: lastError,
|
||||
sessionOk,
|
||||
};
|
||||
}
|
||||
|
||||
/** Haal partner/pakket uit de Viaplay session-cookie (zonder netwerk). */
|
||||
export function subscriptionFromSession(session: string | null | undefined): {
|
||||
partnerName: string | null;
|
||||
productNames: string[];
|
||||
summary: string | null;
|
||||
} {
|
||||
if (!session?.trim()) {
|
||||
return { partnerName: null, productNames: [], summary: null };
|
||||
}
|
||||
try {
|
||||
let raw = session.trim();
|
||||
try {
|
||||
raw = decodeURIComponent(raw);
|
||||
} catch {
|
||||
/* already decoded */
|
||||
}
|
||||
// express-session vorm: s:j:{json}.{signature}
|
||||
const m = /^s:j:(.+)\.[^.]+$/s.exec(raw);
|
||||
const jsonText = m?.[1] ?? null;
|
||||
if (!jsonText) return { partnerName: null, productNames: [], summary: null };
|
||||
const data = JSON.parse(jsonText) as Record<string, unknown>;
|
||||
const contracts = String(data.vmsContracts ?? "").trim();
|
||||
const productNames: string[] = [];
|
||||
let partnerName: string | null = null;
|
||||
if (/tmobile|odido/i.test(contracts) || data.isVmsCustomer === true) {
|
||||
partnerName = "Odido Internet + TV";
|
||||
}
|
||||
if (/b2b_vtotal|vtotal/i.test(contracts)) productNames.push("Total / V Total");
|
||||
else if (/basis|basic/i.test(contracts)) productNames.push("Basis");
|
||||
else if (contracts) {
|
||||
productNames.push(contracts.replace(/\s+/g, " "));
|
||||
}
|
||||
if (data.isTveBCustomer === true && !productNames.length) {
|
||||
productNames.push("TV-pakket");
|
||||
}
|
||||
const parts = [partnerName, productNames.join(", ")].filter(Boolean);
|
||||
return {
|
||||
partnerName,
|
||||
productNames,
|
||||
summary: parts.length ? parts.join(" · ") : null,
|
||||
};
|
||||
} catch {
|
||||
return { partnerName: null, productNames: [], summary: null };
|
||||
}
|
||||
}
|
||||
|
||||
function accessTokenExpiry(token: string | null): string | null {
|
||||
if (!token) return null;
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(token.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString()
|
||||
) as { exp?: number };
|
||||
if (!payload.exp) return null;
|
||||
return new Date(payload.exp * 1000).toLocaleString("nl-NL");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function packageLabel(raw: unknown): string | null {
|
||||
const rec = asRecord(raw);
|
||||
if (!rec) return null;
|
||||
for (const key of ["name", "displayName", "title", "productName", "id"]) {
|
||||
for (const key of ["name", "displayName", "title", "productName"]) {
|
||||
const v = rec[key];
|
||||
if (typeof v === "string" && v.trim()) return v.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Alleen echte overview-velden — geen aannames over abonnementstype. */
|
||||
function parseOverviewBody(body: Record<string, unknown>): ViaplayOverview {
|
||||
const partner = asRecord(body.partnerInformation);
|
||||
const partnerName =
|
||||
typeof partner?.partnerDisplayName === "string"
|
||||
? partner.partnerDisplayName
|
||||
: typeof partner?.partnerId === "string"
|
||||
? partner.partnerId
|
||||
? partner.partnerDisplayName.trim()
|
||||
: null;
|
||||
const productNames = Array.isArray(partner?.productNames)
|
||||
? partner!.productNames.map((p) => String(p).trim()).filter(Boolean)
|
||||
|
|
@ -286,11 +206,11 @@ function parseOverviewBody(body: Record<string, unknown>): ViaplayOverview {
|
|||
|
||||
const parts = [partnerName, productNames.join(", ")].filter(Boolean);
|
||||
return {
|
||||
ok: true,
|
||||
ok: parts.length > 0,
|
||||
status: 200,
|
||||
partnerName,
|
||||
productNames,
|
||||
summary: parts.join(" · ") || "Actief abonnement",
|
||||
summary: parts.join(" · "),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -349,19 +349,28 @@ export async function probeViaplayAccounts(sessionSecret: string, accountId?: st
|
|||
}
|
||||
if (!account.deviceId) account.deviceId = creds.deviceId;
|
||||
const overview = await fetchAccountOverview(creds, config.httpTimeoutMs);
|
||||
if (!overview.ok) {
|
||||
account.healthStatus = "dead";
|
||||
account.healthCheckedAt = now;
|
||||
account.healthError = overview.error || `HTTP ${overview.status}`;
|
||||
account.subscriptionSummary = undefined;
|
||||
account.partnerName = undefined;
|
||||
} else {
|
||||
if (overview.ok && overview.summary) {
|
||||
account.healthStatus = "ok";
|
||||
account.healthCheckedAt = now;
|
||||
account.healthError = undefined;
|
||||
account.subscriptionSummary = overview.summary;
|
||||
account.partnerName = overview.partnerName ?? undefined;
|
||||
account.overviewCheckedAt = now;
|
||||
} else if (overview.sessionOk) {
|
||||
// Tokens werken, maar account.mtg-api.com/overview gaf geen pakket terug
|
||||
account.healthStatus = "ok";
|
||||
account.healthCheckedAt = now;
|
||||
account.subscriptionSummary = undefined;
|
||||
account.partnerName = undefined;
|
||||
account.healthError =
|
||||
overview.error || "Pakketinformatie niet opgehaald — vernieuw tokens en Check opnieuw";
|
||||
account.overviewCheckedAt = now;
|
||||
} else {
|
||||
account.healthStatus = "dead";
|
||||
account.healthCheckedAt = now;
|
||||
account.healthError = overview.error || `HTTP ${overview.status}`;
|
||||
account.subscriptionSummary = undefined;
|
||||
account.partnerName = undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
account.healthStatus = "dead";
|
||||
|
|
|
|||
Loading…
Reference in a new issue