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 path from "node:path";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
|
||||||
|
|
@ -12,6 +12,24 @@ function profileDir(accountId: string): string {
|
||||||
return path.join(app.getPath("userData"), "browser-profiles", "f1tv", accountId);
|
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> {
|
async function readTokens(context: BrowserContext): Promise<F1tvLoginResult | null> {
|
||||||
const cookies = await context.cookies();
|
const cookies = await context.cookies();
|
||||||
const storage = await context.storageState();
|
const storage = await context.storageState();
|
||||||
|
|
@ -26,7 +44,6 @@ async function readTokens(context: BrowserContext): Promise<F1tvLoginResult | nu
|
||||||
byName.get("entitlementtoken") || byName.get("entitlement_token") || "";
|
byName.get("entitlementtoken") || byName.get("entitlement_token") || "";
|
||||||
let sessionId = byName.get("sessionid") || byName.get("session-id") || "";
|
let sessionId = byName.get("sessionid") || byName.get("session-id") || "";
|
||||||
|
|
||||||
// localStorage / sessionStorage vaak JWT's
|
|
||||||
for (const origin of storage.origins || []) {
|
for (const origin of storage.origins || []) {
|
||||||
for (const item of origin.localStorage || []) {
|
for (const item of origin.localStorage || []) {
|
||||||
const k = item.name.toLowerCase();
|
const k = item.name.toLowerCase();
|
||||||
|
|
@ -57,7 +74,7 @@ async function readTokens(context: BrowserContext): Promise<F1tvLoginResult | nu
|
||||||
async function launchProfile(userDataDir: string, headless: boolean) {
|
async function launchProfile(userDataDir: string, headless: boolean) {
|
||||||
const common = {
|
const common = {
|
||||||
headless,
|
headless,
|
||||||
viewport: { width: 1280, height: 800 } as const,
|
viewport: { width: 1280, height: 900 } as const,
|
||||||
args: ["--disable-blink-features=AutomationControlled"],
|
args: ["--disable-blink-features=AutomationControlled"],
|
||||||
};
|
};
|
||||||
for (const channel of ["chrome", "msedge"] as const) {
|
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: {
|
export async function loginF1tvUi(opts: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -88,36 +132,46 @@ export async function loginF1tvUi(opts: {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = context.pages()[0] || (await context.newPage());
|
const page = context.pages()[0] || (await context.newPage());
|
||||||
await page.goto("https://account.formula1.com/#/en/login", {
|
|
||||||
waitUntil: "domcontentloaded",
|
|
||||||
timeout: 60_000,
|
|
||||||
});
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
|
||||||
|
|
||||||
let tokens = await readTokens(context);
|
for (const url of [
|
||||||
if (tokens) return tokens;
|
"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);
|
||||||
|
|
||||||
const emailBox = page
|
let tokens = await readTokens(context);
|
||||||
.locator('input[type="email"]')
|
if (tokens) return tokens;
|
||||||
.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);
|
|
||||||
|
|
||||||
const passBox = page.locator('input[type="password"]').first();
|
// Op f1tv.com eerst op Sign in klikken
|
||||||
await passBox.fill(opts.password);
|
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 submit = page
|
const hasPass = await page
|
||||||
.getByRole("button", { name: /log\s*in|sign\s*in|continue/i })
|
.locator('input[type="password"]')
|
||||||
.or(page.locator('button[type="submit"]'))
|
.filter({ visible: true })
|
||||||
.first();
|
.count();
|
||||||
await submit.click();
|
if (hasPass > 0) {
|
||||||
|
await fillLogin(page, opts.email, opts.password);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deadline = Date.now() + 120_000;
|
const deadline = Date.now() + 120_000;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
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
|
await page
|
||||||
.goto("https://f1tv.formula1.com/", {
|
.goto("https://f1tv.formula1.com/", {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
|
|
@ -125,7 +179,7 @@ export async function loginF1tvUi(opts: {
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
tokens = await readTokens(context);
|
const tokens = await readTokens(context);
|
||||||
if (tokens) return tokens;
|
if (tokens) return tokens;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
||||||
|
|
@ -13,23 +13,69 @@ function profileDir(accountId: string): string {
|
||||||
return path.join(app.getPath("userData"), "browser-profiles", "viaplay", accountId);
|
return path.join(app.getPath("userData"), "browser-profiles", "viaplay", accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fillLogin(page: Page, email: string, password: string) {
|
async function dismissCookies(page: Page) {
|
||||||
// Semantic selectors — werkt op account.viaplay.com / viaplay.com login
|
const candidates = [
|
||||||
const emailBox = page
|
page.getByRole("button", { name: /alles accepteren|accept all|agree|akkoord|toestaan/i }),
|
||||||
.locator('input[type="email"]')
|
page.locator("#onetrust-accept-btn-handler"),
|
||||||
.or(page.getByLabel(/e-?mail/i))
|
page.locator('button[id*="accept" i]'),
|
||||||
.or(page.getByPlaceholder(/e-?mail/i))
|
];
|
||||||
.first();
|
for (const loc of candidates) {
|
||||||
await emailBox.waitFor({ state: "visible", timeout: 30_000 });
|
try {
|
||||||
await emailBox.fill(email);
|
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);
|
await passBox.fill(password);
|
||||||
|
|
||||||
const submit = page
|
const submit = form
|
||||||
.getByRole("button", { name: /aanmelden|log\s*in|sign\s*in|inloggen/i })
|
.getByRole("button", { name: /aanmelden|log\s*in|sign\s*in|inloggen|continue/i })
|
||||||
.or(page.locator('button[type="submit"]'))
|
.or(form.locator('button[type="submit"], input[type="submit"]'))
|
||||||
|
.or(
|
||||||
|
page.getByRole("button", { name: /aanmelden|log\s*in|sign\s*in|inloggen/i })
|
||||||
|
)
|
||||||
.first();
|
.first();
|
||||||
await submit.click();
|
await submit.click();
|
||||||
}
|
}
|
||||||
|
|
@ -50,10 +96,9 @@ async function readCookies(context: BrowserContext): Promise<ViaplayLoginResult
|
||||||
async function launchProfile(userDataDir: string, headless: boolean) {
|
async function launchProfile(userDataDir: string, headless: boolean) {
|
||||||
const common = {
|
const common = {
|
||||||
headless,
|
headless,
|
||||||
viewport: { width: 1280, height: 800 } as const,
|
viewport: { width: 1280, height: 900 } as const,
|
||||||
args: ["--disable-blink-features=AutomationControlled"],
|
args: ["--disable-blink-features=AutomationControlled"],
|
||||||
};
|
};
|
||||||
// Voorkeur: geïnstalleerde Chrome/Edge (geen Playwright-download nodig)
|
|
||||||
for (const channel of ["chrome", "msedge"] as const) {
|
for (const channel of ["chrome", "msedge"] as const) {
|
||||||
try {
|
try {
|
||||||
return await chromium.launchPersistentContext(userDataDir, {
|
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).
|
* Stille UI-login in echte Chromium (geen HTTP login-API → geen cmd-CAPTCHA).
|
||||||
* Hergebruikt persistent profiel per account.
|
|
||||||
*/
|
*/
|
||||||
export async function loginViaplayUi(opts: {
|
export async function loginViaplayUi(opts: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
@ -87,41 +131,56 @@ export async function loginViaplayUi(opts: {
|
||||||
try {
|
try {
|
||||||
const page = context.pages()[0] || (await context.newPage());
|
const page = context.pages()[0] || (await context.newPage());
|
||||||
|
|
||||||
// Snel pad: al cookies?
|
|
||||||
await page.goto("https://viaplay.com/nl-nl", {
|
await page.goto("https://viaplay.com/nl-nl", {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
|
await dismissCookies(page);
|
||||||
let tokens = await readCookies(context);
|
let tokens = await readCookies(context);
|
||||||
if (tokens) return tokens;
|
if (tokens) return tokens;
|
||||||
|
|
||||||
await page.goto("https://viaplay.com/nl-nl/login", {
|
// Direct account-login (stabieler dan /nl-nl/login 404/redirect)
|
||||||
waitUntil: "domcontentloaded",
|
for (const url of [
|
||||||
timeout: 60_000,
|
"https://account.viaplay.com/login",
|
||||||
});
|
"https://viaplay.com/login",
|
||||||
// Soms redirect naar account.viaplay.com
|
"https://viaplay.com/nl-nl/login",
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
]) {
|
||||||
|
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);
|
await fillLogin(page, opts.email, opts.password);
|
||||||
|
|
||||||
// Wacht tot session cookie verschijnt
|
|
||||||
const deadline = Date.now() + 90_000;
|
const deadline = Date.now() + 90_000;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
tokens = await readCookies(context);
|
tokens = await readCookies(context);
|
||||||
if (tokens) return tokens;
|
if (tokens) return tokens;
|
||||||
const profileBtn = page
|
|
||||||
.locator('[data-testid*="profile"], button')
|
|
||||||
.filter({ hasText: /./ })
|
|
||||||
.first();
|
|
||||||
try {
|
try {
|
||||||
if (await profileBtn.isVisible({ timeout: 500 })) {
|
const profile = page
|
||||||
await profileBtn.click().catch(() => undefined);
|
.locator('[data-testid*="profile" i], [class*="profile" i] button, button')
|
||||||
|
.filter({ hasText: /./ })
|
||||||
|
.first();
|
||||||
|
if (await profile.isVisible({ timeout: 400 })) {
|
||||||
|
await profile.click().catch(() => undefined);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* 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 {
|
} finally {
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue