stremio/apps/token-tray/src/viaplay.ts
Jos Vooges | STH 22d46ffac7 Add Windows token-tray MVP for Viaplay session refresh.
Agent API accepts X-Tray-Key pushes; Electron tray captures cookies and upserts accounts by email.
2026-09-22 23:24:44 +02:00

118 lines
3.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;
accessTokenExpMs: number | null;
};
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"].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;
if (!sessionVal || !accessToken) return null;
return {
session: sessionVal,
accessToken,
profileId,
persistentLogin,
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,
}),
});
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;
}