Detect Viaplay Don't go via play-log and send the full cookie jar including DataDome.
This commit is contained in:
parent
ebb986f6c1
commit
8bf3b43444
13 changed files with 509 additions and 52 deletions
|
|
@ -20,6 +20,10 @@ type ViaplayAccount = {
|
|||
hasAccessToken: boolean;
|
||||
hasProfileId: boolean;
|
||||
hasPassword: boolean;
|
||||
hasCookieJar?: boolean;
|
||||
hasDatadome?: boolean;
|
||||
hasBoid?: boolean;
|
||||
cookieNames?: string[];
|
||||
enabled: boolean;
|
||||
eventCount: number;
|
||||
healthStatus: "ok" | "dead" | "unknown";
|
||||
|
|
@ -30,6 +34,30 @@ type ViaplayAccount = {
|
|||
overviewCheckedAt: string | null;
|
||||
};
|
||||
|
||||
type PlayLogEntry = {
|
||||
at: string;
|
||||
kind: "live" | "vod";
|
||||
guid: string;
|
||||
ok: boolean;
|
||||
title: string | null;
|
||||
accountLabel: string | null;
|
||||
mediaPath: string | null;
|
||||
hasLicense: boolean;
|
||||
dontGo: boolean;
|
||||
dontGoReason: string | null;
|
||||
error: string | null;
|
||||
cookies: {
|
||||
names: string[];
|
||||
count: number;
|
||||
sessionStoredDecoded: boolean;
|
||||
sessionStoredEncoded: boolean;
|
||||
hasDatadome: boolean;
|
||||
hasBoid: boolean;
|
||||
hasClientId: boolean;
|
||||
hasPersistentLogin: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type ViaplaySettings = {
|
||||
configured: boolean;
|
||||
enabled: boolean;
|
||||
|
|
@ -62,6 +90,7 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
const [tweaks, setTweaks] = useState<ViaplayTweaks>({});
|
||||
const [showTweaks, setShowTweaks] = useState(false);
|
||||
const [showCookies, setShowCookies] = useState(false);
|
||||
const [playLog, setPlayLog] = useState<PlayLogEntry[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
|
@ -106,6 +135,21 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
void load().catch((e) => setErr(String(e.message ?? e)));
|
||||
}, [load]);
|
||||
|
||||
const loadPlayLog = useCallback(async () => {
|
||||
const r = await fetch("/api/v1/admin/scripts/viaplay/play-log", {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!r.ok) return;
|
||||
const d = (await r.json()) as { log?: PlayLogEntry[] };
|
||||
setPlayLog(Array.isArray(d.log) ? d.log : []);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPlayLog();
|
||||
const t = setInterval(() => void loadPlayLog(), 8000);
|
||||
return () => clearInterval(t);
|
||||
}, [loadPlayLog]);
|
||||
|
||||
async function saveSettings(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
|
|
@ -411,10 +455,11 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
<div className="card" style={{ maxWidth: 760 }}>
|
||||
<h2 style={{ marginTop: 0 }}>Viaplay</h2>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
Sportagenda en live streams voor gekoppelde Viaplay-accounts. Zet e-mail + wachtwoord
|
||||
(Token Tray logt stil in via de browser-UI). Tokens plakken blijft optioneel.
|
||||
Agenda: max 3 dagen vooruit (vervangt Viaplay uit de externe events-feed). Manifests en
|
||||
licenties cache-first (±5 min vóór start).
|
||||
Sportagenda en live streams voor gekoppelde Viaplay-accounts. Token Tray moet
|
||||
de <strong>hele cookie-jar</strong> meesturen (session + datadome + Boid), zoals
|
||||
het werkende livescript — Viaplay/DataDome geeft anders Don't go i.p.v. 403.
|
||||
Playwright-login wordt door DataDome als bot gezien; tokens uit echt Chrome
|
||||
(Netscape-export / tray met Chrome-kanaal) werken beter.
|
||||
</p>
|
||||
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: "1rem" }}>
|
||||
|
|
@ -437,6 +482,50 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<h3 style={{ marginBottom: "0.5rem" }}>Play-diagnose</h3>
|
||||
<p className="muted" style={{ marginTop: 0, fontSize: "0.85rem" }}>
|
||||
Geen cookie-waarden, wel namen + of Viaplay Don't go teruggaf. Speel iets in de
|
||||
app en kijk hier binnen een paar seconden.
|
||||
</p>
|
||||
{playLog.length === 0 ? (
|
||||
<p className="muted">Nog geen plays sinds de laatste API-restart.</p>
|
||||
) : (
|
||||
<ul style={{ listStyle: "none", padding: 0, margin: "0 0 1.25rem" }}>
|
||||
{playLog.slice(0, 12).map((e, i) => (
|
||||
<li
|
||||
key={`${e.at}-${e.guid}-${i}`}
|
||||
style={{
|
||||
padding: "0.55rem 0",
|
||||
borderBottom: "1px solid var(--border, #333)",
|
||||
fontSize: "0.88rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
|
||||
<strong style={{ color: e.ok && !e.dontGo ? "var(--ok, #6c6)" : "var(--danger, #f66)" }}>
|
||||
{e.ok && !e.dontGo ? "OK" : e.dontGo ? "DON'T GO" : "FAIL"}
|
||||
</strong>
|
||||
<span className="muted">{e.kind}</span>
|
||||
<span>{e.title || e.guid}</span>
|
||||
</div>
|
||||
<div className="muted" style={{ marginTop: 4 }}>
|
||||
{new Date(e.at).toLocaleString("nl-NL")}
|
||||
{e.accountLabel ? ` · ${e.accountLabel}` : ""}
|
||||
{e.mediaPath ? ` · ${e.mediaPath}` : ""}
|
||||
{e.hasLicense ? " · widevine" : " · geen licentie"}
|
||||
{` · cookies ${e.cookies.count}`}
|
||||
{e.cookies.hasDatadome ? " · datadome" : " · géén datadome"}
|
||||
{e.cookies.hasBoid ? " · boid" : " · géén boid"}
|
||||
{e.cookies.sessionStoredDecoded ? " · session decoded" : ""}
|
||||
{e.cookies.sessionStoredEncoded ? " · session encoded" : ""}
|
||||
</div>
|
||||
{(e.dontGoReason || e.error) && (
|
||||
<div style={{ marginTop: 4 }}>{e.dontGoReason || e.error}</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<h3 style={{ marginBottom: "0.5rem" }}>Accounts</h3>
|
||||
{accounts.length === 0 ? (
|
||||
<p className="muted">Nog geen accounts — voeg e-mail + sessie-tokens toe.</p>
|
||||
|
|
@ -467,6 +556,9 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
|||
{a.email}
|
||||
{a.hasPassword ? " · wachtwoord" : ""}
|
||||
{a.hasSession && a.hasAccessToken ? " · tokens" : ""}
|
||||
{a.hasCookieJar ? " · cookie-jar" : " · geen cookie-jar"}
|
||||
{a.hasDatadome ? " · datadome" : " · geen datadome"}
|
||||
{a.hasBoid ? " · boid" : " · geen boid"}
|
||||
{a.eventCount ? ` · ${a.eventCount} events` : ""}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1928,19 +1928,27 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
streamUrl: played.streamUrl,
|
||||
keyCount: played.keys.length,
|
||||
promo,
|
||||
resolver: "viaplay-byguid-v9",
|
||||
resolver: "viaplay-byguid-v10",
|
||||
lastPlay: (await import("../viaplay/play")).getLastVodPlay(),
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
guid,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
resolver: "viaplay-byguid-v9",
|
||||
resolver: "viaplay-byguid-v10",
|
||||
lastPlay: (await import("../viaplay/play")).getLastVodPlay(),
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/** Diagnose: laatste live/VOD plays (geen secrets, wel cookie-namen + Don't go-reden). */
|
||||
app.get("/api/v1/admin/scripts/viaplay/play-log", { preHandler: requireAdmin }, async () => {
|
||||
const { getViaplayPlayLog, getLastVodPlay } = await import("../viaplay/play");
|
||||
return { last: getLastVodPlay(), log: getViaplayPlayLog() };
|
||||
});
|
||||
|
||||
app.post("/api/v1/admin/scripts/viaplay/sync", { preHandler: requireAdmin }, async () => {
|
||||
const { syncViaplayCatalog } = await import("../viaplay/sync");
|
||||
return syncViaplayCatalog(config.SESSION_SECRET);
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
|||
profileId?: string;
|
||||
persistentLogin?: string;
|
||||
boid?: string;
|
||||
cookieJar?: Record<string, string>;
|
||||
};
|
||||
const { upsertViaplayAccountByEmail } = await import("../viaplay/settings");
|
||||
return upsertViaplayAccountByEmail(config.SESSION_SECRET, {
|
||||
|
|
@ -174,6 +175,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
|||
profileId: body.profileId,
|
||||
persistentLogin: body.persistentLogin,
|
||||
boid: body.boid,
|
||||
cookieJar: body.cookieJar,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { decryptSecret, encryptSecret } from "../security/crypto";
|
||||
import { jwtExpiryIso } from "../agent/token-expiry";
|
||||
import { pickPlayCookies } from "./cookies";
|
||||
|
||||
export type ViaplayAccountHealth = "ok" | "dead" | "unknown";
|
||||
|
||||
|
|
@ -17,6 +18,10 @@ export type ViaplayAccountStored = {
|
|||
/** Optioneel — voor latere tray/login. */
|
||||
passwordEnc?: string;
|
||||
persistentLoginEnc?: string;
|
||||
/** Encrypted JSON van de play-cookie-jar (datadome, boid, clientId, …). */
|
||||
cookieJarEnc?: string;
|
||||
/** Cookie-namen in de jar — geen waarden, voor admin-diagnose. */
|
||||
cookieNames?: string[];
|
||||
enabled: boolean;
|
||||
healthStatus?: ViaplayAccountHealth;
|
||||
healthCheckedAt?: string;
|
||||
|
|
@ -36,6 +41,10 @@ export type ViaplayAccountPublic = {
|
|||
hasAccessToken: boolean;
|
||||
hasProfileId: boolean;
|
||||
hasPassword: boolean;
|
||||
hasCookieJar: boolean;
|
||||
hasDatadome: boolean;
|
||||
hasBoid: boolean;
|
||||
cookieNames: string[];
|
||||
enabled: boolean;
|
||||
eventCount: number;
|
||||
healthStatus: ViaplayAccountHealth;
|
||||
|
|
@ -58,6 +67,7 @@ export type ViaplayAccountCreds = {
|
|||
deviceId: string;
|
||||
password: string | null;
|
||||
persistentLogin: string | null;
|
||||
cookieJar: Record<string, string> | null;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -82,6 +92,12 @@ export function accountToPublic(
|
|||
hasAccessToken: !!account.accessTokenEnc,
|
||||
hasProfileId: !!account.profileId?.trim(),
|
||||
hasPassword: !!account.passwordEnc,
|
||||
hasCookieJar: !!account.cookieJarEnc,
|
||||
hasDatadome: (account.cookieNames || []).includes("datadome"),
|
||||
hasBoid:
|
||||
(account.cookieNames || []).includes("__Secure-Viaplay-Boid") ||
|
||||
!!account.boid?.trim(),
|
||||
cookieNames: account.cookieNames || [],
|
||||
enabled: account.enabled !== false,
|
||||
eventCount,
|
||||
healthStatus: account.healthStatus ?? "unknown",
|
||||
|
|
@ -117,10 +133,29 @@ export function decryptAccount(
|
|||
persistentLogin: account.persistentLoginEnc
|
||||
? decryptSecret(account.persistentLoginEnc, sessionSecret)
|
||||
: null,
|
||||
cookieJar: parseCookieJar(account.cookieJarEnc, sessionSecret),
|
||||
enabled: account.enabled !== false,
|
||||
};
|
||||
}
|
||||
|
||||
function parseCookieJar(
|
||||
enc: string | undefined,
|
||||
sessionSecret: string
|
||||
): Record<string, string> | null {
|
||||
if (!enc) return null;
|
||||
try {
|
||||
const raw = JSON.parse(decryptSecret(enc, sessionSecret)) as unknown;
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const out: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
||||
if (typeof v === "string" && v.trim()) out[k] = v.trim();
|
||||
}
|
||||
return Object.keys(out).length ? out : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createAccountStored(
|
||||
sessionSecret: string,
|
||||
input: {
|
||||
|
|
@ -135,6 +170,7 @@ export function createAccountStored(
|
|||
deviceId?: string;
|
||||
password?: string;
|
||||
persistentLogin?: string;
|
||||
cookieJar?: Record<string, string>;
|
||||
}
|
||||
): ViaplayAccountStored {
|
||||
const email = input.email.trim().toLowerCase();
|
||||
|
|
@ -163,6 +199,13 @@ export function createAccountStored(
|
|||
persistentLoginEnc: input.persistentLogin?.trim()
|
||||
? encryptSecret(input.persistentLogin.trim(), sessionSecret)
|
||||
: undefined,
|
||||
cookieJarEnc: (() => {
|
||||
const jar = pickPlayCookies(input.cookieJar);
|
||||
return Object.keys(jar).length
|
||||
? encryptSecret(JSON.stringify(jar), sessionSecret)
|
||||
: undefined;
|
||||
})(),
|
||||
cookieNames: Object.keys(pickPlayCookies(input.cookieJar)).sort(),
|
||||
enabled: input.enabled !== false,
|
||||
tokenExpiresAt: accessToken ? jwtExpiryIso(accessToken) ?? undefined : undefined,
|
||||
};
|
||||
|
|
@ -188,6 +231,10 @@ export function parseAccountsJson(raw: unknown): ViaplayAccountStored[] {
|
|||
deviceId: String(row.deviceId ?? "").trim() || undefined,
|
||||
passwordEnc: String(row.passwordEnc ?? "").trim() || undefined,
|
||||
persistentLoginEnc: String(row.persistentLoginEnc ?? "").trim() || undefined,
|
||||
cookieJarEnc: String(row.cookieJarEnc ?? "").trim() || undefined,
|
||||
cookieNames: Array.isArray(row.cookieNames)
|
||||
? row.cookieNames.map((n) => String(n).trim()).filter(Boolean)
|
||||
: undefined,
|
||||
enabled: row.enabled !== false,
|
||||
healthStatus:
|
||||
row.healthStatus === "ok" ||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import type { ViaplayAccountCreds } from "./accounts";
|
||||
import { toMediaGuid } from "./accounts";
|
||||
import { inventoryFromNames, pickPlayCookies, type CookieInventory } from "./cookies";
|
||||
|
||||
/** Zelfde UA als het werkende livescript (viaplay3.py). Edge/153 + sec-ch-ua gaf Don't go. */
|
||||
export const VIAPLAY_USER_AGENT =
|
||||
|
|
@ -55,17 +56,24 @@ function parseSetCookieJar(setCookies: string[]): Record<string, string> {
|
|||
}
|
||||
|
||||
function cookieHeader(creds: ViaplayAccountCreds): string {
|
||||
const parts: string[] = [];
|
||||
if (creds.session) parts.push(`session=${cookieValue(creds.session)}`);
|
||||
if (creds.accessToken) parts.push(`accessToken=${creds.accessToken.trim()}`);
|
||||
if (creds.profileId) parts.push(`viaplay_profileId=${creds.profileId.trim()}`);
|
||||
if (creds.boid) {
|
||||
parts.push(`__Secure-Viaplay-Boid=${creds.boid.trim()}`);
|
||||
}
|
||||
if (creds.persistentLogin) {
|
||||
parts.push(`persistentLogin=${cookieValue(creds.persistentLogin)}`);
|
||||
}
|
||||
return parts.join("; ");
|
||||
const merged: Record<string, string> = { ...(creds.cookieJar || {}) };
|
||||
if (creds.session) merged.session = creds.session;
|
||||
if (creds.accessToken) merged.accessToken = creds.accessToken.trim();
|
||||
if (creds.profileId) merged.viaplay_profileId = creds.profileId.trim();
|
||||
if (creds.boid) merged["__Secure-Viaplay-Boid"] = creds.boid.trim();
|
||||
if (creds.persistentLogin) merged.persistentLogin = creds.persistentLogin;
|
||||
const picked = pickPlayCookies(merged);
|
||||
return Object.entries(picked)
|
||||
.map(([k, v]) => `${k}=${cookieValue(v)}`)
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
export function cookieInventory(creds: ViaplayAccountCreds): CookieInventory {
|
||||
const names = cookieHeader(creds)
|
||||
.split(/;\s*/)
|
||||
.map((p) => p.split("=")[0] || "")
|
||||
.filter(Boolean);
|
||||
return inventoryFromNames(names, creds.session);
|
||||
}
|
||||
|
||||
export function authHeaders(creds: ViaplayAccountCreds): Record<string, string> {
|
||||
|
|
@ -155,6 +163,19 @@ export async function refreshViaplaySession(
|
|||
const session = sessionRaw;
|
||||
const accessToken = normalizeAccessToken(accessRaw) || accessRaw.trim();
|
||||
const nextPl = jar.persistentLogin || pl;
|
||||
const nextJar = pickPlayCookies({
|
||||
...(creds.cookieJar || {}),
|
||||
session,
|
||||
accessToken,
|
||||
persistentLogin: nextPl,
|
||||
...(jar.datadome ? { datadome: jar.datadome } : {}),
|
||||
...(jar["__Secure-Viaplay-Boid"]
|
||||
? { "__Secure-Viaplay-Boid": jar["__Secure-Viaplay-Boid"] }
|
||||
: {}),
|
||||
...(jar["Viaplay-ClientId"]
|
||||
? { "Viaplay-ClientId": jar["Viaplay-ClientId"] }
|
||||
: {}),
|
||||
});
|
||||
|
||||
return {
|
||||
...creds,
|
||||
|
|
@ -162,6 +183,7 @@ export async function refreshViaplaySession(
|
|||
accessToken,
|
||||
persistentLogin: nextPl,
|
||||
boid: jar["__Secure-Viaplay-Boid"] || creds.boid,
|
||||
cookieJar: Object.keys(nextJar).length ? nextJar : creds.cookieJar,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
84
apps/master-api/src/viaplay/cookies.ts
Normal file
84
apps/master-api/src/viaplay/cookies.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/** Cookies die viaplay3.py / Netscape-jar meestuurt en DataDome nodig heeft. */
|
||||
export const VIAPLAY_PLAY_COOKIE_KEEP = new Set([
|
||||
"session",
|
||||
"accessToken",
|
||||
"persistentLogin",
|
||||
"hasPersistentLogin",
|
||||
"isPersistentLogin",
|
||||
"viaplay_profileId",
|
||||
"__Secure-Viaplay-Boid",
|
||||
"__Secure-Viaplay-Boid-exists",
|
||||
"datadome",
|
||||
"Viaplay-ClientId",
|
||||
"splitKey",
|
||||
"countryCode",
|
||||
"eupubconsent-v2",
|
||||
"OptanonConsent",
|
||||
"cookie_agreement",
|
||||
]);
|
||||
|
||||
export function pickPlayCookies(
|
||||
raw: Record<string, string> | null | undefined
|
||||
): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (!raw) return out;
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
const name = String(key || "").trim();
|
||||
const v = String(value || "").trim();
|
||||
if (!name || !v) continue;
|
||||
if (
|
||||
VIAPLAY_PLAY_COOKIE_KEEP.has(name) ||
|
||||
/^__Secure-Viaplay/i.test(name) ||
|
||||
/^datadome$/i.test(name)
|
||||
) {
|
||||
out[name] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export type CookieInventory = {
|
||||
names: string[];
|
||||
count: number;
|
||||
sessionStoredDecoded: boolean;
|
||||
sessionStoredEncoded: boolean;
|
||||
hasDatadome: boolean;
|
||||
hasBoid: boolean;
|
||||
hasClientId: boolean;
|
||||
hasPersistentLogin: boolean;
|
||||
hasAccessToken: boolean;
|
||||
hasProfileId: boolean;
|
||||
hasSession: boolean;
|
||||
};
|
||||
|
||||
export function inspectStoredSession(session: string | null | undefined): {
|
||||
storedDecoded: boolean;
|
||||
storedEncoded: boolean;
|
||||
} {
|
||||
const s = String(session || "").trim();
|
||||
if (!s) return { storedDecoded: false, storedEncoded: false };
|
||||
const storedDecoded = /s:j:\{/.test(s) || /[{}"\s]/.test(s);
|
||||
const storedEncoded = /%[0-9A-Fa-f]{2}/.test(s) && !/[{}]/.test(s);
|
||||
return { storedDecoded, storedEncoded };
|
||||
}
|
||||
|
||||
export function inventoryFromNames(
|
||||
names: string[],
|
||||
session?: string | null
|
||||
): CookieInventory {
|
||||
const set = new Set(names);
|
||||
const sess = inspectStoredSession(session);
|
||||
return {
|
||||
names: [...set].sort(),
|
||||
count: set.size,
|
||||
sessionStoredDecoded: sess.storedDecoded,
|
||||
sessionStoredEncoded: sess.storedEncoded,
|
||||
hasDatadome: set.has("datadome"),
|
||||
hasBoid: set.has("__Secure-Viaplay-Boid"),
|
||||
hasClientId: set.has("Viaplay-ClientId"),
|
||||
hasPersistentLogin: set.has("persistentLogin"),
|
||||
hasAccessToken: set.has("accessToken"),
|
||||
hasProfileId: set.has("viaplay_profileId"),
|
||||
hasSession: set.has("session"),
|
||||
};
|
||||
}
|
||||
60
apps/master-api/src/viaplay/diag.ts
Normal file
60
apps/master-api/src/viaplay/diag.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import type { CookieInventory } from "./cookies";
|
||||
|
||||
export type ViaplayPlayDiag = {
|
||||
at: string;
|
||||
kind: "live" | "vod";
|
||||
guid: string;
|
||||
ok: boolean;
|
||||
title: string | null;
|
||||
accountId: string | null;
|
||||
accountLabel: string | null;
|
||||
httpStatus: number | null;
|
||||
playHost: string | null;
|
||||
mediaPath: string | null;
|
||||
hasLicense: boolean;
|
||||
dontGo: boolean;
|
||||
dontGoReason: string | null;
|
||||
productDurationMs: number | null;
|
||||
mpdDurationMs: number | null;
|
||||
cookies: CookieInventory;
|
||||
refreshOk: boolean | null;
|
||||
error: string | null;
|
||||
resolver: string;
|
||||
triedAccounts?: number;
|
||||
triedLabels?: string[];
|
||||
};
|
||||
|
||||
const MAX = 25;
|
||||
const log: ViaplayPlayDiag[] = [];
|
||||
|
||||
export function recordViaplayPlay(entry: Omit<ViaplayPlayDiag, "at">): ViaplayPlayDiag {
|
||||
const full: ViaplayPlayDiag = { ...entry, at: new Date().toISOString() };
|
||||
log.unshift(full);
|
||||
if (log.length > MAX) log.length = MAX;
|
||||
return full;
|
||||
}
|
||||
|
||||
export function getViaplayPlayLog(): ViaplayPlayDiag[] {
|
||||
return log.slice();
|
||||
}
|
||||
|
||||
export function getLastViaplayPlay(): ViaplayPlayDiag | null {
|
||||
return log[0] ?? null;
|
||||
}
|
||||
|
||||
export function mediaPathOf(url: string | null | undefined): string | null {
|
||||
if (!url) return null;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const parts = u.pathname.split("/").filter(Boolean);
|
||||
return parts.slice(-3).join("/") || u.pathname;
|
||||
} catch {
|
||||
return url.slice(0, 120);
|
||||
}
|
||||
}
|
||||
|
||||
export function isDontGoError(message: string | null | undefined): boolean {
|
||||
return /don'?t\s*go|placeholder|promo-stream|geen live-dash/i.test(
|
||||
String(message || "")
|
||||
);
|
||||
}
|
||||
|
|
@ -14,11 +14,21 @@ import {
|
|||
fetchMpdXml,
|
||||
extractPsshFromMpd,
|
||||
fetchViaplayBootstrapPssh,
|
||||
cookieInventory,
|
||||
isDontGoPlaceholder,
|
||||
isLiveDashMpd,
|
||||
mpdLooksLikeDontGo,
|
||||
VIAPLAY_USER_AGENT,
|
||||
} from "./client";
|
||||
import { inventoryFromNames, type CookieInventory } from "./cookies";
|
||||
import {
|
||||
isDontGoError,
|
||||
mediaPathOf,
|
||||
recordViaplayPlay,
|
||||
getLastViaplayPlay,
|
||||
getViaplayPlayLog,
|
||||
type ViaplayPlayDiag,
|
||||
} from "./diag";
|
||||
import {
|
||||
loadViaplaySettings,
|
||||
patchViaplayEventCache,
|
||||
|
|
@ -27,44 +37,70 @@ import { findViaplayEvent } from "./sync";
|
|||
import { isMpdCacheFresh, type ViaplayEventSnapshot } from "./tweaks";
|
||||
import { VIAPLAY_CATCHUP_MS } from "./events";
|
||||
|
||||
export type LastVodPlayDiag = {
|
||||
at: string;
|
||||
guid: string;
|
||||
ok: boolean;
|
||||
title: string | null;
|
||||
accountId: string | null;
|
||||
mediaPath: string | null;
|
||||
error: string | null;
|
||||
resolver: string;
|
||||
/** Hoeveel accounts deze play heeft geprobeerd (eigen cookies per account). */
|
||||
triedAccounts?: number;
|
||||
/** Korte labels van geprobeerde accounts (geen secrets). */
|
||||
triedLabels?: string[];
|
||||
};
|
||||
export type LastVodPlayDiag = ViaplayPlayDiag;
|
||||
|
||||
let lastVodPlay: LastVodPlayDiag | null = null;
|
||||
|
||||
export function getLastVodPlay(): LastVodPlayDiag | null {
|
||||
return lastVodPlay;
|
||||
export function getLastVodPlay() {
|
||||
return getLastViaplayPlay();
|
||||
}
|
||||
|
||||
function recordVodPlay( partial: Omit<LastVodPlayDiag, "at" | "resolver"> ) {
|
||||
lastVodPlay = {
|
||||
export { getViaplayPlayLog };
|
||||
|
||||
function emptyCookies(): CookieInventory {
|
||||
return inventoryFromNames([], null);
|
||||
}
|
||||
|
||||
function recordVodPlay(
|
||||
partial: Partial<ViaplayPlayDiag> & { guid: string; ok: boolean }
|
||||
) {
|
||||
const error = partial.error ?? null;
|
||||
recordViaplayPlay({
|
||||
kind: "vod",
|
||||
title: null,
|
||||
accountId: null,
|
||||
accountLabel: null,
|
||||
httpStatus: null,
|
||||
playHost: "play.viaplay.com",
|
||||
mediaPath: null,
|
||||
hasLicense: false,
|
||||
dontGo: isDontGoError(error) || !!partial.dontGo,
|
||||
dontGoReason: partial.dontGoReason || (isDontGoError(error) ? error : null),
|
||||
productDurationMs: null,
|
||||
mpdDurationMs: null,
|
||||
cookies: emptyCookies(),
|
||||
refreshOk: null,
|
||||
error,
|
||||
resolver: "viaplay-byguid-v10",
|
||||
...partial,
|
||||
at: new Date().toISOString(),
|
||||
resolver: "viaplay-byguid-v9",
|
||||
};
|
||||
kind: "vod",
|
||||
resolver: "viaplay-byguid-v10",
|
||||
});
|
||||
}
|
||||
|
||||
function mediaPathOf(url: string | null | undefined): string | null {
|
||||
if (!url) return null;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const parts = u.pathname.split("/").filter(Boolean);
|
||||
return parts.slice(-3).join("/") || u.pathname;
|
||||
} catch {
|
||||
return url.slice(0, 120);
|
||||
}
|
||||
function recordLivePlay(
|
||||
partial: Partial<ViaplayPlayDiag> & { guid: string; ok: boolean }
|
||||
) {
|
||||
const error = partial.error ?? null;
|
||||
recordViaplayPlay({
|
||||
kind: "live",
|
||||
title: null,
|
||||
accountId: null,
|
||||
accountLabel: null,
|
||||
httpStatus: null,
|
||||
playHost: "play-live.viaplay.com",
|
||||
mediaPath: null,
|
||||
hasLicense: false,
|
||||
dontGo: isDontGoError(error) || !!partial.dontGo,
|
||||
dontGoReason: partial.dontGoReason || (isDontGoError(error) ? error : null),
|
||||
productDurationMs: null,
|
||||
mpdDurationMs: null,
|
||||
cookies: emptyCookies(),
|
||||
refreshOk: null,
|
||||
error,
|
||||
resolver: "viaplay-play-live-v10",
|
||||
...partial,
|
||||
kind: "live",
|
||||
resolver: "viaplay-play-live-v10",
|
||||
});
|
||||
}
|
||||
|
||||
function parseKey(value: string): { kid: string; key: string } | null {
|
||||
|
|
@ -306,6 +342,7 @@ export async function ensureViaplayEventReady(
|
|||
accessToken: refreshed.accessToken || undefined,
|
||||
persistentLogin: refreshed.persistentLogin || undefined,
|
||||
boid: refreshed.boid || undefined,
|
||||
cookieJar: refreshed.cookieJar || undefined,
|
||||
});
|
||||
} catch {
|
||||
/* continue with in-memory refresh */
|
||||
|
|
@ -396,6 +433,19 @@ export async function ensureViaplayEventReady(
|
|||
accountIds: event.accountIds.length ? event.accountIds : [account.id],
|
||||
});
|
||||
|
||||
recordLivePlay({
|
||||
guid: mediaGuid,
|
||||
ok: true,
|
||||
title: play.title,
|
||||
accountId: account.id,
|
||||
accountLabel: account.label,
|
||||
mediaPath: mediaPathOf(kap.mpdUrl),
|
||||
hasLicense: !!play.licenseUrl,
|
||||
dontGo: isDontGoPlaceholder(kap.mpdUrl),
|
||||
cookies: cookieInventory(creds),
|
||||
error: null,
|
||||
});
|
||||
|
||||
return {
|
||||
streamUrl: kap.mpdUrl,
|
||||
format: "dash",
|
||||
|
|
@ -407,6 +457,14 @@ export async function ensureViaplayEventReady(
|
|||
};
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
recordLivePlay({
|
||||
guid: mediaGuid,
|
||||
ok: false,
|
||||
accountId: account.id,
|
||||
accountLabel: account.label,
|
||||
cookies: cookieInventory(account),
|
||||
error: lastError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -516,6 +574,7 @@ async function resolveViaplayGuidPlayInner(
|
|||
accessToken: refreshed.accessToken || undefined,
|
||||
persistentLogin: refreshed.persistentLogin || undefined,
|
||||
boid: refreshed.boid || undefined,
|
||||
cookieJar: refreshed.cookieJar || undefined,
|
||||
});
|
||||
} catch {
|
||||
/* play mag doorgaan met in-memory refresh */
|
||||
|
|
@ -609,7 +668,12 @@ async function resolveViaplayGuidPlayInner(
|
|||
ok: true,
|
||||
title: play.title,
|
||||
accountId: account.id,
|
||||
accountLabel: label,
|
||||
mediaPath: mediaPathOf(mpdUrl),
|
||||
hasLicense: !!play.licenseUrl,
|
||||
dontGo: isDontGoPlaceholder(mpdUrl),
|
||||
productDurationMs: play.durationMs,
|
||||
cookies: cookieInventory(creds),
|
||||
error: null,
|
||||
triedAccounts: triedLabels.length,
|
||||
triedLabels,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
type ViaplayAccountStored,
|
||||
type ViaplayEventOwners,
|
||||
} from "./accounts";
|
||||
import { pickPlayCookies } from "./cookies";
|
||||
import { fetchAccountOverview } from "./client";
|
||||
import {
|
||||
mergeViaplayTweaks,
|
||||
|
|
@ -205,6 +206,7 @@ export async function addViaplayAccount(
|
|||
boid?: string;
|
||||
password?: string;
|
||||
persistentLogin?: string;
|
||||
cookieJar?: Record<string, string>;
|
||||
}
|
||||
) {
|
||||
const existing = await prisma.integrationSetting.findUnique({ where: { id: VIAPLAY_ID } });
|
||||
|
|
@ -236,6 +238,7 @@ export async function upsertViaplayAccountByEmail(
|
|||
profileId?: string;
|
||||
boid?: string;
|
||||
persistentLogin?: string;
|
||||
cookieJar?: Record<string, string>;
|
||||
}
|
||||
) {
|
||||
const existing = await prisma.integrationSetting.findUnique({ where: { id: VIAPLAY_ID } });
|
||||
|
|
@ -256,6 +259,7 @@ export async function upsertViaplayAccountByEmail(
|
|||
profileId: input.profileId,
|
||||
boid: input.boid,
|
||||
persistentLogin: input.persistentLogin,
|
||||
cookieJar: input.cookieJar,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -267,6 +271,7 @@ export async function upsertViaplayAccountByEmail(
|
|||
profileId: input.profileId,
|
||||
boid: input.boid,
|
||||
persistentLogin: input.persistentLogin,
|
||||
cookieJar: input.cookieJar,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -283,6 +288,7 @@ export async function updateViaplayAccount(
|
|||
boid?: string;
|
||||
password?: string;
|
||||
persistentLogin?: string;
|
||||
cookieJar?: Record<string, string>;
|
||||
clearCookies?: boolean;
|
||||
}
|
||||
) {
|
||||
|
|
@ -303,12 +309,16 @@ export async function updateViaplayAccount(
|
|||
let profileId = current.profileId;
|
||||
let boid = current.boid;
|
||||
let persistentLoginEnc = current.persistentLoginEnc;
|
||||
let cookieJarEnc = current.cookieJarEnc;
|
||||
let cookieNames = current.cookieNames;
|
||||
if (input.clearCookies) {
|
||||
sessionEnc = undefined;
|
||||
accessTokenEnc = undefined;
|
||||
profileId = undefined;
|
||||
boid = undefined;
|
||||
persistentLoginEnc = undefined;
|
||||
cookieJarEnc = undefined;
|
||||
cookieNames = undefined;
|
||||
}
|
||||
if (input.session?.trim()) {
|
||||
sessionEnc = encryptSecret(input.session.trim(), sessionSecret);
|
||||
|
|
@ -329,6 +339,13 @@ export async function updateViaplayAccount(
|
|||
if (input.persistentLogin?.trim()) {
|
||||
persistentLoginEnc = encryptSecret(input.persistentLogin.trim(), sessionSecret);
|
||||
}
|
||||
if (input.cookieJar && typeof input.cookieJar === "object") {
|
||||
const jar = pickPlayCookies(input.cookieJar);
|
||||
if (Object.keys(jar).length) {
|
||||
cookieJarEnc = encryptSecret(JSON.stringify(jar), sessionSecret);
|
||||
cookieNames = Object.keys(jar).sort();
|
||||
}
|
||||
}
|
||||
|
||||
let tokenExpiresAt = current.tokenExpiresAt;
|
||||
if (input.clearCookies) {
|
||||
|
|
@ -351,6 +368,8 @@ export async function updateViaplayAccount(
|
|||
profileId,
|
||||
boid,
|
||||
persistentLoginEnc,
|
||||
cookieJarEnc,
|
||||
cookieNames,
|
||||
passwordEnc: input.password?.trim()
|
||||
? encryptSecret(input.password.trim(), sessionSecret)
|
||||
: current.passwordEnc,
|
||||
|
|
|
|||
|
|
@ -1096,7 +1096,7 @@ export function registerViewerRoutes(
|
|||
fallbackFormat: null,
|
||||
live: false,
|
||||
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
||||
resolver: "viaplay-byguid-v9",
|
||||
resolver: "viaplay-byguid-v10",
|
||||
mediaPath: (() => {
|
||||
try {
|
||||
const u = new URL(played.streamUrl);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ async function pushViaplay(
|
|||
profileId: string | null;
|
||||
persistentLogin: string | null;
|
||||
boid?: string | null;
|
||||
cookieJar?: Record<string, string>;
|
||||
}
|
||||
) {
|
||||
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, {
|
||||
|
|
@ -70,6 +71,7 @@ async function pushViaplay(
|
|||
profileId: tokens.profileId || undefined,
|
||||
persistentLogin: tokens.persistentLogin || undefined,
|
||||
boid: tokens.boid || undefined,
|
||||
cookieJar: tokens.cookieJar,
|
||||
}),
|
||||
});
|
||||
const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type ViaplayLoginResult = {
|
|||
profileId: string | null;
|
||||
persistentLogin: string | null;
|
||||
boid: string | null;
|
||||
cookieJar: Record<string, string>;
|
||||
};
|
||||
|
||||
function profileDir(accountId: string): string {
|
||||
|
|
@ -98,8 +99,32 @@ async function readCookies(context: BrowserContext): Promise<ViaplayLoginResult
|
|||
const profileId = (byName.get("viaplay_profileId") || "").trim() || null;
|
||||
const persistentLogin = (byName.get("persistentLogin") || "").trim() || null;
|
||||
const boid = (byName.get("__Secure-Viaplay-Boid") || "").trim() || null;
|
||||
const cookieJar: Record<string, string> = {};
|
||||
const keep = new Set([
|
||||
"session",
|
||||
"accessToken",
|
||||
"persistentLogin",
|
||||
"hasPersistentLogin",
|
||||
"isPersistentLogin",
|
||||
"viaplay_profileId",
|
||||
"__Secure-Viaplay-Boid",
|
||||
"__Secure-Viaplay-Boid-exists",
|
||||
"datadome",
|
||||
"Viaplay-ClientId",
|
||||
"splitKey",
|
||||
"countryCode",
|
||||
"eupubconsent-v2",
|
||||
"OptanonConsent",
|
||||
"cookie_agreement",
|
||||
]);
|
||||
for (const c of cookies) {
|
||||
if (!c.name || !c.value) continue;
|
||||
if (keep.has(c.name) || /^__Secure-Viaplay/i.test(c.name)) {
|
||||
cookieJar[c.name] = c.value;
|
||||
}
|
||||
}
|
||||
if (!session || !accessToken) return null;
|
||||
return { session, accessToken, profileId, persistentLogin, boid };
|
||||
return { session, accessToken, profileId, persistentLogin, boid, cookieJar };
|
||||
}
|
||||
|
||||
async function launchProfile(userDataDir: string, headless: boolean) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,39 @@ export type CapturedTokens = {
|
|||
profileId: string | null;
|
||||
persistentLogin: string | null;
|
||||
boid: string | null;
|
||||
cookieJar: Record<string, string>;
|
||||
accessTokenExpMs: number | null;
|
||||
};
|
||||
|
||||
const PLAY_COOKIE_KEEP = new Set([
|
||||
"session",
|
||||
"accessToken",
|
||||
"persistentLogin",
|
||||
"hasPersistentLogin",
|
||||
"isPersistentLogin",
|
||||
"viaplay_profileId",
|
||||
"__Secure-Viaplay-Boid",
|
||||
"__Secure-Viaplay-Boid-exists",
|
||||
"datadome",
|
||||
"Viaplay-ClientId",
|
||||
"splitKey",
|
||||
"countryCode",
|
||||
"eupubconsent-v2",
|
||||
"OptanonConsent",
|
||||
"cookie_agreement",
|
||||
]);
|
||||
|
||||
function pickPlayCookies(byName: Map<string, string>): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const [name, value] of byName) {
|
||||
if (!name || !value?.trim()) continue;
|
||||
if (PLAY_COOKIE_KEEP.has(name) || /^__Secure-Viaplay/i.test(name)) {
|
||||
out[name] = value.trim();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const COOKIE_DOMAINS = [
|
||||
"viaplay.com",
|
||||
".viaplay.com",
|
||||
|
|
@ -50,7 +80,7 @@ export async function captureViaplayTokens(partition: string): Promise<CapturedT
|
|||
if (!c.name || !c.value) continue;
|
||||
if (
|
||||
/viaplay|mtg-api|login\.viaplay/i.test(c.domain || "") ||
|
||||
["session", "accessToken", "viaplay_profileId", "persistentLogin", "__Secure-Viaplay-Boid"].includes(c.name)
|
||||
["session", "accessToken", "viaplay_profileId", "persistentLogin", "__Secure-Viaplay-Boid", "datadome", "Viaplay-ClientId"].includes(c.name)
|
||||
) {
|
||||
byName.set(c.name, c.value);
|
||||
}
|
||||
|
|
@ -74,6 +104,7 @@ export async function captureViaplayTokens(partition: string): Promise<CapturedT
|
|||
profileId,
|
||||
persistentLogin,
|
||||
boid,
|
||||
cookieJar: pickPlayCookies(byName),
|
||||
accessTokenExpMs: jwtExpMs(accessToken),
|
||||
};
|
||||
}
|
||||
|
|
@ -97,6 +128,7 @@ export async function pushViaplayTokens(
|
|||
profileId: tokens.profileId || undefined,
|
||||
persistentLogin: tokens.persistentLogin || undefined,
|
||||
boid: tokens.boid || undefined,
|
||||
cookieJar: tokens.cookieJar,
|
||||
}),
|
||||
});
|
||||
const data = (await res.json().catch(() => ({}))) as {
|
||||
|
|
|
|||
Loading…
Reference in a new issue