stremio/apps/token-tray/src/login/viaplay.ts
Jos Vooges | STH f9ec3be09e Fix Viaplay VOD live-edge seek and slow Token Tray typing.
Clips no longer use DASH live configuration (was jumping to end). Tray 0.1.2 types slower; no inter-account pause.
2026-09-23 02:38:26 +02:00

198 lines
5.9 KiB
TypeScript

import { chromium, type BrowserContext, type Page } from "playwright";
import path from "node:path";
import { app } from "electron";
import {
afterNavigate,
humanClick,
humanDelay,
humanType,
think,
waitForVisiblePassword,
} from "./human";
export type ViaplayLoginResult = {
session: string;
accessToken: string;
profileId: string | null;
persistentLogin: string | null;
};
function profileDir(accountId: string): string {
return path.join(app.getPath("userData"), "browser-profiles", "viaplay", accountId);
}
async function dismissCookies(page: Page) {
const candidates = [
page.getByRole("button", { name: /alles accepteren|accept all|agree|akkoord|toestaan/i }),
page.locator("#onetrust-accept-btn-handler"),
page.locator('button[id*="accept" i]'),
];
for (const loc of candidates) {
try {
const btn = loc.first();
if (await btn.isVisible({ timeout: 2500 })) {
await humanClick(btn, 3000);
return;
}
} catch {
/* next */
}
}
}
/** Vul login via zichtbaar wachtwoordveld → zelfde form (vermijdt verborgen forgot-password). */
async function fillLogin(page: Page, email: string, password: string) {
await dismissCookies(page);
await think();
const passBox = await waitForVisiblePassword(page, 45_000);
const form = page.locator("form").filter({ has: passBox }).first();
const emailInForm = form
.locator(
'input[type="email"], input[type="text"], input[name*="email" i], input[autocomplete="username"], input[autocomplete="email"]'
)
.filter({ visible: true })
.first();
if (await emailInForm.count()) {
await humanType(emailInForm, email);
} else {
const emailBox = page
.locator(
'input[type="email"], input[autocomplete="username"], input[autocomplete="email"]'
)
.or(
page.locator('input[type="text"], input[name*="email" i]').filter({
visible: true,
hasNot: page.locator("[data-i18n*='forgot' i]"),
})
)
.filter({ visible: true })
.first();
await emailBox.waitFor({ state: "visible", timeout: 15_000 });
await humanType(emailBox, email);
}
await think();
await humanType(passBox, password);
await humanDelay(600, 1600);
const submit = form
.getByRole("button", { name: /aanmelden|log\s*in|sign\s*in|inloggen|continue/i })
.or(form.locator('button[type="submit"], input[type="submit"]'))
.or(
page.getByRole("button", { name: /aanmelden|log\s*in|sign\s*in|inloggen/i })
)
.first();
await humanClick(submit);
}
async function readCookies(context: BrowserContext): Promise<ViaplayLoginResult | null> {
const cookies = await context.cookies();
const byName = new Map(cookies.map((c) => [c.name, c.value]));
const session = (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 (!session || !accessToken) return null;
return { session, accessToken, profileId, persistentLogin };
}
async function launchProfile(userDataDir: string, headless: boolean) {
const common = {
headless,
viewport: { width: 1280, height: 900 } as const,
args: ["--disable-blink-features=AutomationControlled"],
// Lichte slowMo; echte CAPTCHA-fix zit in humanType (toetsaanslagen)
slowMo: 15 + Math.floor(Math.random() * 25),
};
for (const channel of ["chrome", "msedge"] as const) {
try {
return await chromium.launchPersistentContext(userDataDir, {
...common,
channel,
locale: "nl-NL",
});
} catch {
/* try next */
}
}
return chromium.launchPersistentContext(userDataDir, {
...common,
locale: "nl-NL",
});
}
/**
* Stille UI-login in echte Chromium (geen HTTP login-API → geen cmd-CAPTCHA).
* Tempo is expres menselijk — te snelle fills/clicks triggeren CAPTCHA.
*/
export async function loginViaplayUi(opts: {
accountId: string;
email: string;
password: string;
headless?: boolean;
}): Promise<ViaplayLoginResult> {
const userDataDir = profileDir(opts.accountId);
const context = await launchProfile(userDataDir, opts.headless !== false);
try {
const page = context.pages()[0] || (await context.newPage());
await page.goto("https://viaplay.com/nl-nl", {
waitUntil: "domcontentloaded",
timeout: 60_000,
});
await afterNavigate();
await dismissCookies(page);
let tokens = await readCookies(context);
if (tokens) return tokens;
// Direct account-login (stabieler dan /nl-nl/login 404/redirect)
for (const url of [
"https://account.viaplay.com/login",
"https://viaplay.com/login",
"https://viaplay.com/nl-nl/login",
]) {
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45_000 });
await afterNavigate();
await dismissCookies(page);
const hasPass = await page
.locator('input[type="password"]')
.filter({ visible: true })
.count();
if (hasPass > 0) break;
} catch {
/* try next url */
}
}
await fillLogin(page, opts.email, opts.password);
const deadline = Date.now() + 120_000;
while (Date.now() < deadline) {
await humanDelay(2000, 3500);
tokens = await readCookies(context);
if (tokens) return tokens;
try {
const profile = page
.locator('[data-testid*="profile" i], [class*="profile" i] button, button')
.filter({ hasText: /./ })
.first();
if (await profile.isVisible({ timeout: 800 })) {
await humanClick(profile).catch(() => undefined);
}
} catch {
/* ignore */
}
}
throw new Error(
"Geen session/accessToken na UI-login — check wachtwoord of pagina-wijziging"
);
} finally {
await context.close();
}
}