Fix tray login selectors to use visible password form fields.
Avoid hidden forgot-password inputs on F1/Viaplay and prefer account.viaplay.com login URL.
This commit is contained in:
parent
06ba757094
commit
4b4f0abcff
2 changed files with 171 additions and 58 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { chromium, type BrowserContext } from "playwright";
|
||||
import { chromium, type BrowserContext, type Page } from "playwright";
|
||||
import path from "node:path";
|
||||
import { app } from "electron";
|
||||
|
||||
|
|
@ -12,6 +12,24 @@ function profileDir(accountId: string): string {
|
|||
return path.join(app.getPath("userData"), "browser-profiles", "f1tv", accountId);
|
||||
}
|
||||
|
||||
async function dismissCookies(page: Page) {
|
||||
const candidates = [
|
||||
page.getByRole("button", { name: /accept all|agree|allow all|akkoord/i }),
|
||||
page.locator("#onetrust-accept-btn-handler"),
|
||||
];
|
||||
for (const loc of candidates) {
|
||||
try {
|
||||
const btn = loc.first();
|
||||
if (await btn.isVisible({ timeout: 1500 })) {
|
||||
await btn.click({ timeout: 2000 });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* next */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readTokens(context: BrowserContext): Promise<F1tvLoginResult | null> {
|
||||
const cookies = await context.cookies();
|
||||
const storage = await context.storageState();
|
||||
|
|
@ -26,7 +44,6 @@ async function readTokens(context: BrowserContext): Promise<F1tvLoginResult | nu
|
|||
byName.get("entitlementtoken") || byName.get("entitlement_token") || "";
|
||||
let sessionId = byName.get("sessionid") || byName.get("session-id") || "";
|
||||
|
||||
// localStorage / sessionStorage vaak JWT's
|
||||
for (const origin of storage.origins || []) {
|
||||
for (const item of origin.localStorage || []) {
|
||||
const k = item.name.toLowerCase();
|
||||
|
|
@ -57,7 +74,7 @@ async function readTokens(context: BrowserContext): Promise<F1tvLoginResult | nu
|
|||
async function launchProfile(userDataDir: string, headless: boolean) {
|
||||
const common = {
|
||||
headless,
|
||||
viewport: { width: 1280, height: 800 } as const,
|
||||
viewport: { width: 1280, height: 900 } as const,
|
||||
args: ["--disable-blink-features=AutomationControlled"],
|
||||
};
|
||||
for (const channel of ["chrome", "msedge"] as const) {
|
||||
|
|
@ -77,6 +94,33 @@ async function launchProfile(userDataDir: string, headless: boolean) {
|
|||
});
|
||||
}
|
||||
|
||||
/** Login-formulier: zichtbaar password → e-mail in zelfde form (niet forgot-password). */
|
||||
async function fillLogin(page: Page, email: string, password: string) {
|
||||
await dismissCookies(page);
|
||||
|
||||
const passBox = page.locator('input[type="password"]').filter({ visible: true }).first();
|
||||
await passBox.waitFor({ state: "visible", timeout: 60_000 });
|
||||
|
||||
const form = page.locator("form").filter({ has: passBox }).first();
|
||||
// F1 gebruikt type=text name=email class=txtEmail — niet type=email
|
||||
const emailBox = form
|
||||
.locator(
|
||||
'input.txtEmail, input[name="email"], input[type="email"], input[type="text"], input[autocomplete="username"]'
|
||||
)
|
||||
.filter({ visible: true })
|
||||
.first();
|
||||
await emailBox.waitFor({ state: "visible", timeout: 15_000 });
|
||||
await emailBox.click({ clickCount: 3 });
|
||||
await emailBox.fill(email);
|
||||
await passBox.fill(password);
|
||||
|
||||
const submit = form
|
||||
.getByRole("button", { name: /log\s*in|sign\s*in|continue|submit/i })
|
||||
.or(form.locator('button[type="submit"], input[type="submit"], button.btnLogin, a.btnLogin'))
|
||||
.first();
|
||||
await submit.click();
|
||||
}
|
||||
|
||||
export async function loginF1tvUi(opts: {
|
||||
accountId: string;
|
||||
email: string;
|
||||
|
|
@ -88,36 +132,46 @@ export async function loginF1tvUi(opts: {
|
|||
|
||||
try {
|
||||
const page = context.pages()[0] || (await context.newPage());
|
||||
await page.goto("https://account.formula1.com/#/en/login", {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
for (const url of [
|
||||
"https://account.formula1.com/#/en/login?redirect=https%3A%2F%2Ff1tv.formula1.com%2F",
|
||||
"https://account.formula1.com/#/en/login",
|
||||
"https://f1tv.formula1.com/",
|
||||
]) {
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
await dismissCookies(page);
|
||||
|
||||
let tokens = await readTokens(context);
|
||||
if (tokens) return tokens;
|
||||
|
||||
const emailBox = page
|
||||
.locator('input[type="email"]')
|
||||
.or(page.getByLabel(/e-?mail/i))
|
||||
.or(page.locator("#LoginEmailAddress, #Email, input[name*=email i]"))
|
||||
.first();
|
||||
await emailBox.waitFor({ state: "visible", timeout: 45_000 });
|
||||
await emailBox.fill(opts.email);
|
||||
// Op f1tv.com eerst op Sign in klikken
|
||||
try {
|
||||
const signIn = page.getByRole("link", { name: /sign in|log in/i }).or(
|
||||
page.getByRole("button", { name: /sign in|log in/i })
|
||||
);
|
||||
if (await signIn.first().isVisible({ timeout: 2000 })) {
|
||||
await signIn.first().click();
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
const passBox = page.locator('input[type="password"]').first();
|
||||
await passBox.fill(opts.password);
|
||||
|
||||
const submit = page
|
||||
.getByRole("button", { name: /log\s*in|sign\s*in|continue/i })
|
||||
.or(page.locator('button[type="submit"]'))
|
||||
.first();
|
||||
await submit.click();
|
||||
const hasPass = await page
|
||||
.locator('input[type="password"]')
|
||||
.filter({ visible: true })
|
||||
.count();
|
||||
if (hasPass > 0) {
|
||||
await fillLogin(page, opts.email, opts.password);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
if (!page.url().includes("f1tv") && Date.now() > deadline - 60_000) {
|
||||
if (!/f1tv\.formula1\.com/i.test(page.url())) {
|
||||
await page
|
||||
.goto("https://f1tv.formula1.com/", {
|
||||
waitUntil: "domcontentloaded",
|
||||
|
|
@ -125,7 +179,7 @@ export async function loginF1tvUi(opts: {
|
|||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
tokens = await readTokens(context);
|
||||
const tokens = await readTokens(context);
|
||||
if (tokens) return tokens;
|
||||
}
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -13,23 +13,69 @@ function profileDir(accountId: string): string {
|
|||
return path.join(app.getPath("userData"), "browser-profiles", "viaplay", accountId);
|
||||
}
|
||||
|
||||
async function fillLogin(page: Page, email: string, password: string) {
|
||||
// Semantic selectors — werkt op account.viaplay.com / viaplay.com login
|
||||
const emailBox = page
|
||||
.locator('input[type="email"]')
|
||||
.or(page.getByLabel(/e-?mail/i))
|
||||
.or(page.getByPlaceholder(/e-?mail/i))
|
||||
.first();
|
||||
await emailBox.waitFor({ state: "visible", timeout: 30_000 });
|
||||
await emailBox.fill(email);
|
||||
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: 1500 })) {
|
||||
await btn.click({ timeout: 2000 });
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
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);
|
||||
|
||||
const passBox = page.locator('input[type="password"]').filter({ visible: true }).first();
|
||||
await passBox.waitFor({ state: "visible", timeout: 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 emailInForm.fill(email);
|
||||
} else {
|
||||
// Fallback: zichtbaar e-mailveld op de pagina (niet forgotPassword)
|
||||
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 emailBox.fill(email);
|
||||
}
|
||||
|
||||
const passBox = page.locator('input[type="password"]').first();
|
||||
await passBox.waitFor({ state: "visible", timeout: 15_000 });
|
||||
await passBox.fill(password);
|
||||
|
||||
const submit = page
|
||||
.getByRole("button", { name: /aanmelden|log\s*in|sign\s*in|inloggen/i })
|
||||
.or(page.locator('button[type="submit"]'))
|
||||
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 submit.click();
|
||||
}
|
||||
|
|
@ -50,10 +96,9 @@ async function readCookies(context: BrowserContext): Promise<ViaplayLoginResult
|
|||
async function launchProfile(userDataDir: string, headless: boolean) {
|
||||
const common = {
|
||||
headless,
|
||||
viewport: { width: 1280, height: 800 } as const,
|
||||
viewport: { width: 1280, height: 900 } as const,
|
||||
args: ["--disable-blink-features=AutomationControlled"],
|
||||
};
|
||||
// Voorkeur: geïnstalleerde Chrome/Edge (geen Playwright-download nodig)
|
||||
for (const channel of ["chrome", "msedge"] as const) {
|
||||
try {
|
||||
return await chromium.launchPersistentContext(userDataDir, {
|
||||
|
|
@ -73,7 +118,6 @@ async function launchProfile(userDataDir: string, headless: boolean) {
|
|||
|
||||
/**
|
||||
* Stille UI-login in echte Chromium (geen HTTP login-API → geen cmd-CAPTCHA).
|
||||
* Hergebruikt persistent profiel per account.
|
||||
*/
|
||||
export async function loginViaplayUi(opts: {
|
||||
accountId: string;
|
||||
|
|
@ -87,41 +131,56 @@ export async function loginViaplayUi(opts: {
|
|||
try {
|
||||
const page = context.pages()[0] || (await context.newPage());
|
||||
|
||||
// Snel pad: al cookies?
|
||||
await page.goto("https://viaplay.com/nl-nl", {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60_000,
|
||||
});
|
||||
await dismissCookies(page);
|
||||
let tokens = await readCookies(context);
|
||||
if (tokens) return tokens;
|
||||
|
||||
await page.goto("https://viaplay.com/nl-nl/login", {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60_000,
|
||||
});
|
||||
// Soms redirect naar account.viaplay.com
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
// 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 new Promise((r) => setTimeout(r, 1200));
|
||||
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);
|
||||
|
||||
// Wacht tot session cookie verschijnt
|
||||
const deadline = Date.now() + 90_000;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
tokens = await readCookies(context);
|
||||
if (tokens) return tokens;
|
||||
const profileBtn = page
|
||||
.locator('[data-testid*="profile"], button')
|
||||
try {
|
||||
const profile = page
|
||||
.locator('[data-testid*="profile" i], [class*="profile" i] button, button')
|
||||
.filter({ hasText: /./ })
|
||||
.first();
|
||||
try {
|
||||
if (await profileBtn.isVisible({ timeout: 500 })) {
|
||||
await profileBtn.click().catch(() => undefined);
|
||||
if (await profile.isVisible({ timeout: 400 })) {
|
||||
await profile.click().catch(() => undefined);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
throw new Error("Geen session/accessToken na UI-login — check wachtwoord of pagina-wijziging");
|
||||
throw new Error(
|
||||
"Geen session/accessToken na UI-login — check wachtwoord of pagina-wijziging"
|
||||
);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue