eg: use patchright to avoid captcha, #183

This commit is contained in:
Ralf Vogler 2025-05-16 00:54:28 +02:00
parent 991e0a1449
commit ba69580d51
4 changed files with 83 additions and 25 deletions

View file

@ -36,13 +36,13 @@ Data (including json files with claimed games, codes to redeem, screenshots) is
1. [Install Node.js](https://nodejs.org/en/download) 1. [Install Node.js](https://nodejs.org/en/download)
2. Clone/download this repository and `cd` into it in a terminal 2. Clone/download this repository and `cd` into it in a terminal
3. Run `npm install` 3. Run `npm install && npx patchright install chromium`
4. Run `pip install apprise` (or use [pipx](https://github.com/pypa/pipx) if you have [problems](https://stackoverflow.com/questions/75608323/how-do-i-solve-error-externally-managed-environment-every-time-i-use-pip-3)) to install [apprise](https://github.com/caronc/apprise) if you want notifications 4. Run `pip install apprise` (or use [pipx](https://github.com/pypa/pipx) if you have [problems](https://stackoverflow.com/questions/75608323/how-do-i-solve-error-externally-managed-environment-every-time-i-use-pip-3)) to install [apprise](https://github.com/caronc/apprise) if you want notifications
5. To get updates: `git pull; npm install` 5. To get updates: `git pull; npm install`
6. Run `node epic-games`, `node prime-gaming`, `node gog`... 6. Run `node epic-games`, `node prime-gaming`, `node gog`...
During `npm install` Playwright will download its Firefox to a cache in home ([doc](https://playwright.dev/docs/browsers#managing-browser-binaries)). Patchright/Playwright will download its Chromium to a cache in home ([doc](https://playwright.dev/docs/browsers#managing-browser-binaries)).
If you are missing some dependencies for the browser on your system, you can use `sudo npx playwright install firefox --with-deps`. If you are missing some dependencies for the browser on your system, you can use `sudo npx patchright install chromium --with-deps`.
If you don't want to use Docker for quasi-headless mode, you could run inside a virtual machine, on a server, or you wake your PC at night to avoid being interrupted. If you don't want to use Docker for quasi-headless mode, you could run inside a virtual machine, on a server, or you wake your PC at night to avoid being interrupted.
</details> </details>

View file

@ -1,9 +1,10 @@
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra // import { chromium } from 'playwright-chromium';
import { chromium } from 'patchright';
import { authenticator } from 'otplib'; import { authenticator } from 'otplib';
import chalk from 'chalk'; import chalk from 'chalk';
import path from 'path'; import path from 'path';
import { existsSync, writeFileSync, appendFileSync } from 'fs'; import { existsSync, writeFileSync, appendFileSync } from 'fs';
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js'; import { resolve, jsonDb, datetime, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
import { cfg } from './src/config.js'; import { cfg } from './src/config.js';
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a); const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
@ -17,35 +18,26 @@ const db = await jsonDb('epic-games.json', {});
if (cfg.time) console.time('startup'); if (cfg.time) console.time('startup');
const browserPrefs = path.join(cfg.dir.browser, 'prefs.js');
if (existsSync(browserPrefs)) {
console.log('Adding webgl.disabled to', browserPrefs);
appendFileSync(browserPrefs, 'user_pref("webgl.disabled", true);'); // apparently Firefox removes duplicates (and sorts), so no problem appending every time
} else {
console.log(browserPrefs, 'does not exist yet, will patch it on next run. Restart the script if you get a captcha.');
}
// https://playwright.dev/docs/auth#multi-factor-authentication // https://playwright.dev/docs/auth#multi-factor-authentication
const context = await firefox.launchPersistentContext(cfg.dir.browser, { const context = await chromium.launchPersistentContext(cfg.dir.browser, {
// channel: 'chrome', // recommended, but `npx patchright install chrome` clashes with system Chrome - https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-nodejs#best-practice----use-chrome-without-fingerprint-injection
headless: cfg.headless, headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height }, viewport: { width: cfg.width, height: cfg.height },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated? // locale: 'en-US', // ignore OS locale to be sure to have english text for locators
// userAgent firefox (macOS): Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0
// userAgent firefox (docker): Mozilla/5.0 (X11; Linux aarch64; rv:109.0) Gecko/20100101 Firefox/115.0
locale: 'en-US', // ignore OS locale to be sure to have english text for locators
recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800
recordHar: cfg.record ? { path: `data/record/eg-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools recordHar: cfg.record ? { path: `data/record/eg-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
// user settings for firefox have to be put in $BROWSER_DIR/user.js // https://peter.sh/experiments/chromium-command-line-switches/
args: [ // https://wiki.mozilla.org/Firefox/CommandLineOptions args: [
// '-kiosk', '--hide-crash-restore-bubble',
], ],
chromiumSandbox: true, // https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/issues/52
}); });
handleSIGINT(context); // console.log(context.browser().browserType()); // browser is null...
if (cfg.debug) console.log(chromium.executablePath());
// Without stealth plugin, the website shows an hcaptcha on login with username/password and in the last step of claiming a game. It may have other heuristics like unsuccessful logins as well. After <6h (TBD) it resets to no captcha again. Getting a new IP also resets. handleSIGINT(context);
await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
@ -150,7 +142,7 @@ try {
// debug showed that in those cases the href was still correct, so we `goto` the urls instead of clicking. // debug showed that in those cases the href was still correct, so we `goto` the urls instead of clicking.
// Alternative: parse the json loaded to build the page https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions // Alternative: parse the json loaded to build the page https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions
// i.e. filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213 // i.e. filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213
const urlSlugs = await Promise.all((await game_loc.elementHandles()).map(a => a.getAttribute('href'))); const urlSlugs = await Promise.all((await game_loc.all()).map(a => a.getAttribute('href')));
const urls = urlSlugs.map(s => 'https://store.epicgames.com' + s); const urls = urlSlugs.map(s => 'https://store.epicgames.com' + s);
console.log('Free games:', urls); console.log('Free games:', urls);

65
package-lock.json generated
View file

@ -16,6 +16,7 @@
"fingerprint-injector": "^2.1.66", "fingerprint-injector": "^2.1.66",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"patchright": "^1.52.3",
"playwright-firefox": "^1.52.0", "playwright-firefox": "^1.52.0",
"puppeteer-extra-plugin-stealth": "^2.11.2" "puppeteer-extra-plugin-stealth": "^2.11.2"
}, },
@ -1386,6 +1387,20 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -2138,6 +2153,36 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/patchright": {
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/patchright/-/patchright-1.52.3.tgz",
"integrity": "sha512-hi4MhiUrAFIS0CQH8tcHaRwWIhgK+B9Kz74pp95Mn/MJQHPTGLRYlNiJCxjcfxF3qDNhO1lrxDORKfRPvd45MA==",
"license": "Apache-2.0",
"dependencies": {
"patchright-core": "1.52.3"
},
"bin": {
"patchright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/patchright-core": {
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/patchright-core/-/patchright-core-1.52.3.tgz",
"integrity": "sha512-vTGjtGGR0VEZPujPd/2Xfq4uFIE1Rrz5pFwApwyrbyPILERLhzRXwR9MwtIQw4infpoPhF4QAd8qisB9znrTMg==",
"license": "Apache-2.0",
"bin": {
"patchright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -3786,6 +3831,12 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
}, },
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"function-bind": { "function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -4301,6 +4352,20 @@
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true "dev": true
}, },
"patchright": {
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/patchright/-/patchright-1.52.3.tgz",
"integrity": "sha512-hi4MhiUrAFIS0CQH8tcHaRwWIhgK+B9Kz74pp95Mn/MJQHPTGLRYlNiJCxjcfxF3qDNhO1lrxDORKfRPvd45MA==",
"requires": {
"fsevents": "2.3.2",
"patchright-core": "1.52.3"
}
},
"patchright-core": {
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/patchright-core/-/patchright-core-1.52.3.tgz",
"integrity": "sha512-vTGjtGGR0VEZPujPd/2Xfq4uFIE1Rrz5pFwApwyrbyPILERLhzRXwR9MwtIQw4infpoPhF4QAd8qisB9znrTMg=="
},
"path-exists": { "path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",

View file

@ -27,6 +27,7 @@
"fingerprint-injector": "^2.1.66", "fingerprint-injector": "^2.1.66",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"patchright": "^1.52.3",
"playwright-firefox": "^1.52.0", "playwright-firefox": "^1.52.0",
"puppeteer-extra-plugin-stealth": "^2.11.2" "puppeteer-extra-plugin-stealth": "^2.11.2"
}, },