154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
import { session as electronSession } from "electron";
|
|
import type { TrayAccount, TrayConfig } from "./config";
|
|
|
|
export type CapturedTokens = {
|
|
session: string;
|
|
accessToken: string;
|
|
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",
|
|
"login.viaplay.com",
|
|
"account.mtg-api.com",
|
|
".mtg-api.com",
|
|
];
|
|
|
|
function jwtExpMs(token: string): number | null {
|
|
try {
|
|
const payload = JSON.parse(
|
|
Buffer.from(token.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString()
|
|
) as { exp?: number };
|
|
return payload.exp ? payload.exp * 1000 : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function captureViaplayTokens(partition: string): Promise<CapturedTokens | null> {
|
|
const ses = electronSession.fromPartition(`persist:${partition}`);
|
|
const byName = new Map<string, string>();
|
|
for (const domain of COOKIE_DOMAINS) {
|
|
try {
|
|
const cookies = await ses.cookies.get({ domain });
|
|
for (const c of cookies) {
|
|
if (c.name && c.value) byName.set(c.name, c.value);
|
|
}
|
|
} catch {
|
|
/* ignore domain miss */
|
|
}
|
|
}
|
|
|
|
// Also scan without domain filter
|
|
try {
|
|
const all = await ses.cookies.get({});
|
|
for (const c of all) {
|
|
if (!c.name || !c.value) continue;
|
|
if (
|
|
/viaplay|mtg-api|login\.viaplay/i.test(c.domain || "") ||
|
|
["session", "accessToken", "viaplay_profileId", "persistentLogin", "__Secure-Viaplay-Boid", "datadome", "Viaplay-ClientId"].includes(c.name)
|
|
) {
|
|
byName.set(c.name, c.value);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
const sessionVal = byName.get("session")?.trim() || "";
|
|
const accessToken = (byName.get("accessToken") || "")
|
|
.trim()
|
|
.replace(/^(VIAPLAY-AT|MTG-AT|Bearer)\s+/i, "");
|
|
const profileId = byName.get("viaplay_profileId")?.trim() || null;
|
|
const persistentLogin = byName.get("persistentLogin")?.trim() || null;
|
|
const boid = byName.get("__Secure-Viaplay-Boid")?.trim() || null;
|
|
|
|
if (!sessionVal || !accessToken) return null;
|
|
return {
|
|
session: sessionVal,
|
|
accessToken,
|
|
profileId,
|
|
persistentLogin,
|
|
boid,
|
|
cookieJar: pickPlayCookies(byName),
|
|
accessTokenExpMs: jwtExpMs(accessToken),
|
|
};
|
|
}
|
|
|
|
export async function pushViaplayTokens(
|
|
cfg: TrayConfig,
|
|
account: TrayAccount,
|
|
tokens: CapturedTokens
|
|
): Promise<{ summary: string; subscription?: string }> {
|
|
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Tray-Key": cfg.trayKey,
|
|
},
|
|
body: JSON.stringify({
|
|
email: account.email,
|
|
label: account.label || undefined,
|
|
session: tokens.session,
|
|
accessToken: tokens.accessToken,
|
|
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 };
|
|
accounts?: Array<{ email?: string; subscriptionSummary?: string | null }>;
|
|
};
|
|
if (!res.ok) {
|
|
throw new Error(data.error?.message || `HTTP ${res.status}`);
|
|
}
|
|
const match = data.accounts?.find((a) => a.email === account.email);
|
|
const sub = match?.subscriptionSummary || undefined;
|
|
return {
|
|
summary: sub ? `ok · ${sub}` : "ok · tokens geüpdatet",
|
|
subscription: sub,
|
|
};
|
|
}
|
|
|
|
export async function pingAgent(cfg: TrayConfig): Promise<boolean> {
|
|
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/status`, {
|
|
headers: { "X-Tray-Key": cfg.trayKey },
|
|
});
|
|
return res.ok;
|
|
}
|