Clips no longer use DASH live configuration (was jumping to end). Tray 0.1.2 types slower; no inter-account pause.
198 lines
5.9 KiB
TypeScript
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();
|
|
}
|
|
}
|