Skip to content

Commit af8b0eb

Browse files
MohamedBassemclaude
andcommitted
feat(workers): drive the crawler browser with patchright
Playwright always enables the CDP Runtime domain, which bot-protection scripts detect. patchright is a maintained drop-in Playwright fork that avoids it. The stale puppeteer stealth plugin is dropped: its evasions contradicted each other (Windows UA, Linux platform, Mac GPU) and differed between pages and workers. patchright's page.evaluate defaults to an isolated world, so autoconsent's lifecycle is now read through waitForFunction, which runs in the page's main world under both Playwright and patchright. Requires the new karakeep-chrome image: on the old headless-shell image, dropping the stealth plugin regresses PerimeterX-protected sites. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent b7252e7 commit af8b0eb

8 files changed

Lines changed: 54 additions & 279 deletions

File tree

‎apps/workers/package.json‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,11 @@
4848
"metascraper-youtube": "^5.58.1",
4949
"node-cron": "^3.0.3",
5050
"node-fetch": "^3.3.2",
51+
"patchright": "1.59.4",
5152
"pdf2json": "^3.1.5",
5253
"pdf2pic": "^3.1.3",
5354
"pdfjs-dist": "^4.2.67",
54-
"playwright": "^1.59.1",
55-
"playwright-extra": "^4.3.6",
5655
"prom-client": "^15.1.3",
57-
"puppeteer-extra-plugin-stealth": "^2.11.2",
5856
"rss-parser": "^3.13.0",
5957
"tesseract.js": "^7.0.0",
6058
"tsx": "^4.23.13",

‎apps/workers/tsconfig.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
"exclude": ["node_modules"],
66
"compilerOptions": {
77
"baseUrl": ".",
8-
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
8+
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
9+
// @ghostery/adblocker-playwright only imports "playwright" for its type
10+
// declarations; point them at patchright (same API) so it accepts our pages.
11+
"paths": {
12+
"playwright": ["../../node_modules/patchright"]
13+
}
914
}
1015
}

‎apps/workers/workers/crawler/autoconsent.ts‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { readFileSync } from "node:fs";
1212
import { createRequire } from "node:module";
1313
import * as path from "node:path";
1414
import { setTimeout as sleep } from "node:timers/promises";
15-
import type { Page } from "playwright";
15+
import type { Page } from "patchright";
1616
import { abortRaceResolve, raceWith } from "utils";
1717

1818
import serverConfig from "@karakeep/shared/config";
@@ -108,24 +108,29 @@ export async function installAutoconsent(
108108

109109
/**
110110
* Waits (capped at AUTOCONSENT_WAIT_MS, abort-aware) until the main frame's
111-
* autoconsent lifecycle leaves `pendingStates`.
111+
* autoconsent lifecycle leaves `pendingStates`, and returns that lifecycle
112+
* (undefined on timeout/abort). Reads it through waitForFunction, which runs in
113+
* the page's main world (where the bundle lives) in both Playwright and
114+
* patchright; patchright's page.evaluate defaults to an isolated world.
112115
*/
113116
async function waitWhileIn(
114117
page: Page,
115118
pendingStates: string[],
116119
abortSignal: AbortSignal,
117-
): Promise<void> {
118-
await raceWith<unknown>(
120+
): Promise<string | undefined> {
121+
return await raceWith<string | undefined>(
119122
page
120123
.waitForFunction(
121-
(pending) =>
122-
!pending.includes(
124+
(pending) => {
125+
const lifecycle =
123126
globalThis.autoconsentStandalone?.instance.state.lifecycle ??
124-
"loading",
125-
),
127+
"loading";
128+
return pending.includes(lifecycle) ? false : lifecycle;
129+
},
126130
pendingStates,
127131
{ timeout: AUTOCONSENT_WAIT_MS, polling: 100 },
128132
)
133+
.then(async (handle) => (await handle.jsonValue()) || undefined)
129134
.catch(() => undefined),
130135
abortRaceResolve(abortSignal, undefined),
131136
);
@@ -152,11 +157,7 @@ export async function waitForPageLoadAndAutoconsent(
152157
return;
153158
}
154159
await Promise.all([pageLoad, waitWhileIn(page, PENDING_STATES, abortSignal)]);
155-
await waitWhileIn(page, OPTING_OUT_STATES, abortSignal);
156-
157-
const lifecycle = await page
158-
.evaluate(() => globalThis.autoconsentStandalone?.instance.state.lifecycle)
159-
.catch(() => undefined);
160+
const lifecycle = await waitWhileIn(page, OPTING_OUT_STATES, abortSignal);
160161
if (lifecycle && OPTED_OUT_STATES.includes(lifecycle)) {
161162
await sleep(DISMISS_SETTLE_MS, undefined, { signal: abortSignal }).catch(
162163
() => undefined,

‎apps/workers/workers/crawler/browser.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ import { Mutex } from "async-mutex";
1111
import { exitAbortController } from "exit";
1212
import { fetchWithProxy } from "network";
1313
import type { RunProxyConfig } from "network";
14-
import { Browser, BrowserContext, BrowserContextOptions } from "playwright";
15-
import { chromium } from "playwright-extra";
16-
import StealthPlugin from "puppeteer-extra-plugin-stealth";
14+
// patchright is a drop-in Playwright fork that drives Chrome without enabling
15+
// the CDP Runtime domain, the most widely checked automation signal.
16+
import {
17+
Browser,
18+
BrowserContext,
19+
BrowserContextOptions,
20+
chromium,
21+
} from "patchright";
1722
import { raceWith, timeoutRace } from "utils";
1823
import { z } from "zod";
1924

@@ -297,12 +302,11 @@ async function loadCookiesFromFile(): Promise<void> {
297302
}
298303

299304
/**
300-
* One-time setup of the crawler's browser environment: stealth plugin,
301-
* adblocker, the shared browser connection (unless connecting on demand),
302-
* cookies, and the stale-context reaper.
305+
* One-time setup of the crawler's browser environment: adblocker, the shared
306+
* browser connection (unless connecting on demand), cookies, and the
307+
* stale-context reaper.
303308
*/
304309
export async function initializeBrowserEnvironment(): Promise<void> {
305-
chromium.use(StealthPlugin());
306310
await loadAdblocker();
307311
loadAutoconsent();
308312
if (!serverConfig.crawler.browserConnectOnDemand) {

‎apps/workers/workers/crawler/crawlPage.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
BrowserContextOptions,
1818
CDPSession,
1919
Page,
20-
} from "playwright";
20+
} from "patchright";
2121
import { abortRace, abortRaceResolve, raceWith, timeoutRace } from "utils";
2222

2323
import { db } from "@karakeep/db";

‎patches/playwright-extra@4.3.6.patch‎

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)