diff --git a/apps/token-tray/src/login/f1tv.ts b/apps/token-tray/src/login/f1tv.ts index e8f5ccf..5d889dc 100644 --- a/apps/token-tray/src/login/f1tv.ts +++ b/apps/token-tray/src/login/f1tv.ts @@ -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 { const cookies = await context.cookies(); const storage = await context.storageState(); @@ -26,7 +44,6 @@ async function readTokens(context: BrowserContext): Promise setTimeout(r, 2000)); - let tokens = await readTokens(context); - if (tokens) return tokens; + 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); - 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); + let tokens = await readTokens(context); + if (tokens) return tokens; - const passBox = page.locator('input[type="password"]').first(); - await passBox.fill(opts.password); + // 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 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( diff --git a/apps/token-tray/src/login/viaplay.ts b/apps/token-tray/src/login/viaplay.ts index 12d40a6..dd411bd 100644 --- a/apps/token-tray/src/login/viaplay.ts +++ b/apps/token-tray/src/login/viaplay.ts @@ -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 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') - .filter({ hasText: /./ }) - .first(); try { - if (await profileBtn.isVisible({ timeout: 500 })) { - await profileBtn.click().catch(() => undefined); + const profile = page + .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 { /* 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(); }