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 ? (
|
{a.subscriptionSummary ? (
|
||||||
<strong>{a.subscriptionSummary}</strong>
|
<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>
|
</div>
|
||||||
{a.partnerName && a.subscriptionSummary && !a.subscriptionSummary.includes(a.partnerName) ? (
|
{a.partnerName && a.subscriptionSummary && !a.subscriptionSummary.includes(a.partnerName) ? (
|
||||||
|
|
|
||||||
|
|
@ -69,37 +69,56 @@ export type ViaplayOverview = {
|
||||||
productNames: string[];
|
productNames: string[];
|
||||||
summary: string;
|
summary: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
/** Sessie werkt, maar pakketinfo ontbreekt/mislukt. */
|
||||||
|
sessionOk?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchAccountOverview(
|
export async function fetchAccountOverview(
|
||||||
creds: ViaplayAccountCreds,
|
creds: ViaplayAccountCreds,
|
||||||
timeoutMs: number
|
timeoutMs: number
|
||||||
): Promise<ViaplayOverview> {
|
): 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",
|
label: "cookie+bearer",
|
||||||
|
url: "https://account.mtg-api.com/overview",
|
||||||
headers: {
|
headers: {
|
||||||
...authHeaders(creds),
|
...baseHeaders,
|
||||||
Accept: "application/json",
|
Cookie: cookie,
|
||||||
...(creds.accessToken
|
...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}),
|
||||||
? { Authorization: `Bearer ${creds.accessToken}` }
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "cookie",
|
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: {
|
headers: {
|
||||||
Accept: "application/json",
|
...baseHeaders,
|
||||||
Origin: "https://viaplay.com",
|
...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}),
|
||||||
Referer: "https://viaplay.com/",
|
...(creds.profileId ? { Cookie: `viaplay_profileId=${creds.profileId}` } : {}),
|
||||||
"User-Agent": VIAPLAY_USER_AGENT,
|
|
||||||
...(creds.accessToken
|
|
||||||
? { Authorization: `Bearer ${creds.accessToken}` }
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -108,7 +127,7 @@ export async function fetchAccountOverview(
|
||||||
let lastError = "Overview mislukt";
|
let lastError = "Overview mislukt";
|
||||||
for (const attempt of attempts) {
|
for (const attempt of attempts) {
|
||||||
try {
|
try {
|
||||||
const res = await fetchJson("https://account.mtg-api.com/overview", {
|
const res = await fetchJson(attempt.url, {
|
||||||
headers: attempt.headers,
|
headers: attempt.headers,
|
||||||
timeoutMs: Math.min(timeoutMs, 15_000),
|
timeoutMs: Math.min(timeoutMs, 15_000),
|
||||||
});
|
});
|
||||||
|
|
@ -116,21 +135,9 @@ export async function fetchAccountOverview(
|
||||||
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.productNames.length || !parsed.partnerName) {
|
if (parsed.ok && parsed.summary) return parsed;
|
||||||
const fromSession = subscriptionFromSession(creds.session);
|
lastError = "Overview gaf geen pakketinformatie terug";
|
||||||
const partnerName = parsed.partnerName || fromSession.partnerName;
|
continue;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
lastError =
|
lastError =
|
||||||
(typeof body?.message === "string" && body.message) ||
|
(typeof body?.message === "string" && body.message) ||
|
||||||
|
|
@ -141,139 +148,52 @@ export async function fetchAccountOverview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: sessie geldig? (light content call)
|
let sessionOk = false;
|
||||||
try {
|
try {
|
||||||
const ping = await fetchJson(
|
const ping = await fetchJson("https://content.viaplay.com/pcdash-nl?language=nl", {
|
||||||
"https://content.viaplay.com/pcdash-nl?language=nl",
|
headers: { ...authHeaders(creds), Accept: "application/json" },
|
||||||
{
|
timeoutMs: Math.min(timeoutMs, 12_000),
|
||||||
headers: { ...authHeaders(creds), Accept: "application/json" },
|
});
|
||||||
timeoutMs: Math.min(timeoutMs, 12_000),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const root = asRecord(ping.json);
|
const root = asRecord(ping.json);
|
||||||
const hasUser = Boolean(
|
sessionOk = Boolean(
|
||||||
asRecord(root?._links)?.["viaplay:userData"] ||
|
ping.status === 200 &&
|
||||||
asRecord(root?._links)?.["viaplay:logout"]
|
(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 {
|
} catch {
|
||||||
/* ignore */
|
/* 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 {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
status: lastStatus,
|
status: lastStatus,
|
||||||
partnerName: null,
|
partnerName: null,
|
||||||
productNames: [],
|
productNames: [],
|
||||||
summary: "",
|
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 {
|
function packageLabel(raw: unknown): string | null {
|
||||||
const rec = asRecord(raw);
|
const rec = asRecord(raw);
|
||||||
if (!rec) return null;
|
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];
|
const v = rec[key];
|
||||||
if (typeof v === "string" && v.trim()) return v.trim();
|
if (typeof v === "string" && v.trim()) return v.trim();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Alleen echte overview-velden — geen aannames over abonnementstype. */
|
||||||
function parseOverviewBody(body: Record<string, unknown>): ViaplayOverview {
|
function parseOverviewBody(body: Record<string, unknown>): ViaplayOverview {
|
||||||
const partner = asRecord(body.partnerInformation);
|
const partner = asRecord(body.partnerInformation);
|
||||||
const partnerName =
|
const partnerName =
|
||||||
typeof partner?.partnerDisplayName === "string"
|
typeof partner?.partnerDisplayName === "string"
|
||||||
? partner.partnerDisplayName
|
? partner.partnerDisplayName.trim()
|
||||||
: typeof partner?.partnerId === "string"
|
: null;
|
||||||
? partner.partnerId
|
|
||||||
: null;
|
|
||||||
const productNames = Array.isArray(partner?.productNames)
|
const productNames = Array.isArray(partner?.productNames)
|
||||||
? partner!.productNames.map((p) => String(p).trim()).filter(Boolean)
|
? 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);
|
const parts = [partnerName, productNames.join(", ")].filter(Boolean);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: parts.length > 0,
|
||||||
status: 200,
|
status: 200,
|
||||||
partnerName,
|
partnerName,
|
||||||
productNames,
|
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;
|
if (!account.deviceId) account.deviceId = creds.deviceId;
|
||||||
const overview = await fetchAccountOverview(creds, config.httpTimeoutMs);
|
const overview = await fetchAccountOverview(creds, config.httpTimeoutMs);
|
||||||
if (!overview.ok) {
|
if (overview.ok && overview.summary) {
|
||||||
account.healthStatus = "dead";
|
|
||||||
account.healthCheckedAt = now;
|
|
||||||
account.healthError = overview.error || `HTTP ${overview.status}`;
|
|
||||||
account.subscriptionSummary = undefined;
|
|
||||||
account.partnerName = undefined;
|
|
||||||
} else {
|
|
||||||
account.healthStatus = "ok";
|
account.healthStatus = "ok";
|
||||||
account.healthCheckedAt = now;
|
account.healthCheckedAt = now;
|
||||||
account.healthError = undefined;
|
account.healthError = undefined;
|
||||||
account.subscriptionSummary = overview.summary;
|
account.subscriptionSummary = overview.summary;
|
||||||
account.partnerName = overview.partnerName ?? undefined;
|
account.partnerName = overview.partnerName ?? undefined;
|
||||||
account.overviewCheckedAt = now;
|
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) {
|
} catch (err) {
|
||||||
account.healthStatus = "dead";
|
account.healthStatus = "dead";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue