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 { 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 { 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(); } }