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;
|
hasAccessToken: boolean;
|
||||||
hasProfileId: boolean;
|
hasProfileId: boolean;
|
||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
|
hasCookieJar?: boolean;
|
||||||
|
hasDatadome?: boolean;
|
||||||
|
hasBoid?: boolean;
|
||||||
|
cookieNames?: string[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
eventCount: number;
|
eventCount: number;
|
||||||
healthStatus: "ok" | "dead" | "unknown";
|
healthStatus: "ok" | "dead" | "unknown";
|
||||||
|
|
@ -30,6 +34,30 @@ type ViaplayAccount = {
|
||||||
overviewCheckedAt: string | null;
|
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 = {
|
type ViaplaySettings = {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
@ -62,6 +90,7 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
||||||
const [tweaks, setTweaks] = useState<ViaplayTweaks>({});
|
const [tweaks, setTweaks] = useState<ViaplayTweaks>({});
|
||||||
const [showTweaks, setShowTweaks] = useState(false);
|
const [showTweaks, setShowTweaks] = useState(false);
|
||||||
const [showCookies, setShowCookies] = useState(false);
|
const [showCookies, setShowCookies] = useState(false);
|
||||||
|
const [playLog, setPlayLog] = useState<PlayLogEntry[]>([]);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [msg, setMsg] = useState<string | null>(null);
|
const [msg, setMsg] = useState<string | null>(null);
|
||||||
const [err, setErr] = 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)));
|
void load().catch((e) => setErr(String(e.message ?? e)));
|
||||||
}, [load]);
|
}, [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) {
|
async function saveSettings(e: FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
@ -411,10 +455,11 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
||||||
<div className="card" style={{ maxWidth: 760 }}>
|
<div className="card" style={{ maxWidth: 760 }}>
|
||||||
<h2 style={{ marginTop: 0 }}>Viaplay</h2>
|
<h2 style={{ marginTop: 0 }}>Viaplay</h2>
|
||||||
<p className="muted" style={{ marginTop: 0 }}>
|
<p className="muted" style={{ marginTop: 0 }}>
|
||||||
Sportagenda en live streams voor gekoppelde Viaplay-accounts. Zet e-mail + wachtwoord
|
Sportagenda en live streams voor gekoppelde Viaplay-accounts. Token Tray moet
|
||||||
(Token Tray logt stil in via de browser-UI). Tokens plakken blijft optioneel.
|
de <strong>hele cookie-jar</strong> meesturen (session + datadome + Boid), zoals
|
||||||
Agenda: max 3 dagen vooruit (vervangt Viaplay uit de externe events-feed). Manifests en
|
het werkende livescript — Viaplay/DataDome geeft anders Don't go i.p.v. 403.
|
||||||
licenties cache-first (±5 min vóór start).
|
Playwright-login wordt door DataDome als bot gezien; tokens uit echt Chrome
|
||||||
|
(Netscape-export / tray met Chrome-kanaal) werken beter.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: "1rem" }}>
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: "1rem" }}>
|
||||||
|
|
@ -437,6 +482,50 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<h3 style={{ marginBottom: "0.5rem" }}>Accounts</h3>
|
||||||
{accounts.length === 0 ? (
|
{accounts.length === 0 ? (
|
||||||
<p className="muted">Nog geen accounts — voeg e-mail + sessie-tokens toe.</p>
|
<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.email}
|
||||||
{a.hasPassword ? " · wachtwoord" : ""}
|
{a.hasPassword ? " · wachtwoord" : ""}
|
||||||
{a.hasSession && a.hasAccessToken ? " · tokens" : ""}
|
{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` : ""}
|
{a.eventCount ? ` · ${a.eventCount} events` : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1928,19 +1928,27 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
||||||
streamUrl: played.streamUrl,
|
streamUrl: played.streamUrl,
|
||||||
keyCount: played.keys.length,
|
keyCount: played.keys.length,
|
||||||
promo,
|
promo,
|
||||||
resolver: "viaplay-byguid-v9",
|
resolver: "viaplay-byguid-v10",
|
||||||
|
lastPlay: (await import("../viaplay/play")).getLastVodPlay(),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
guid,
|
guid,
|
||||||
error: err instanceof Error ? err.message : String(err),
|
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 () => {
|
app.post("/api/v1/admin/scripts/viaplay/sync", { preHandler: requireAdmin }, async () => {
|
||||||
const { syncViaplayCatalog } = await import("../viaplay/sync");
|
const { syncViaplayCatalog } = await import("../viaplay/sync");
|
||||||
return syncViaplayCatalog(config.SESSION_SECRET);
|
return syncViaplayCatalog(config.SESSION_SECRET);
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
boid?: string;
|
boid?: string;
|
||||||
|
cookieJar?: Record<string, string>;
|
||||||
};
|
};
|
||||||
const { upsertViaplayAccountByEmail } = await import("../viaplay/settings");
|
const { upsertViaplayAccountByEmail } = await import("../viaplay/settings");
|
||||||
return upsertViaplayAccountByEmail(config.SESSION_SECRET, {
|
return upsertViaplayAccountByEmail(config.SESSION_SECRET, {
|
||||||
|
|
@ -174,6 +175,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
||||||
profileId: body.profileId,
|
profileId: body.profileId,
|
||||||
persistentLogin: body.persistentLogin,
|
persistentLogin: body.persistentLogin,
|
||||||
boid: body.boid,
|
boid: body.boid,
|
||||||
|
cookieJar: body.cookieJar,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { decryptSecret, encryptSecret } from "../security/crypto";
|
import { decryptSecret, encryptSecret } from "../security/crypto";
|
||||||
import { jwtExpiryIso } from "../agent/token-expiry";
|
import { jwtExpiryIso } from "../agent/token-expiry";
|
||||||
|
import { pickPlayCookies } from "./cookies";
|
||||||
|
|
||||||
export type ViaplayAccountHealth = "ok" | "dead" | "unknown";
|
export type ViaplayAccountHealth = "ok" | "dead" | "unknown";
|
||||||
|
|
||||||
|
|
@ -17,6 +18,10 @@ export type ViaplayAccountStored = {
|
||||||
/** Optioneel — voor latere tray/login. */
|
/** Optioneel — voor latere tray/login. */
|
||||||
passwordEnc?: string;
|
passwordEnc?: string;
|
||||||
persistentLoginEnc?: 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;
|
enabled: boolean;
|
||||||
healthStatus?: ViaplayAccountHealth;
|
healthStatus?: ViaplayAccountHealth;
|
||||||
healthCheckedAt?: string;
|
healthCheckedAt?: string;
|
||||||
|
|
@ -36,6 +41,10 @@ export type ViaplayAccountPublic = {
|
||||||
hasAccessToken: boolean;
|
hasAccessToken: boolean;
|
||||||
hasProfileId: boolean;
|
hasProfileId: boolean;
|
||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
|
hasCookieJar: boolean;
|
||||||
|
hasDatadome: boolean;
|
||||||
|
hasBoid: boolean;
|
||||||
|
cookieNames: string[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
eventCount: number;
|
eventCount: number;
|
||||||
healthStatus: ViaplayAccountHealth;
|
healthStatus: ViaplayAccountHealth;
|
||||||
|
|
@ -58,6 +67,7 @@ export type ViaplayAccountCreds = {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
|
cookieJar: Record<string, string> | null;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,6 +92,12 @@ export function accountToPublic(
|
||||||
hasAccessToken: !!account.accessTokenEnc,
|
hasAccessToken: !!account.accessTokenEnc,
|
||||||
hasProfileId: !!account.profileId?.trim(),
|
hasProfileId: !!account.profileId?.trim(),
|
||||||
hasPassword: !!account.passwordEnc,
|
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,
|
enabled: account.enabled !== false,
|
||||||
eventCount,
|
eventCount,
|
||||||
healthStatus: account.healthStatus ?? "unknown",
|
healthStatus: account.healthStatus ?? "unknown",
|
||||||
|
|
@ -117,10 +133,29 @@ export function decryptAccount(
|
||||||
persistentLogin: account.persistentLoginEnc
|
persistentLogin: account.persistentLoginEnc
|
||||||
? decryptSecret(account.persistentLoginEnc, sessionSecret)
|
? decryptSecret(account.persistentLoginEnc, sessionSecret)
|
||||||
: null,
|
: null,
|
||||||
|
cookieJar: parseCookieJar(account.cookieJarEnc, sessionSecret),
|
||||||
enabled: account.enabled !== false,
|
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(
|
export function createAccountStored(
|
||||||
sessionSecret: string,
|
sessionSecret: string,
|
||||||
input: {
|
input: {
|
||||||
|
|
@ -135,6 +170,7 @@ export function createAccountStored(
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
|
cookieJar?: Record<string, string>;
|
||||||
}
|
}
|
||||||
): ViaplayAccountStored {
|
): ViaplayAccountStored {
|
||||||
const email = input.email.trim().toLowerCase();
|
const email = input.email.trim().toLowerCase();
|
||||||
|
|
@ -163,6 +199,13 @@ export function createAccountStored(
|
||||||
persistentLoginEnc: input.persistentLogin?.trim()
|
persistentLoginEnc: input.persistentLogin?.trim()
|
||||||
? encryptSecret(input.persistentLogin.trim(), sessionSecret)
|
? encryptSecret(input.persistentLogin.trim(), sessionSecret)
|
||||||
: undefined,
|
: 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,
|
enabled: input.enabled !== false,
|
||||||
tokenExpiresAt: accessToken ? jwtExpiryIso(accessToken) ?? undefined : undefined,
|
tokenExpiresAt: accessToken ? jwtExpiryIso(accessToken) ?? undefined : undefined,
|
||||||
};
|
};
|
||||||
|
|
@ -188,6 +231,10 @@ export function parseAccountsJson(raw: unknown): ViaplayAccountStored[] {
|
||||||
deviceId: String(row.deviceId ?? "").trim() || undefined,
|
deviceId: String(row.deviceId ?? "").trim() || undefined,
|
||||||
passwordEnc: String(row.passwordEnc ?? "").trim() || undefined,
|
passwordEnc: String(row.passwordEnc ?? "").trim() || undefined,
|
||||||
persistentLoginEnc: String(row.persistentLoginEnc ?? "").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,
|
enabled: row.enabled !== false,
|
||||||
healthStatus:
|
healthStatus:
|
||||||
row.healthStatus === "ok" ||
|
row.healthStatus === "ok" ||
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { ViaplayAccountCreds } from "./accounts";
|
import type { ViaplayAccountCreds } from "./accounts";
|
||||||
import { toMediaGuid } 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. */
|
/** Zelfde UA als het werkende livescript (viaplay3.py). Edge/153 + sec-ch-ua gaf Don't go. */
|
||||||
export const VIAPLAY_USER_AGENT =
|
export const VIAPLAY_USER_AGENT =
|
||||||
|
|
@ -55,17 +56,24 @@ function parseSetCookieJar(setCookies: string[]): Record<string, string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cookieHeader(creds: ViaplayAccountCreds): string {
|
function cookieHeader(creds: ViaplayAccountCreds): string {
|
||||||
const parts: string[] = [];
|
const merged: Record<string, string> = { ...(creds.cookieJar || {}) };
|
||||||
if (creds.session) parts.push(`session=${cookieValue(creds.session)}`);
|
if (creds.session) merged.session = creds.session;
|
||||||
if (creds.accessToken) parts.push(`accessToken=${creds.accessToken.trim()}`);
|
if (creds.accessToken) merged.accessToken = creds.accessToken.trim();
|
||||||
if (creds.profileId) parts.push(`viaplay_profileId=${creds.profileId.trim()}`);
|
if (creds.profileId) merged.viaplay_profileId = creds.profileId.trim();
|
||||||
if (creds.boid) {
|
if (creds.boid) merged["__Secure-Viaplay-Boid"] = creds.boid.trim();
|
||||||
parts.push(`__Secure-Viaplay-Boid=${creds.boid.trim()}`);
|
if (creds.persistentLogin) merged.persistentLogin = creds.persistentLogin;
|
||||||
}
|
const picked = pickPlayCookies(merged);
|
||||||
if (creds.persistentLogin) {
|
return Object.entries(picked)
|
||||||
parts.push(`persistentLogin=${cookieValue(creds.persistentLogin)}`);
|
.map(([k, v]) => `${k}=${cookieValue(v)}`)
|
||||||
}
|
.join("; ");
|
||||||
return parts.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> {
|
export function authHeaders(creds: ViaplayAccountCreds): Record<string, string> {
|
||||||
|
|
@ -155,6 +163,19 @@ export async function refreshViaplaySession(
|
||||||
const session = sessionRaw;
|
const session = sessionRaw;
|
||||||
const accessToken = normalizeAccessToken(accessRaw) || accessRaw.trim();
|
const accessToken = normalizeAccessToken(accessRaw) || accessRaw.trim();
|
||||||
const nextPl = jar.persistentLogin || pl;
|
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 {
|
return {
|
||||||
...creds,
|
...creds,
|
||||||
|
|
@ -162,6 +183,7 @@ export async function refreshViaplaySession(
|
||||||
accessToken,
|
accessToken,
|
||||||
persistentLogin: nextPl,
|
persistentLogin: nextPl,
|
||||||
boid: jar["__Secure-Viaplay-Boid"] || creds.boid,
|
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,
|
fetchMpdXml,
|
||||||
extractPsshFromMpd,
|
extractPsshFromMpd,
|
||||||
fetchViaplayBootstrapPssh,
|
fetchViaplayBootstrapPssh,
|
||||||
|
cookieInventory,
|
||||||
isDontGoPlaceholder,
|
isDontGoPlaceholder,
|
||||||
isLiveDashMpd,
|
isLiveDashMpd,
|
||||||
mpdLooksLikeDontGo,
|
mpdLooksLikeDontGo,
|
||||||
VIAPLAY_USER_AGENT,
|
VIAPLAY_USER_AGENT,
|
||||||
} from "./client";
|
} from "./client";
|
||||||
|
import { inventoryFromNames, type CookieInventory } from "./cookies";
|
||||||
|
import {
|
||||||
|
isDontGoError,
|
||||||
|
mediaPathOf,
|
||||||
|
recordViaplayPlay,
|
||||||
|
getLastViaplayPlay,
|
||||||
|
getViaplayPlayLog,
|
||||||
|
type ViaplayPlayDiag,
|
||||||
|
} from "./diag";
|
||||||
import {
|
import {
|
||||||
loadViaplaySettings,
|
loadViaplaySettings,
|
||||||
patchViaplayEventCache,
|
patchViaplayEventCache,
|
||||||
|
|
@ -27,44 +37,70 @@ import { findViaplayEvent } from "./sync";
|
||||||
import { isMpdCacheFresh, type ViaplayEventSnapshot } from "./tweaks";
|
import { isMpdCacheFresh, type ViaplayEventSnapshot } from "./tweaks";
|
||||||
import { VIAPLAY_CATCHUP_MS } from "./events";
|
import { VIAPLAY_CATCHUP_MS } from "./events";
|
||||||
|
|
||||||
export type LastVodPlayDiag = {
|
export type LastVodPlayDiag = ViaplayPlayDiag;
|
||||||
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[];
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastVodPlay: LastVodPlayDiag | null = null;
|
export function getLastVodPlay() {
|
||||||
|
return getLastViaplayPlay();
|
||||||
export function getLastVodPlay(): LastVodPlayDiag | null {
|
|
||||||
return lastVodPlay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordVodPlay( partial: Omit<LastVodPlayDiag, "at" | "resolver"> ) {
|
export { getViaplayPlayLog };
|
||||||
lastVodPlay = {
|
|
||||||
|
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,
|
...partial,
|
||||||
at: new Date().toISOString(),
|
kind: "vod",
|
||||||
resolver: "viaplay-byguid-v9",
|
resolver: "viaplay-byguid-v10",
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaPathOf(url: string | null | undefined): string | null {
|
function recordLivePlay(
|
||||||
if (!url) return null;
|
partial: Partial<ViaplayPlayDiag> & { guid: string; ok: boolean }
|
||||||
try {
|
) {
|
||||||
const u = new URL(url);
|
const error = partial.error ?? null;
|
||||||
const parts = u.pathname.split("/").filter(Boolean);
|
recordViaplayPlay({
|
||||||
return parts.slice(-3).join("/") || u.pathname;
|
kind: "live",
|
||||||
} catch {
|
title: null,
|
||||||
return url.slice(0, 120);
|
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 {
|
function parseKey(value: string): { kid: string; key: string } | null {
|
||||||
|
|
@ -306,6 +342,7 @@ export async function ensureViaplayEventReady(
|
||||||
accessToken: refreshed.accessToken || undefined,
|
accessToken: refreshed.accessToken || undefined,
|
||||||
persistentLogin: refreshed.persistentLogin || undefined,
|
persistentLogin: refreshed.persistentLogin || undefined,
|
||||||
boid: refreshed.boid || undefined,
|
boid: refreshed.boid || undefined,
|
||||||
|
cookieJar: refreshed.cookieJar || undefined,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* continue with in-memory refresh */
|
/* continue with in-memory refresh */
|
||||||
|
|
@ -396,6 +433,19 @@ export async function ensureViaplayEventReady(
|
||||||
accountIds: event.accountIds.length ? event.accountIds : [account.id],
|
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 {
|
return {
|
||||||
streamUrl: kap.mpdUrl,
|
streamUrl: kap.mpdUrl,
|
||||||
format: "dash",
|
format: "dash",
|
||||||
|
|
@ -407,6 +457,14 @@ export async function ensureViaplayEventReady(
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastError = err instanceof Error ? err : new Error(String(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,
|
accessToken: refreshed.accessToken || undefined,
|
||||||
persistentLogin: refreshed.persistentLogin || undefined,
|
persistentLogin: refreshed.persistentLogin || undefined,
|
||||||
boid: refreshed.boid || undefined,
|
boid: refreshed.boid || undefined,
|
||||||
|
cookieJar: refreshed.cookieJar || undefined,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* play mag doorgaan met in-memory refresh */
|
/* play mag doorgaan met in-memory refresh */
|
||||||
|
|
@ -609,7 +668,12 @@ async function resolveViaplayGuidPlayInner(
|
||||||
ok: true,
|
ok: true,
|
||||||
title: play.title,
|
title: play.title,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
|
accountLabel: label,
|
||||||
mediaPath: mediaPathOf(mpdUrl),
|
mediaPath: mediaPathOf(mpdUrl),
|
||||||
|
hasLicense: !!play.licenseUrl,
|
||||||
|
dontGo: isDontGoPlaceholder(mpdUrl),
|
||||||
|
productDurationMs: play.durationMs,
|
||||||
|
cookies: cookieInventory(creds),
|
||||||
error: null,
|
error: null,
|
||||||
triedAccounts: triedLabels.length,
|
triedAccounts: triedLabels.length,
|
||||||
triedLabels,
|
triedLabels,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
type ViaplayAccountStored,
|
type ViaplayAccountStored,
|
||||||
type ViaplayEventOwners,
|
type ViaplayEventOwners,
|
||||||
} from "./accounts";
|
} from "./accounts";
|
||||||
|
import { pickPlayCookies } from "./cookies";
|
||||||
import { fetchAccountOverview } from "./client";
|
import { fetchAccountOverview } from "./client";
|
||||||
import {
|
import {
|
||||||
mergeViaplayTweaks,
|
mergeViaplayTweaks,
|
||||||
|
|
@ -205,6 +206,7 @@ export async function addViaplayAccount(
|
||||||
boid?: string;
|
boid?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
|
cookieJar?: Record<string, string>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const existing = await prisma.integrationSetting.findUnique({ where: { id: VIAPLAY_ID } });
|
const existing = await prisma.integrationSetting.findUnique({ where: { id: VIAPLAY_ID } });
|
||||||
|
|
@ -236,6 +238,7 @@ export async function upsertViaplayAccountByEmail(
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
boid?: string;
|
boid?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
|
cookieJar?: Record<string, string>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const existing = await prisma.integrationSetting.findUnique({ where: { id: VIAPLAY_ID } });
|
const existing = await prisma.integrationSetting.findUnique({ where: { id: VIAPLAY_ID } });
|
||||||
|
|
@ -256,6 +259,7 @@ export async function upsertViaplayAccountByEmail(
|
||||||
profileId: input.profileId,
|
profileId: input.profileId,
|
||||||
boid: input.boid,
|
boid: input.boid,
|
||||||
persistentLogin: input.persistentLogin,
|
persistentLogin: input.persistentLogin,
|
||||||
|
cookieJar: input.cookieJar,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +271,7 @@ export async function upsertViaplayAccountByEmail(
|
||||||
profileId: input.profileId,
|
profileId: input.profileId,
|
||||||
boid: input.boid,
|
boid: input.boid,
|
||||||
persistentLogin: input.persistentLogin,
|
persistentLogin: input.persistentLogin,
|
||||||
|
cookieJar: input.cookieJar,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,6 +288,7 @@ export async function updateViaplayAccount(
|
||||||
boid?: string;
|
boid?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
persistentLogin?: string;
|
persistentLogin?: string;
|
||||||
|
cookieJar?: Record<string, string>;
|
||||||
clearCookies?: boolean;
|
clearCookies?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -303,12 +309,16 @@ export async function updateViaplayAccount(
|
||||||
let profileId = current.profileId;
|
let profileId = current.profileId;
|
||||||
let boid = current.boid;
|
let boid = current.boid;
|
||||||
let persistentLoginEnc = current.persistentLoginEnc;
|
let persistentLoginEnc = current.persistentLoginEnc;
|
||||||
|
let cookieJarEnc = current.cookieJarEnc;
|
||||||
|
let cookieNames = current.cookieNames;
|
||||||
if (input.clearCookies) {
|
if (input.clearCookies) {
|
||||||
sessionEnc = undefined;
|
sessionEnc = undefined;
|
||||||
accessTokenEnc = undefined;
|
accessTokenEnc = undefined;
|
||||||
profileId = undefined;
|
profileId = undefined;
|
||||||
boid = undefined;
|
boid = undefined;
|
||||||
persistentLoginEnc = undefined;
|
persistentLoginEnc = undefined;
|
||||||
|
cookieJarEnc = undefined;
|
||||||
|
cookieNames = undefined;
|
||||||
}
|
}
|
||||||
if (input.session?.trim()) {
|
if (input.session?.trim()) {
|
||||||
sessionEnc = encryptSecret(input.session.trim(), sessionSecret);
|
sessionEnc = encryptSecret(input.session.trim(), sessionSecret);
|
||||||
|
|
@ -329,6 +339,13 @@ export async function updateViaplayAccount(
|
||||||
if (input.persistentLogin?.trim()) {
|
if (input.persistentLogin?.trim()) {
|
||||||
persistentLoginEnc = encryptSecret(input.persistentLogin.trim(), sessionSecret);
|
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;
|
let tokenExpiresAt = current.tokenExpiresAt;
|
||||||
if (input.clearCookies) {
|
if (input.clearCookies) {
|
||||||
|
|
@ -351,6 +368,8 @@ export async function updateViaplayAccount(
|
||||||
profileId,
|
profileId,
|
||||||
boid,
|
boid,
|
||||||
persistentLoginEnc,
|
persistentLoginEnc,
|
||||||
|
cookieJarEnc,
|
||||||
|
cookieNames,
|
||||||
passwordEnc: input.password?.trim()
|
passwordEnc: input.password?.trim()
|
||||||
? encryptSecret(input.password.trim(), sessionSecret)
|
? encryptSecret(input.password.trim(), sessionSecret)
|
||||||
: current.passwordEnc,
|
: current.passwordEnc,
|
||||||
|
|
|
||||||
|
|
@ -1096,7 +1096,7 @@ export function registerViewerRoutes(
|
||||||
fallbackFormat: null,
|
fallbackFormat: null,
|
||||||
live: false,
|
live: false,
|
||||||
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
||||||
resolver: "viaplay-byguid-v9",
|
resolver: "viaplay-byguid-v10",
|
||||||
mediaPath: (() => {
|
mediaPath: (() => {
|
||||||
try {
|
try {
|
||||||
const u = new URL(played.streamUrl);
|
const u = new URL(played.streamUrl);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ async function pushViaplay(
|
||||||
profileId: string | null;
|
profileId: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
boid?: string | null;
|
boid?: string | null;
|
||||||
|
cookieJar?: Record<string, string>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, {
|
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, {
|
||||||
|
|
@ -70,6 +71,7 @@ async function pushViaplay(
|
||||||
profileId: tokens.profileId || undefined,
|
profileId: tokens.profileId || undefined,
|
||||||
persistentLogin: tokens.persistentLogin || undefined,
|
persistentLogin: tokens.persistentLogin || undefined,
|
||||||
boid: tokens.boid || undefined,
|
boid: tokens.boid || undefined,
|
||||||
|
cookieJar: tokens.cookieJar,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
|
const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export type ViaplayLoginResult = {
|
||||||
profileId: string | null;
|
profileId: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
boid: string | null;
|
boid: string | null;
|
||||||
|
cookieJar: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function profileDir(accountId: 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 profileId = (byName.get("viaplay_profileId") || "").trim() || null;
|
||||||
const persistentLogin = (byName.get("persistentLogin") || "").trim() || null;
|
const persistentLogin = (byName.get("persistentLogin") || "").trim() || null;
|
||||||
const boid = (byName.get("__Secure-Viaplay-Boid") || "").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;
|
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) {
|
async function launchProfile(userDataDir: string, headless: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,39 @@ export type CapturedTokens = {
|
||||||
profileId: string | null;
|
profileId: string | null;
|
||||||
persistentLogin: string | null;
|
persistentLogin: string | null;
|
||||||
boid: string | null;
|
boid: string | null;
|
||||||
|
cookieJar: Record<string, string>;
|
||||||
accessTokenExpMs: number | null;
|
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 = [
|
const COOKIE_DOMAINS = [
|
||||||
"viaplay.com",
|
"viaplay.com",
|
||||||
".viaplay.com",
|
".viaplay.com",
|
||||||
|
|
@ -50,7 +80,7 @@ export async function captureViaplayTokens(partition: string): Promise<CapturedT
|
||||||
if (!c.name || !c.value) continue;
|
if (!c.name || !c.value) continue;
|
||||||
if (
|
if (
|
||||||
/viaplay|mtg-api|login\.viaplay/i.test(c.domain || "") ||
|
/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);
|
byName.set(c.name, c.value);
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +104,7 @@ export async function captureViaplayTokens(partition: string): Promise<CapturedT
|
||||||
profileId,
|
profileId,
|
||||||
persistentLogin,
|
persistentLogin,
|
||||||
boid,
|
boid,
|
||||||
|
cookieJar: pickPlayCookies(byName),
|
||||||
accessTokenExpMs: jwtExpMs(accessToken),
|
accessTokenExpMs: jwtExpMs(accessToken),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +128,7 @@ export async function pushViaplayTokens(
|
||||||
profileId: tokens.profileId || undefined,
|
profileId: tokens.profileId || undefined,
|
||||||
persistentLogin: tokens.persistentLogin || undefined,
|
persistentLogin: tokens.persistentLogin || undefined,
|
||||||
boid: tokens.boid || undefined,
|
boid: tokens.boid || undefined,
|
||||||
|
cookieJar: tokens.cookieJar,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json().catch(() => ({}))) as {
|
const data = (await res.json().catch(() => ({}))) as {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue