Fix Viaplay account probe and serve agenda from Scripts only.
Show subscription status more reliably; drop FAAB Viaplay from Events and limit our agenda to 3 days ahead.
This commit is contained in:
parent
f92323847a
commit
7cab13c5a8
6 changed files with 203 additions and 38 deletions
|
|
@ -210,15 +210,21 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: accountId ? undefined : JSON.stringify({}),
|
||||
body: JSON.stringify(accountId ? { accountId } : {}),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.error?.message ?? "Statuscheck mislukt");
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (!r.ok) {
|
||||
throw new Error(
|
||||
d?.error?.message || d?.message || `Statuscheck mislukt (${r.status})`
|
||||
);
|
||||
}
|
||||
applySettings(d);
|
||||
const list: ViaplayAccount[] = d.accounts ?? [];
|
||||
const ok = list.filter((a) => a.healthStatus === "ok").length;
|
||||
const dead = list.filter((a) => a.healthStatus === "dead").length;
|
||||
setMsg(`Accountstatus: ${ok} actief · ${dead} dood`);
|
||||
const sample = list.find((a) => a.id === accountId) || list.find((a) => a.subscriptionSummary);
|
||||
const sub = sample?.subscriptionSummary ? ` · ${sample.subscriptionSummary}` : "";
|
||||
setMsg(`Accountstatus: ${ok} actief · ${dead} probleem${sub}`);
|
||||
} catch (e) {
|
||||
setErr(String((e as Error).message ?? e));
|
||||
} finally {
|
||||
|
|
@ -290,10 +296,10 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
return (
|
||||
<span
|
||||
className="err-banner"
|
||||
title={a.healthError || "Cookies ongeldig"}
|
||||
title={a.healthError || "Sessie ongeldig"}
|
||||
style={{ display: "inline", padding: "2px 8px", fontSize: "0.8rem" }}
|
||||
>
|
||||
dood
|
||||
probleem
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -330,7 +336,8 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
Sportagenda en live streams voor gekoppelde Viaplay-accounts. Voeg een account toe met
|
||||
sessie-token, access token en optioneel profile-id (uit een ingelogde browsersessie).
|
||||
Manifests en licenties worden cache-first opgehaald (±5 min vóór start, gestaggerd).
|
||||
Agenda: max 3 dagen vooruit (vervangt Viaplay uit de externe events-feed). Manifests en
|
||||
licenties cache-first (±5 min vóór start).
|
||||
</p>
|
||||
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: "1rem" }}>
|
||||
|
|
@ -378,9 +385,24 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
{a.email}
|
||||
{" · "}
|
||||
{a.hasSession && a.hasAccessToken ? "tokens ok" : "tokens missen"}
|
||||
{a.subscriptionSummary ? ` · ${a.subscriptionSummary}` : ""}
|
||||
{a.eventCount ? ` · ${a.eventCount} events` : ""}
|
||||
</span>
|
||||
{(a.subscriptionSummary || a.partnerName || a.healthError) && (
|
||||
<div style={{ width: "100%", fontSize: "0.85rem", marginTop: 2 }}>
|
||||
{a.subscriptionSummary ? (
|
||||
<span className="ok-banner" style={{ display: "inline", padding: "2px 8px" }}>
|
||||
{a.subscriptionSummary}
|
||||
</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>
|
||||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
||||
<button type="button" disabled={busy} onClick={() => void probeAccounts(a.id)}>
|
||||
|
|
|
|||
|
|
@ -75,38 +75,145 @@ export async function fetchAccountOverview(
|
|||
creds: ViaplayAccountCreds,
|
||||
timeoutMs: number
|
||||
): Promise<ViaplayOverview> {
|
||||
const res = await fetchJson("https://account.mtg-api.com/overview", {
|
||||
headers: authHeaders(creds),
|
||||
timeoutMs,
|
||||
});
|
||||
if (res.status !== 200 || !asRecord(res.json)) {
|
||||
return {
|
||||
ok: false,
|
||||
status: res.status,
|
||||
partnerName: null,
|
||||
productNames: [],
|
||||
summary: "",
|
||||
error:
|
||||
(asRecord(res.json)?.message as string) ||
|
||||
`Overview mislukt (${res.status})`,
|
||||
};
|
||||
const attempts: Array<{ label: string; headers: Record<string, string> }> = [
|
||||
{
|
||||
label: "cookie+bearer",
|
||||
headers: {
|
||||
...authHeaders(creds),
|
||||
Accept: "application/json",
|
||||
...(creds.accessToken
|
||||
? { Authorization: `Bearer ${creds.accessToken}` }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "cookie",
|
||||
headers: { ...authHeaders(creds), Accept: "application/json" },
|
||||
},
|
||||
{
|
||||
label: "bearer",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Origin: "https://viaplay.com",
|
||||
Referer: "https://viaplay.com/",
|
||||
"User-Agent": VIAPLAY_USER_AGENT,
|
||||
...(creds.accessToken
|
||||
? { Authorization: `Bearer ${creds.accessToken}` }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let lastStatus = 0;
|
||||
let lastError = "Overview mislukt";
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
const res = await fetchJson("https://account.mtg-api.com/overview", {
|
||||
headers: attempt.headers,
|
||||
timeoutMs: Math.min(timeoutMs, 15_000),
|
||||
});
|
||||
lastStatus = res.status;
|
||||
const body = asRecord(res.json);
|
||||
if (res.status === 200 && body) {
|
||||
return parseOverviewBody(body);
|
||||
}
|
||||
lastError =
|
||||
(typeof body?.message === "string" && body.message) ||
|
||||
(typeof body?.userMessage === "string" && body.userMessage) ||
|
||||
`Overview HTTP ${res.status}`;
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
const body = asRecord(res.json)!;
|
||||
|
||||
// Fallback: sessie geldig? (light content call)
|
||||
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 root = asRecord(ping.json);
|
||||
const hasUser = Boolean(
|
||||
asRecord(root?._links)?.["viaplay:userData"] ||
|
||||
asRecord(root?._links)?.["viaplay:logout"]
|
||||
);
|
||||
if (ping.status === 200 && hasUser) {
|
||||
const exp = accessTokenExpiry(creds.accessToken);
|
||||
return {
|
||||
ok: true,
|
||||
status: ping.status,
|
||||
partnerName: null,
|
||||
productNames: [],
|
||||
summary: exp
|
||||
? `Sessie actief · token tot ${exp}`
|
||||
: "Sessie actief (abonnementdetails niet beschikbaar)",
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: lastStatus,
|
||||
partnerName: null,
|
||||
productNames: [],
|
||||
summary: "",
|
||||
error: lastError,
|
||||
};
|
||||
}
|
||||
|
||||
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"]) {
|
||||
const v = rec[key];
|
||||
if (typeof v === "string" && v.trim()) return v.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseOverviewBody(body: Record<string, unknown>): ViaplayOverview {
|
||||
const partner = asRecord(body.partnerInformation);
|
||||
const partnerName =
|
||||
typeof partner?.partnerDisplayName === "string"
|
||||
? partner.partnerDisplayName
|
||||
: null;
|
||||
: typeof partner?.partnerId === "string"
|
||||
? partner.partnerId
|
||||
: null;
|
||||
const productNames = Array.isArray(partner?.productNames)
|
||||
? partner!.productNames.map((p) => String(p).trim()).filter(Boolean)
|
||||
: [];
|
||||
const summary = [partnerName, productNames.join(", ")].filter(Boolean).join(" · ");
|
||||
const pkg =
|
||||
packageLabel(body.package) ||
|
||||
packageLabel(body.contract) ||
|
||||
packageLabel(body.rolloverPackage) ||
|
||||
packageLabel(body.promotion);
|
||||
if (pkg && !productNames.includes(pkg)) productNames.push(pkg);
|
||||
|
||||
const parts = [partnerName, productNames.join(", ")].filter(Boolean);
|
||||
return {
|
||||
ok: true,
|
||||
status: res.status,
|
||||
status: 200,
|
||||
partnerName,
|
||||
productNames,
|
||||
summary: summary || "Actief",
|
||||
summary: parts.join(" · ") || "Actief abonnement",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,8 +56,12 @@ export async function listViaplayScheduleEvents(
|
|||
): Promise<ScheduleEventPublic[]> {
|
||||
const config = await loadViaplayConfigOnly();
|
||||
if (!config?.events?.length) return [];
|
||||
const aheadMs = Math.max(1, config.syncDaysAhead || 3) * 86_400_000;
|
||||
const horizon = nowMs + aheadMs;
|
||||
const out: ScheduleEventPublic[] = [];
|
||||
for (const event of config.events) {
|
||||
if (event.endMs < nowMs) continue;
|
||||
if (event.startMs > horizon) continue;
|
||||
const pub = eventToPublic(event, nowMs);
|
||||
if (pub.phase === "ended") continue;
|
||||
out.push(pub);
|
||||
|
|
|
|||
|
|
@ -133,9 +133,10 @@ export async function syncViaplayCatalog(_sessionSecret?: string) {
|
|||
events.push(toSnapshot(product, prev, owners));
|
||||
}
|
||||
|
||||
// Drop ended events older than 6h to keep config small
|
||||
// Drop ended events older than 6h; also drop further than sync horizon+1d
|
||||
const cutoff = now - 6 * 3_600_000;
|
||||
const kept = events.filter((e) => e.endMs >= cutoff);
|
||||
const horizon = now + config.syncDaysAhead * 86_400_000 + 86_400_000;
|
||||
const kept = events.filter((e) => e.endMs >= cutoff && e.startMs <= horizon);
|
||||
kept.sort((a, b) => a.startMs - b.startMs);
|
||||
|
||||
const summary = {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export type ViaplayTweaks = {
|
|||
|
||||
export const VIAPLAY_TWEAK_DEFAULTS: ViaplayTweaks = {
|
||||
deviceKey: "pcdash-nl",
|
||||
syncDaysAhead: 7,
|
||||
syncDaysAhead: 3,
|
||||
httpTimeoutMs: 30_000,
|
||||
preLiveMinutes: 5,
|
||||
preLiveIntervalSeconds: 60,
|
||||
|
|
@ -55,8 +55,8 @@ export const VIAPLAY_TWEAK_META: Array<{
|
|||
},
|
||||
{
|
||||
key: "syncDaysAhead",
|
||||
label: "EPG-dagen vooruit",
|
||||
hint: "Hoeveel dagen sport/all?date=… syncen.",
|
||||
label: "Agenda-dagen vooruit",
|
||||
hint: "Hoeveel dagen sportagenda syncen/tonen (max 3 aanbevolen).",
|
||||
kind: "number",
|
||||
},
|
||||
{
|
||||
|
|
@ -141,7 +141,7 @@ export function mergeViaplayTweaks(
|
|||
};
|
||||
return {
|
||||
deviceKey: s(raw.deviceKey, d.deviceKey),
|
||||
syncDaysAhead: Math.min(21, Math.max(1, n(raw.syncDaysAhead, d.syncDaysAhead))),
|
||||
syncDaysAhead: Math.min(3, Math.max(1, n(raw.syncDaysAhead, d.syncDaysAhead))),
|
||||
httpTimeoutMs: Math.min(120_000, Math.max(5_000, n(raw.httpTimeoutMs, d.httpTimeoutMs))),
|
||||
preLiveMinutes: Math.min(60, Math.max(1, n(raw.preLiveMinutes, d.preLiveMinutes))),
|
||||
preLiveIntervalSeconds: Math.min(
|
||||
|
|
|
|||
|
|
@ -802,17 +802,39 @@ export async function discoverScheduleEventProviders(config: Config): Promise<st
|
|||
const raw = await loadRawEvents(config);
|
||||
for (const e of raw.events) {
|
||||
const p = (str(e.provider) || "unknown").toLowerCase();
|
||||
// Viaplay komt uit Scripts, niet uit FAAB-feed
|
||||
if (p === "viaplay") continue;
|
||||
if (seen.has(p)) continue;
|
||||
seen.add(p);
|
||||
out.push(p);
|
||||
}
|
||||
} catch {
|
||||
/* feed optioneel als F1TV aan staat */
|
||||
/* feed optioneel als F1TV/Viaplay aan staat */
|
||||
}
|
||||
try {
|
||||
const { isF1tvIntegrationEnabled } = await import("../f1tv/settings");
|
||||
if (await isF1tvIntegrationEnabled()) {
|
||||
if (!seen.has("f1tv")) out.push("f1tv");
|
||||
if (!seen.has("f1tv")) {
|
||||
seen.add("f1tv");
|
||||
out.push("f1tv");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
const row = await prisma.integrationSetting.findUnique({ where: { id: "viaplay" } });
|
||||
if (row?.enabled !== false) {
|
||||
const cfg = row?.configJson ? JSON.parse(row.configJson) : null;
|
||||
const accounts = Array.isArray(cfg?.accounts) ? cfg.accounts : [];
|
||||
const hasAccount = accounts.some(
|
||||
(a: { enabled?: boolean; sessionEnc?: string; accessTokenEnc?: string }) =>
|
||||
a && a.enabled !== false && a.sessionEnc && a.accessTokenEnc
|
||||
);
|
||||
if (hasAccount && !seen.has("viaplay")) {
|
||||
seen.add("viaplay");
|
||||
out.push("viaplay");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
|
|
@ -839,9 +861,11 @@ export async function listScheduleEvents(
|
|||
mapped = raw.events
|
||||
.map((e) => toPublic(e, nowMs))
|
||||
.filter((e): e is ScheduleEventPublic => e != null)
|
||||
.filter((e) => e.phase !== "ended");
|
||||
.filter((e) => e.phase !== "ended")
|
||||
// Viaplay komt uit Scripts → Viaplay, niet uit de externe FAAB-feed
|
||||
.filter((e) => e.provider.toLowerCase() !== "viaplay");
|
||||
} catch {
|
||||
/* FAAB-feed optioneel wanneer F1TV events levert */
|
||||
/* FAAB-feed optioneel wanneer F1TV/Viaplay events leveren */
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -937,6 +961,9 @@ export async function getScheduleEvent(
|
|||
if (!found) throw new AppError("NOT_FOUND", "Event niet gevonden", 404);
|
||||
const pub = toPublic(found, nowMs);
|
||||
if (!pub) throw new AppError("NOT_FOUND", "Event niet gevonden", 404);
|
||||
if (pub.provider.toLowerCase() === "viaplay") {
|
||||
throw new AppError("NOT_FOUND", "Event niet gevonden", 404);
|
||||
}
|
||||
if (!providerAllowed(pub.provider, disabledGlobal, access?.allowedProviders ?? null)) {
|
||||
throw new AppError("NOT_FOUND", "Event niet gevonden", 404);
|
||||
}
|
||||
|
|
@ -1049,6 +1076,10 @@ export async function getScheduleEventPlay(
|
|||
const pub = toPublic(found, nowMs);
|
||||
if (!pub) throw new AppError("NOT_FOUND", "Event niet gevonden", 404);
|
||||
|
||||
if (pub.provider.toLowerCase() === "viaplay") {
|
||||
throw new AppError("NOT_FOUND", "Event niet gevonden", 404);
|
||||
}
|
||||
|
||||
if (!providerAllowed(pub.provider, disabledGlobal, access?.allowedProviders ?? null)) {
|
||||
throw new AppError("NOT_FOUND", "Event niet gevonden", 404);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue