stremio/apps/token-tray/src/jobs.ts
Jos Vooges | STH 2c14e2ce64 Silent browser UI login for Viaplay and F1TV via Token Tray.
Admin stores email/password; tray uses Playwright against real Chrome to click login and push tokens.
2026-09-22 23:54:10 +02:00

150 lines
4.1 KiB
TypeScript

import { Notification } from "electron";
import type { TrayConfig } from "./config";
import { saveConfig } from "./config";
import { loginViaplayUi } from "./login/viaplay";
import { loginF1tvUi } from "./login/f1tv";
export type AgentAccount = {
provider: "viaplay" | "f1tv";
id: string;
email: string;
label: string;
password: string | null;
hasTokens: boolean;
enabled: boolean;
};
export async function fetchAgentAccounts(cfg: TrayConfig): Promise<AgentAccount[]> {
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/accounts`, {
headers: { "X-Tray-Key": cfg.trayKey },
});
const data = (await res.json().catch(() => ({}))) as {
error?: { message?: string };
accounts?: AgentAccount[];
};
if (!res.ok) throw new Error(data.error?.message || `HTTP ${res.status}`);
return data.accounts ?? [];
}
async function pushViaplay(
cfg: TrayConfig,
email: string,
label: string,
tokens: {
session: string;
accessToken: string;
profileId: string | null;
persistentLogin: string | null;
}
) {
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,
label,
session: tokens.session,
accessToken: tokens.accessToken,
profileId: tokens.profileId || undefined,
persistentLogin: tokens.persistentLogin || undefined,
}),
});
const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
if (!res.ok) throw new Error(data.error?.message || `HTTP ${res.status}`);
}
async function pushF1tv(
cfg: TrayConfig,
email: string,
label: string,
tokens: {
ascendontoken: string;
entitlementtoken: string;
sessionId: string | null;
}
) {
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/f1tv/tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Tray-Key": cfg.trayKey,
},
body: JSON.stringify({
email,
label,
ascendontoken: tokens.ascendontoken,
entitlementtoken: tokens.entitlementtoken,
sessionId: tokens.sessionId || undefined,
}),
});
const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
if (!res.ok) throw new Error(data.error?.message || `HTTP ${res.status}`);
}
export type RefreshSummary = {
ok: number;
failed: number;
skipped: number;
errors: string[];
};
/** Headless UI-login voor alle accounts met wachtwoord. */
export async function refreshAllSilent(
cfg: TrayConfig,
opts?: { headless?: boolean; providers?: Array<"viaplay" | "f1tv"> }
): Promise<RefreshSummary> {
const headless = opts?.headless !== false;
const providers = new Set(opts?.providers ?? ["viaplay", "f1tv"]);
const accounts = await fetchAgentAccounts(cfg);
const summary: RefreshSummary = { ok: 0, failed: 0, skipped: 0, errors: [] };
for (const acc of accounts) {
if (!providers.has(acc.provider)) continue;
if (!acc.password?.trim()) {
summary.skipped += 1;
continue;
}
try {
if (acc.provider === "viaplay") {
const tokens = await loginViaplayUi({
accountId: acc.id,
email: acc.email,
password: acc.password,
headless,
});
await pushViaplay(cfg, acc.email, acc.label, tokens);
} else {
const tokens = await loginF1tvUi({
accountId: acc.id,
email: acc.email,
password: acc.password,
headless,
});
await pushF1tv(cfg, acc.email, acc.label, tokens);
}
summary.ok += 1;
} catch (err) {
summary.failed += 1;
summary.errors.push(
`${acc.provider}:${acc.email}: ${err instanceof Error ? err.message : String(err)}`
);
}
}
cfg.lastPushAt = new Date().toISOString();
cfg.lastPushSummary = `Stil: ${summary.ok} ok · ${summary.failed} fout · ${summary.skipped} overgeslagen`;
cfg.lastError = summary.errors[0];
saveConfig(cfg);
if (Notification.isSupported()) {
new Notification({
title: "Token Tray",
body: cfg.lastPushSummary,
}).show();
}
return summary;
}