diff --git a/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx b/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx
index cfbfa23..90bba9b 100644
--- a/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx
+++ b/apps/admin-ui/src/app/scripts/ViaplayPanel.tsx
@@ -410,7 +410,11 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
{a.subscriptionSummary ? (
{a.subscriptionSummary}
) : (
- nog niet opgehaald — druk Check
+
+ {a.healthError?.includes("pakket")
+ ? "niet opgehaald (zie melding hieronder)"
+ : "nog niet opgehaald — druk Check"}
+
)}
{a.partnerName && a.subscriptionSummary && !a.subscriptionSummary.includes(a.partnerName) ? (
diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts
index 4f00f22..3c16d32 100644
--- a/apps/master-api/src/viaplay/client.ts
+++ b/apps/master-api/src/viaplay/client.ts
@@ -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 {
- const attempts: Array<{ label: string; headers: Record }> = [
+ const baseHeaders: Record = {
+ 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 }> = [
{
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,139 +148,52 @@ 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",
- {
- headers: { ...authHeaders(creds), Accept: "application/json" },
- timeoutMs: Math.min(timeoutMs, 12_000),
- }
- );
+ 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;
- 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): ViaplayOverview {
const partner = asRecord(body.partnerInformation);
const partnerName =
typeof partner?.partnerDisplayName === "string"
- ? partner.partnerDisplayName
- : typeof partner?.partnerId === "string"
- ? partner.partnerId
- : null;
+ ? 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): 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(" · "),
};
}
diff --git a/apps/master-api/src/viaplay/settings.ts b/apps/master-api/src/viaplay/settings.ts
index 80716cb..72435e0 100644
--- a/apps/master-api/src/viaplay/settings.ts
+++ b/apps/master-api/src/viaplay/settings.ts
@@ -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";