Show Viaplay subscription type on each account after add/check.

Auto-probe on account add and always render an Abonnement row, with session-cookie fallback when overview is unavailable.
This commit is contained in:
Jos Vooges | STH 2026-09-22 22:37:40 +02:00
parent 7cab13c5a8
commit 8be634f783
3 changed files with 139 additions and 31 deletions

View file

@ -143,13 +143,22 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
}); });
const d = await r.json(); const d = await r.json();
if (!r.ok) throw new Error(d.error?.message ?? "Account toevoegen mislukt"); if (!r.ok) throw new Error(d.error?.message ?? "Account toevoegen mislukt");
const emailKey = newEmail.trim().toLowerCase();
applySettings(d); applySettings(d);
setNewLabel(""); setNewLabel("");
setNewEmail(""); setNewEmail("");
setNewSession(""); setNewSession("");
setNewAccessToken(""); setNewAccessToken("");
setNewProfileId(""); setNewProfileId("");
setMsg("Viaplay-account toegevoegd"); const added = (d.accounts as ViaplayAccount[] | undefined)?.find(
(a) => a.email === emailKey
);
const sub = added?.subscriptionSummary;
setMsg(
sub
? `Account toegevoegd · abonnement: ${sub}`
: "Account toegevoegd — abonnement nog niet zichtbaar, druk Check"
);
} catch (e) { } catch (e) {
setErr(String((e as Error).message ?? e)); setErr(String((e as Error).message ?? e));
} finally { } finally {
@ -378,31 +387,43 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
borderBottom: "1px solid var(--border, #333)", borderBottom: "1px solid var(--border, #333)",
}} }}
> >
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}> <div style={{ flex: "1 1 280px", minWidth: 0 }}>
<strong>{a.label || a.email}</strong> <div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
{healthBadge(a)} <strong>{a.label || a.email}</strong>
<span className="muted" style={{ fontSize: "0.85rem" }}> {healthBadge(a)}
{a.email} <span className="muted" style={{ fontSize: "0.85rem" }}>
{" · "} {a.email}
{a.hasSession && a.hasAccessToken ? "tokens ok" : "tokens missen"} {a.eventCount ? ` · ${a.eventCount} events` : ""}
{a.eventCount ? ` · ${a.eventCount} events` : ""} </span>
</span> </div>
{(a.subscriptionSummary || a.partnerName || a.healthError) && ( <div
<div style={{ width: "100%", fontSize: "0.85rem", marginTop: 2 }}> style={{
marginTop: 8,
padding: "8px 10px",
borderRadius: 6,
background: "var(--surface-2, rgba(127,127,127,0.12))",
fontSize: "0.9rem",
}}
>
<div>
<span className="muted">Abonnement: </span>
{a.subscriptionSummary ? ( {a.subscriptionSummary ? (
<span className="ok-banner" style={{ display: "inline", padding: "2px 8px" }}> <strong>{a.subscriptionSummary}</strong>
{a.subscriptionSummary} ) : (
</span> <span className="muted">nog niet opgehaald — druk Check</span>
) : a.partnerName ? ( )}
<span className="muted">{a.partnerName}</span>
) : null}
{a.healthError ? (
<span className="muted" style={{ marginLeft: 8 }} title={a.healthError}>
{a.healthError}
</span>
) : null}
</div> </div>
)} {a.partnerName && a.subscriptionSummary && !a.subscriptionSummary.includes(a.partnerName) ? (
<div className="muted" style={{ fontSize: "0.8rem", marginTop: 2 }}>
Partner: {a.partnerName}
</div>
) : null}
{a.healthError ? (
<div className="muted" style={{ fontSize: "0.8rem", marginTop: 4 }}>
{a.healthError}
</div>
) : null}
</div>
</div> </div>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}> <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
<button type="button" disabled={busy} onClick={() => void probeAccounts(a.id)}> <button type="button" disabled={busy} onClick={() => void probeAccounts(a.id)}>

View file

@ -115,7 +115,22 @@ export async function fetchAccountOverview(
lastStatus = res.status; lastStatus = res.status;
const body = asRecord(res.json); const body = asRecord(res.json);
if (res.status === 200 && body) { if (res.status === 200 && body) {
return parseOverviewBody(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;
} }
lastError = lastError =
(typeof body?.message === "string" && body.message) || (typeof body?.message === "string" && body.message) ||
@ -141,21 +156,37 @@ export async function fetchAccountOverview(
asRecord(root?._links)?.["viaplay:logout"] asRecord(root?._links)?.["viaplay:logout"]
); );
if (ping.status === 200 && hasUser) { if (ping.status === 200 && hasUser) {
const fromSession = subscriptionFromSession(creds.session);
const exp = accessTokenExpiry(creds.accessToken); const exp = accessTokenExpiry(creds.accessToken);
const summary =
fromSession.summary ||
(exp
? `Sessie actief · token tot ${exp}`
: "Sessie actief (abonnementdetails niet beschikbaar)");
return { return {
ok: true, ok: true,
status: ping.status, status: ping.status,
partnerName: null, partnerName: fromSession.partnerName,
productNames: [], productNames: fromSession.productNames,
summary: exp summary,
? `Sessie actief · token tot ${exp}`
: "Sessie actief (abonnementdetails niet beschikbaar)",
}; };
} }
} 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,
@ -166,6 +197,52 @@ export async function fetchAccountOverview(
}; };
} }
/** 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 { function accessTokenExpiry(token: string | null): string | null {
if (!token) return null; if (!token) return null;
try { try {

View file

@ -216,7 +216,8 @@ export async function addViaplayAccount(
} }
config.accounts.push(account); config.accounts.push(account);
await persist(config, existing?.enabled ?? true); await persist(config, existing?.enabled ?? true);
return getViaplaySettingsPublic(); // Direct abonnement ophalen zodat UI het meteen toont
return probeViaplayAccounts(sessionSecret, account.id);
} }
export async function updateViaplayAccount( export async function updateViaplayAccount(
@ -286,6 +287,15 @@ export async function updateViaplayAccount(
: current.passwordEnc, : current.passwordEnc,
}; };
await persist(config, existing?.enabled ?? true); await persist(config, existing?.enabled ?? true);
const tokensChanged = !!(
input.session?.trim() ||
input.accessToken?.trim() ||
input.profileId !== undefined ||
input.clearCookies
);
if (tokensChanged) {
return probeViaplayAccounts(sessionSecret, accountId);
}
return getViaplaySettingsPublic(); return getViaplaySettingsPublic();
} }