From 1c0275c21d029fdc4890695167d987ac805f6ec3 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Tue, 22 Sep 2026 22:43:38 +0200 Subject: [PATCH] Stop guessing Viaplay packages; only show overview API pakketinfo. Remove session-based subscription inventing and surface a clear error when package lookup fails. --- .../admin-ui/src/app/scripts/ViaplayPanel.tsx | 6 +- apps/master-api/src/viaplay/client.ts | 194 +++++------------- apps/master-api/src/viaplay/settings.ts | 23 ++- 3 files changed, 78 insertions(+), 145 deletions(-) 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";