diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index a54117c5..5e5adbaf 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -27,12 +27,13 @@ jobs:
-
name: Set environment variables
run: |
- echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
+ BRANCH="${GITHUB_REF#refs/heads/}"
+ echo "BRANCH=$BRANCH" >> $GITHUB_ENV
echo "NOW=$(date -R)" >> $GITHUB_ENV # date -Iseconds; date +'%Y-%m-%dT%H:%M:%S'
- if [[ "${{ env.BRANCH }}" == "main" ]]; then
+ if [[ "$BRANCH" == "main" ]]; then
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
else
- echo "IMAGE_TAG=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
+ echo "IMAGE_TAG=$BRANCH" >> $GITHUB_ENV
fi
-
name: Set up QEMU
diff --git a/README.md b/README.md
index dfb5c923..2431c55c 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ _Works on Windows/macOS/Linux._
Raspberry Pi (3, 4, Zero 2): [requires 64-bit OS](https://github.com/vogler/free-games-claimer/issues/3) like Raspberry Pi OS or Ubuntu (Raspbian won't work since it's 32-bit).
## How to run
-Easy option: [install Docker](https://docs.docker.com/get-docker/) (or [podman](https://podman-desktop.io/)) and run this command in a terminal (Windows: `cmd`, `.bat` file):
+Easy option: [install Docker](https://docs.docker.com/get-docker/) (or [podman](https://podman-desktop.io/)) and run this command in a terminal:
```
docker run --rm -it -p 6080:6080 -v fgc:/fgc/data --pull=always ghcr.io/vogler/free-games-claimer
```
@@ -92,6 +92,7 @@ Available options/variables and their default values:
| GOG_EMAIL | | GOG email for login. Overrides EMAIL. |
| GOG_PASSWORD | | GOG password for login. Overrides PASSWORD. |
| GOG_NEWSLETTER | 0 | Do not unsubscribe from newsletter after claiming a game if 1. |
+| LG_EMAIL | | Legacy Games: email to use for redeeming (if not set, defaults to PG_EMAIL) |
See `src/config.js` for all options.
@@ -103,7 +104,7 @@ You can pass variables using `-e VAR=VAL`, for example `docker run -e EMAIL=foo@
If you are using [docker compose](https://docs.docker.com/compose/environment-variables/) (or Portainer etc.), you can put options in the `environment:` section.
##### Without Docker
-On Linux/macOS you can prefix the variables you want to set, for example `EMAIL=foo@bar.baz SHOW=1 node epic-games` will show the browser and skip asking you for your login email.
+On Linux/macOS you can prefix the variables you want to set, for example `EMAIL=foo@bar.baz SHOW=1 node epic-games` will show the browser and skip asking you for your login email. On Windows you have to use `set`, [example](https://github.com/vogler/free-games-claimer/issues/314).
You can also put options in `data/config.env` which will be loaded by [dotenv](https://github.com/motdotla/dotenv).
### Notifications
@@ -156,7 +157,7 @@ If you want it to run regularly, you have to schedule the runs yourself:
- Linux/macOS: `crontab -e` ([example](https://github.com/vogler/free-games-claimer/discussions/56))
- macOS: [launchd](https://stackoverflow.com/questions/132955/how-do-i-set-a-task-to-run-every-so-often)
-- Windows: [task scheduler](https://active-directory-wp.com/docs/Usage/How_to_add_a_cron_job_on_Windows/Scheduled_tasks_and_cron_jobs_on_Windows/index.html), [other options](https://stackoverflow.com/questions/132971/what-is-the-windows-version-of-cron)
+- Windows: [task scheduler](https://active-directory-wp.com/docs/Usage/How_to_add_a_cron_job_on_Windows/Scheduled_tasks_and_cron_jobs_on_Windows/index.html) ([example](https://github.com/vogler/free-games-claimer/wiki/%5BHowTo%5D-Schedule-runs-on-Windows)), [other options](https://stackoverflow.com/questions/132971/what-is-the-windows-version-of-cron), or just put the command in a `.bat` file in Autostart if you restart often...
- any OS: use a process manager like [pm2](https://pm2.keymetrics.io/docs/usage/restart-strategies/)
- Docker Compose `command: bash -c "node epic-games; node prime-gaming; node gog; echo sleeping; sleep 1d"` additionally add `restart: unless-stopped` to it.
@@ -212,6 +213,9 @@ Added notifications via [apprise](https://github.com/caronc/apprise).
[![Star History Chart](https://api.star-history.com/svg?repos=vogler/free-games-claimer&type=Date)](https://star-history.com/#vogler/free-games-claimer&Date)
+
+
+![Alt](https://repobeats.axiom.co/api/embed/a1c5e6e420d90e0d6b34c1285e92a69a44138faa.svg "Repobeats analytics image")
---
diff --git a/aliexpress.js b/aliexpress.js
new file mode 100644
index 00000000..c44c6736
--- /dev/null
+++ b/aliexpress.js
@@ -0,0 +1,105 @@
+import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
+import { datetime, filenamify, prompt, handleSIGINT, stealth } from './src/util.js';
+import { cfg } from './src/config.js';
+
+const context = await firefox.launchPersistentContext(cfg.dir.browser, {
+ headless: cfg.headless,
+ viewport: { width: cfg.width, height: cfg.height },
+ locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
+ 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/gog-${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(context);
+await stealth(context);
+
+context.setDefaultTimeout(cfg.debug ? 0 : cfg.timeout);
+
+const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
+
+const auth = async (url) => {
+ console.log('auth', url);
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
+ // redirects to https://login.aliexpress.com/?return_url=https%3A%2F%2Fwww.aliexpress.com%2Fp%2Fcoin-pc-index%2Findex.html
+ await Promise.any([page.waitForURL(/.*login.aliexpress.com.*/).then(async () => {
+ // manual login
+ console.error('Not logged in! Will wait for 120s for you to login...');
+ // await page.waitForTimeout(120*1000);
+ // or try automated
+ page.locator('span:has-text("Switch account")').click().catch(_ => {}); // sometimes no longer logged in, but previous user/email is pre-selected -> in this case we want to go back to the classic login
+ const login = page.locator('.login-container');
+ const email = cfg.ae_email || await prompt({ message: 'Enter email' });
+ const emailInput = login.locator('input[label="Email or phone number"]');
+ await emailInput.fill(email);
+ await emailInput.blur(); // otherwise Continue button stays disabled
+ const continueButton = login.locator('button:has-text("Continue")');
+ await continueButton.click({ force: true }); // normal click waits for button to no longer be covered by their suggestion menu, so we have to force click somewhere for the menu to close and then click
+ await continueButton.click();
+ const password = email && (cfg.ae_password || await prompt({ type: 'password', message: 'Enter password' }));
+ await login.locator('input[label="Password"]').fill(password);
+ await login.locator('button:has-text("Sign in")').click();
+ const error = login.locator('.error-text');
+ error.waitFor().then(async _ => console.error('Login error:', await error.innerText()));
+ await page.waitForURL(url);
+ // await page.addLocatorHandler(page.getByRole('button', { name: 'Accept cookies' }), btn => btn.click());
+ page.getByRole('button', { name: 'Accept cookies' }).click().then(_ => console.log('Accepted cookies')).catch(_ => { });
+ }), page.locator('#nav-user-account').waitFor()]).catch(_ => {});
+
+ // await page.locator('#nav-user-account').hover();
+ // console.log('Logged in as:', await page.locator('.welcome-name').innerText());
+};
+
+// copied URLs from AliExpress app on tablet which has menu for the used webview
+const urls = {
+ // works with desktop view, but stuck at 100% loading in mobile view:
+ coins: 'https://www.aliexpress.com/p/coin-pc-index/index.html',
+ // only work with mobile view:
+ grow: 'https://m.aliexpress.com/p/ae_fruit/index.html', // firefox: stuck at 60% loading, chrome: loads, but canvas
+ gogo: 'https://m.aliexpress.com/p/gogo-match-cc/index.html', // closes firefox?!
+ // only show notification to install the app
+ euro: 'https://m.aliexpress.com/p/european-cup/index.html', // doesn't load
+ merge: 'https://m.aliexpress.com/p/merge-market/index.html',
+};
+
+const coins = async () => {
+ // await auth(urls.coins);
+ await Promise.any([page.locator('.checkin-button').click(), page.locator('.addcoin').waitFor()]);
+ console.log('Coins:', await page.locator('.mycoin-content-right-money').innerText());
+ console.log('Streak:', await page.locator('.title-box').innerText());
+ console.log('Tomorrow:', await page.locator('.addcoin').innerText());
+};
+
+const grow = async () => {
+ await page.pause();
+};
+
+const gogo = async () => {
+ await page.pause();
+};
+
+const euro = async () => {
+ await page.pause();
+};
+
+const merge = async () => {
+ await page.pause();
+};
+
+try {
+ // await coins();
+ await [
+ coins,
+ // grow,
+ // gogo,
+ // euro,
+ // merge,
+ ].reduce((a, f) => a.then(async _ => { await auth(urls[f.name]); await f(); console.log() }), Promise.resolve());
+
+ // await page.pause();
+} catch (error) {
+ process.exitCode ||= 1;
+ console.error('--- Exception:');
+ console.error(error); // .toString()?
+}
+if (page.video()) console.log('Recorded video:', await page.video().path());
+await context.close();
diff --git a/epic-games.js b/epic-games.js
index 1a2b602b..f8533804 100644
--- a/epic-games.js
+++ b/epic-games.js
@@ -1,7 +1,7 @@
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
import { authenticator } from 'otplib';
import path from 'path';
-import { existsSync, writeFileSync } 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 { cfg } from './src/config.js';
@@ -16,16 +16,24 @@ const db = await jsonDb('epic-games.json', {});
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
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height },
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated?
+ 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?
// 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
- recordHar: cfg.record ? { path: `data/record/eg-${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
// user settings for firefox have to be put in $BROWSER_DIR/user.js
args: [ // https://wiki.mozilla.org/Firefox/CommandLineOptions
@@ -41,8 +49,11 @@ await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
+await page.setViewportSize({ width: cfg.width, height: cfg.height }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
+
+// some debug info about the page (screen dimensions, user agent, platform)
// eslint-disable-next-line no-undef
-if (cfg.debug) console.debug(await page.evaluate(() => [window.screen, navigator.userAgent, navigator.platform]));
+if (cfg.debug) console.debug(await page.evaluate(() => [(({ width, height, availWidth, availHeight }) => ({ width, height, availWidth, availHeight }))(window.screen), navigator.userAgent, navigator.platform, navigator.vendor])); // deconstruct screen needed since `window.screen` prints {}, `window.screen.toString()` '[object Screen]', and can't use some pick function without defining it on `page`
if (cfg.debug_network) {
// const filter = _ => true;
const filter = r => r.url().includes('store.epicgames.com');
@@ -87,19 +98,26 @@ try {
if (!email) await notifyBrowserLogin();
else {
// await page.click('text=Sign in with Epic Games');
- await page.fill('#email', email);
- await page.click('button[type="submit"]');
page.waitForSelector('.h_captcha_challenge iframe').then(async () => {
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.');
await notify('epic-games: got captcha during login. Please check.');
}).catch(_ => { });
page.waitForSelector('p:has-text("Incorrect response.")').then(async () => {
- console.error('Incorrect repsonse for captcha!');
+ console.error('Incorrect response for captcha!');
}).catch(_ => { });
+ await page.fill('#email', email);
+ // await page.click('button[type="submit"]'); login was split in two steps for some time, now email and password are on the same form again
const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' }));
if (!password) await notifyBrowserLogin();
- await page.fill('#password', password);
- await page.click('button[type="submit"]');
+ else {
+ await page.fill('#password', password);
+ await page.click('button[type="submit"]');
+ }
+ const error = page.locator('#form-error-message');
+ error.waitFor().then(async () => {
+ console.error('Login error:', await error.innerText());
+ console.log('Please login in the browser!');
+ }).catch(_ => { });
// handle MFA, but don't await it
page.waitForURL('**/id/login/mfa**').then(async () => {
console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...');
@@ -138,31 +156,52 @@ try {
for (const url of urls) {
if (cfg.time) console.time('claim game');
await page.goto(url); // , { waitUntil: 'domcontentloaded' });
- const btnText = await page.locator('//button[@data-testid="purchase-cta-button"][not(contains(.,"Loading"))]').first().innerText(); // barrier to block until page is loaded
+ const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"] >> :has-text("e"), :has-text("i")').first(); // when loading, the button text is empty -> need to wait for some text {'get', 'in library', 'requires base game'} -> just wait for e or i to not be too specific; :text-matches("\w+") somehow didn't work - https://github.com/vogler/free-games-claimer/issues/375
+ await purchaseBtn.waitFor();
+ const btnText = (await purchaseBtn.innerText()).toLowerCase(); // barrier to block until page is loaded
// click Continue if 'This game contains mature content recommended only for ages 18+'
if (await page.locator('button:has-text("Continue")').count() > 0) {
console.log(' This game contains mature content recommended only for ages 18+');
if (await page.locator('[data-testid="AgeSelect"]').count()) {
console.error(' Got "To continue, please provide your date of birth" - This shouldn\'t happen due to cookie set above. Please report to https://github.com/vogler/free-games-claimer/issues/275');
+ await page.locator('#month_toggle').click();
+ await page.locator('#month_menu li:has-text("01")').click();
+ await page.locator('#day_toggle').click();
+ await page.locator('#day_menu li:has-text("01")').click();
+ await page.locator('#year_toggle').click();
+ await page.locator('#year_menu li:has-text("1987")').click();
}
await page.click('button:has-text("Continue")', { delay: 111 });
await page.waitForTimeout(2000);
}
- const title = await page.locator('h1').first().innerText();
+ let title;
+ let bundle_includes;
+ if (await page.locator('span:text-is("About Bundle")').count()) {
+ title = (await page.locator('span:has-text("Buy"):left-of([data-testid="purchase-cta-button"])').first().innerText()).replace('Buy ', '');
+ // h1 first didn't exist for bundles but now it does... However h1 would e.g. be 'FalloutĀ® Classic Collection' instead of 'Fallout Classic Collection'
+ try {
+ bundle_includes = await Promise.all((await page.locator('.product-card-top-row h5').all()).map(b => b.innerText()));
+ } catch (e) {
+ console.error('Failed to get "Bundle Includes":', e);
+ }
+ } else {
+ title = await page.locator('h1').first().innerText();
+ }
const game_id = page.url().split('/').pop();
db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only!
console.log('Current free game:', title);
+ if (bundle_includes) console.log(' This bundle includes:', bundle_includes);
const notify_game = { title, url, status: 'failed' };
notify_games.push(notify_game); // status is updated below
- if (btnText.toLowerCase() == 'in library') {
+ if (btnText == 'in library') {
console.log(' Already in library! Nothing to claim.');
notify_game.status = 'existed';
db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed
if (db.data[user][game_id].status.startsWith('failed')) db.data[user][game_id].status = 'manual'; // was failed but now it's claimed
- } else if (btnText.toLowerCase() == 'requires base game') {
+ } else if (btnText == 'requires base game') {
console.log(' Requires base game! Nothing to claim.');
notify_game.status = 'requires base game';
db.data[user][game_id].status ||= 'failed:requires-base-game';
@@ -170,9 +209,12 @@ try {
const baseUrl = 'https://store.epicgames.com' + await page.locator('a:has-text("Overview")').getAttribute('href');
console.log(' Base game:', baseUrl);
// await page.click('a:has-text("Overview")');
+ // TODO handle this via function call for base game above since this will never terminate if DRYRUN=1
+ urls.push(baseUrl); // add base game to the list of games to claim
+ urls.push(url); // add add-on itself again
} else { // GET
- console.log(' Not in library yet! Click GET.');
- await page.click('[data-testid="purchase-cta-button"]', { delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough
+ console.log(' Not in library yet! Click', btnText);
+ await purchaseBtn.click({ delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough
// click Continue if 'Device not supported. This product is not compatible with your current device.' - avoided by Windows userAgent?
page.click('button:has-text("Continue")').catch(_ => { }); // needed since change from Chromium to Firefox?
@@ -181,9 +223,11 @@ try {
page.click('button:has-text("Yes, buy now")').catch(_ => { });
// Accept End User License Agreement (only needed once)
- page.locator('input#agree').waitFor().then(async () => {
- console.log('Accept End User License Agreement (only needed once)');
- await page.locator('input#agree').check();
+ page.locator(':has-text("end user license agreement")').waitFor().then(async () => {
+ console.log(' Accept End User License Agreement (only needed once)');
+ console.log(page.innerHTML);
+ console.log('Please report the HTML above here: https://github.com/vogler/free-games-claimer/issues/371');
+ await page.locator('input#agree').check(); // TODO Bundle: got stuck here; likely unrelated to bundle and locator just changed: https://github.com/vogler/free-games-claimer/issues/371
await page.locator('button:has-text("Accept")').click();
}).catch(_ => { });
@@ -219,7 +263,7 @@ try {
await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 });
// I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872
- const btnAgree = iframe.locator('button:has-text("I Agree")');
+ const btnAgree = iframe.locator('button:has-text("I Accept")');
btnAgree.waitFor().then(() => btnAgree.click()).catch(_ => { }); // EU: wait for and click 'I Agree'
try {
// context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s?
@@ -234,7 +278,11 @@ try {
// console.info(' Saved a screenshot of hcaptcha challenge to', p);
// console.error(' Got hcaptcha challenge. To avoid it, get a link from https://www.hcaptcha.com/accessibility'); // TODO save this link in config and visit it daily to set accessibility cookie to avoid captcha challenge?
}).catch(_ => { }); // may time out if not shown
- await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' });
+ iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor().then(async () => {
+ console.error(' Failed to challenge captcha, please try again later.');
+ await notify('epic-games: failed to challenge captcha. Please check.');
+ }).catch(_ => { });
+ await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); // TODO Bundle: got stuck here, but normal game now as well
db.data[user][game_id].status = 'claimed';
db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time
console.log(' Claimed successfully!');
diff --git a/eslint.config.js b/eslint.config.js
index bc6447fa..48b1cbc7 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -24,6 +24,7 @@ export default [
// https://eslint.style/packages/js
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ 'prefer-const': 'error',
'@stylistic/js/array-bracket-newline': ['error', 'consistent'],
'@stylistic/js/array-bracket-spacing': 'error',
'@stylistic/js/array-element-newline': ['error', 'consistent'],
diff --git a/gog.js b/gog.js
index 048e04a2..06886262 100644
--- a/gog.js
+++ b/gog.js
@@ -10,13 +10,18 @@ console.log(datetime(), 'started checking gog');
const db = await jsonDb('gog.json', {});
+if (cfg.width < 1280) { // otherwise 'Sign in' and #menuUsername are hidden (but attached to DOM), see https://github.com/vogler/free-games-claimer/issues/335
+ console.error(`Window width is set to ${cfg.width} but needs to be at least 1280 for GOG!`);
+ process.exit(1);
+}
+
// https://playwright.dev/docs/auth#multi-factor-authentication
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height },
locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
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/gog-${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/gog-${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
});
@@ -25,6 +30,7 @@ handleSIGINT(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
+await page.setViewportSize({ width: cfg.width, height: cfg.height }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
const notify_games = [];
@@ -92,10 +98,9 @@ try {
if (!await banner.count()) {
console.log('Currently no free giveaway!');
} else {
- const text = await page.locator('.giveaway-banner__title').innerText();
- const title = text.match(/Claim (.*)/)[1];
- const slug = await banner.getAttribute('href');
- const url = `https://gog.com${slug}`;
+ const text = await page.locator('.giveaway__content-header').innerText();
+ const title = text.match(/Claim (.*) and don't miss the/)[1];
+ const url = await banner.locator('a').first().getAttribute('href');
console.log(`Current free game: ${title} - ${url}`);
db.data[user][title] ||= { title, time: datetime(), url };
if (cfg.dryrun) process.exit(1);
diff --git a/package-lock.json b/package-lock.json
index 930e7bf7..e6f66b28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,19 +11,19 @@
"dependencies": {
"chalk": "^5.3.0",
"cross-env": "^7.0.3",
- "dotenv": "^16.3.1",
+ "dotenv": "^16.4.5",
"enquirer": "^2.4.1",
- "lowdb": "^6.1.1",
+ "lowdb": "^7.0.1",
"otplib": "^12.0.1",
- "playwright-firefox": "^1.40.1",
+ "playwright-firefox": "^1.45.0",
"puppeteer-extra-plugin-stealth": "^2.11.2"
},
"devDependencies": {
- "@stylistic/eslint-plugin-js": "^1.5.1",
- "eslint": "^8.56.0"
+ "@stylistic/eslint-plugin-js": "^2.2.2",
+ "eslint": "^9.5.0"
},
"engines": {
- "node": ">=15"
+ "node": ">=17"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -59,16 +59,32 @@
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
+ "node_modules/@eslint/config-array": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz",
+ "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.4",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@eslint/eslintrc": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
- "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+ "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.6.0",
- "globals": "^13.19.0",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@@ -76,33 +92,30 @@
"strip-json-comments": "^3.1.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/js": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
- "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz",
+ "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@humanwhocodes/config-array": {
- "version": "0.11.13",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
- "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
"dev": true,
- "dependencies": {
- "@humanwhocodes/object-schema": "^2.0.1",
- "debug": "^4.1.1",
- "minimatch": "^3.0.5"
- },
+ "license": "Apache-2.0",
"engines": {
- "node": ">=10.10.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
@@ -118,11 +131,19 @@
"url": "https://github.com/sponsors/nzakas"
}
},
- "node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
- "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
- "dev": true
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
+ "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@@ -202,23 +223,37 @@
}
},
"node_modules/@stylistic/eslint-plugin-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.1.tgz",
- "integrity": "sha512-iZF0rF+uOhAmOJYOJx1Yvmm3CZ1uz9n0SRd9dpBYHA3QAvfABUORh9LADWwZCigjHJkp2QbCZelGFJGwGz7Siw==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.2.2.tgz",
+ "integrity": "sha512-Vj2Q1YHVvJw+ThtOvmk5Yx7wZanVrIBRUTT89horLDb4xdP9GA1um9XOYQC6j67VeUC2gjZQnz5/RVJMzaOhtw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "acorn": "^8.11.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1"
+ "@types/eslint": "^8.56.10",
+ "acorn": "^8.11.3",
+ "eslint-visitor-keys": "^4.0.0",
+ "espree": "^10.0.1"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"peerDependencies": {
"eslint": ">=8.40.0"
}
},
+ "node_modules/@stylistic/eslint-plugin-js/node_modules/eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
@@ -227,22 +262,42 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/eslint": {
+ "version": "8.56.10",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",
+ "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
- "node_modules/@ungap/structured-clone": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
- "dev": true
- },
"node_modules/acorn": {
- "version": "8.11.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
- "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
+ "license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -255,6 +310,7 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
+ "license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -264,6 +320,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -310,7 +367,8 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "dev": true,
+ "license": "Python-2.0"
},
"node_modules/arr-union": {
"version": "3.1.0",
@@ -339,6 +397,7 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -452,27 +511,16 @@
"node": ">=0.10.0"
}
},
- "node_modules/doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/dotenv": {
- "version": "16.3.1",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
- "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
- "url": "https://github.com/motdotla/dotenv?sponsor=1"
+ "url": "https://dotenvx.com"
}
},
"node_modules/enquirer": {
@@ -500,41 +548,38 @@
}
},
"node_modules/eslint": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
- "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz",
+ "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
- "@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.56.0",
- "@humanwhocodes/config-array": "^0.11.13",
+ "@eslint/config-array": "^0.16.0",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "9.5.0",
"@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
- "@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
- "doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.2.2",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1",
- "esquery": "^1.4.2",
+ "eslint-scope": "^8.0.1",
+ "eslint-visitor-keys": "^4.0.0",
+ "espree": "^10.0.1",
+ "esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^6.0.1",
+ "file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
- "globals": "^13.19.0",
- "graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
- "js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
@@ -548,23 +593,24 @@
"eslint": "bin/eslint.js"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "url": "https://eslint.org/donate"
}
},
"node_modules/eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz",
+ "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -598,18 +644,45 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/espree": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
- "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
+ "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "acorn": "^8.9.0",
+ "acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.1"
+ "eslint-visitor-keys": "^4.0.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -632,6 +705,7 @@
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -653,6 +727,7 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -661,13 +736,15 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
@@ -685,15 +762,16 @@
}
},
"node_modules/file-entry-cache": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
- "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "flat-cache": "^3.0.4"
+ "flat-cache": "^4.0.0"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": ">=16.0.0"
}
},
"node_modules/find-up": {
@@ -713,24 +791,25 @@
}
},
"node_modules/flat-cache": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
- "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
- "keyv": "^4.5.3",
- "rimraf": "^3.0.2"
+ "keyv": "^4.5.4"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": ">=16"
}
},
"node_modules/flatted": {
- "version": "3.2.9",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
- "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
- "dev": true
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/for-in": {
"version": "1.0.2",
@@ -801,15 +880,13 @@
}
},
"node_modules/globals": {
- "version": "13.24.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
- "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
- "dependencies": {
- "type-fest": "^0.20.2"
- },
+ "license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -820,12 +897,6 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true
- },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -836,10 +907,11 @@
}
},
"node_modules/ignore": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
- "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 4"
}
@@ -849,6 +921,7 @@
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -955,6 +1028,7 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -966,13 +1040,15 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -996,6 +1072,7 @@
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
@@ -1054,14 +1131,15 @@
"dev": true
},
"node_modules/lowdb": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-6.1.1.tgz",
- "integrity": "sha512-HO13FCxI8SCwfj2JRXOKgXggxnmfSc+l0aJsZ5I34X3pwzG/DPBSKyKu3Zkgg/pNmx854SVgE2la0oUeh6wzNw==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
+ "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
+ "license": "MIT",
"dependencies": {
- "steno": "^3.1.1"
+ "steno": "^4.0.2"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
@@ -1192,6 +1270,7 @@
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
@@ -1225,29 +1304,31 @@
}
},
"node_modules/playwright-core": {
- "version": "1.40.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz",
- "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==",
+ "version": "1.45.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz",
+ "integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==",
+ "license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
}
},
"node_modules/playwright-firefox": {
- "version": "1.40.1",
- "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.40.1.tgz",
- "integrity": "sha512-+C5eOWZv/CvALM0yBZFSYThvUzGKusNw6soDMhTEJwDUB5i9q/yZVFkj6I8CFXM4U6Pf1q0PXHMca70HoIwnCQ==",
+ "version": "1.45.0",
+ "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.45.0.tgz",
+ "integrity": "sha512-JmGESfFR8xTjAYQzECYO00yBbSSnu4dBImsrmJVeOXTvT+i9p1dpVUaxKz6lTFMI/xzYROqB4E4Km8NBiOgslw==",
"hasInstallScript": true,
+ "license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.40.1"
+ "playwright-core": "1.45.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
}
},
"node_modules/prelude-ls": {
@@ -1264,6 +1345,7 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -1395,6 +1477,7 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=4"
}
@@ -1499,11 +1582,12 @@
}
},
"node_modules/steno": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/steno/-/steno-3.1.1.tgz",
- "integrity": "sha512-B7c6EVH7oEiaMRW36SjUnktkDwp/qd4pQiduylyiqvcZEZDeX0IIFZRBZdwO/RaVo60M0wkDwC0e8yeKaR4VGg==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz",
+ "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==",
+ "license": "MIT",
"engines": {
- "node": ">=16"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
@@ -1525,6 +1609,7 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
},
@@ -1570,18 +1655,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -1595,6 +1668,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
@@ -1653,16 +1727,27 @@
"integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
"dev": true
},
+ "@eslint/config-array": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz",
+ "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==",
+ "dev": true,
+ "requires": {
+ "@eslint/object-schema": "^2.1.4",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ }
+ },
"@eslint/eslintrc": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
- "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+ "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.6.0",
- "globals": "^13.19.0",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@@ -1671,21 +1756,16 @@
}
},
"@eslint/js": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
- "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz",
+ "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==",
"dev": true
},
- "@humanwhocodes/config-array": {
- "version": "0.11.13",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
- "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
- "dev": true,
- "requires": {
- "@humanwhocodes/object-schema": "^2.0.1",
- "debug": "^4.1.1",
- "minimatch": "^3.0.5"
- }
+ "@eslint/object-schema": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+ "dev": true
},
"@humanwhocodes/module-importer": {
"version": "1.0.1",
@@ -1693,10 +1773,10 @@
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true
},
- "@humanwhocodes/object-schema": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
- "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+ "@humanwhocodes/retry": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
+ "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
"dev": true
},
"@nodelib/fs.scandir": {
@@ -1768,15 +1848,23 @@
}
},
"@stylistic/eslint-plugin-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.1.tgz",
- "integrity": "sha512-iZF0rF+uOhAmOJYOJx1Yvmm3CZ1uz9n0SRd9dpBYHA3QAvfABUORh9LADWwZCigjHJkp2QbCZelGFJGwGz7Siw==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.2.2.tgz",
+ "integrity": "sha512-Vj2Q1YHVvJw+ThtOvmk5Yx7wZanVrIBRUTT89horLDb4xdP9GA1um9XOYQC6j67VeUC2gjZQnz5/RVJMzaOhtw==",
"dev": true,
"requires": {
- "acorn": "^8.11.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1"
+ "@types/eslint": "^8.56.10",
+ "acorn": "^8.11.3",
+ "eslint-visitor-keys": "^4.0.0",
+ "espree": "^10.0.1"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "dev": true
+ }
}
},
"@types/debug": {
@@ -1787,21 +1875,37 @@
"@types/ms": "*"
}
},
+ "@types/eslint": {
+ "version": "8.56.10",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",
+ "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==",
+ "dev": true,
+ "requires": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "@types/estree": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "dev": true
+ },
+ "@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
- "@ungap/structured-clone": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
- "dev": true
- },
"acorn": {
- "version": "8.11.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
- "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true
},
"acorn-jsx": {
@@ -1947,19 +2051,10 @@
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
},
- "doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2"
- }
- },
"dotenv": {
- "version": "16.3.1",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
- "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ=="
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
},
"enquirer": {
"version": "2.4.1",
@@ -1977,41 +2072,37 @@
"dev": true
},
"eslint": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
- "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz",
+ "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
- "@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.56.0",
- "@humanwhocodes/config-array": "^0.11.13",
+ "@eslint/config-array": "^0.16.0",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "9.5.0",
"@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
- "@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
- "doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.2.2",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1",
- "esquery": "^1.4.2",
+ "eslint-scope": "^8.0.1",
+ "eslint-visitor-keys": "^4.0.0",
+ "espree": "^10.0.1",
+ "esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^6.0.1",
+ "file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
- "globals": "^13.19.0",
- "graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
- "js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
@@ -2031,13 +2122,19 @@
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
+ },
+ "eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "dev": true
}
}
},
"eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz",
+ "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==",
"dev": true,
"requires": {
"esrecurse": "^4.3.0",
@@ -2051,14 +2148,22 @@
"dev": true
},
"espree": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
- "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
+ "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
"requires": {
- "acorn": "^8.9.0",
+ "acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.1"
+ "eslint-visitor-keys": "^4.0.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "dev": true
+ }
}
},
"esquery": {
@@ -2119,12 +2224,12 @@
}
},
"file-entry-cache": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
- "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"requires": {
- "flat-cache": "^3.0.4"
+ "flat-cache": "^4.0.0"
}
},
"find-up": {
@@ -2138,20 +2243,19 @@
}
},
"flat-cache": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
- "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"requires": {
"flatted": "^3.2.9",
- "keyv": "^4.5.3",
- "rimraf": "^3.0.2"
+ "keyv": "^4.5.4"
}
},
"flatted": {
- "version": "3.2.9",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
- "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
"for-in": {
@@ -2205,25 +2309,16 @@
}
},
"globals": {
- "version": "13.24.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
- "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
- "dev": true,
- "requires": {
- "type-fest": "^0.20.2"
- }
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true
},
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
- "graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true
- },
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2231,9 +2326,9 @@
"dev": true
},
"ignore": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
- "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true
},
"import-fresh": {
@@ -2399,11 +2494,11 @@
"dev": true
},
"lowdb": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-6.1.1.tgz",
- "integrity": "sha512-HO13FCxI8SCwfj2JRXOKgXggxnmfSc+l0aJsZ5I34X3pwzG/DPBSKyKu3Zkgg/pNmx854SVgE2la0oUeh6wzNw==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
+ "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
"requires": {
- "steno": "^3.1.1"
+ "steno": "^4.0.2"
}
},
"merge-deep": {
@@ -2527,16 +2622,16 @@
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
},
"playwright-core": {
- "version": "1.40.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz",
- "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ=="
+ "version": "1.45.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz",
+ "integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ=="
},
"playwright-firefox": {
- "version": "1.40.1",
- "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.40.1.tgz",
- "integrity": "sha512-+C5eOWZv/CvALM0yBZFSYThvUzGKusNw6soDMhTEJwDUB5i9q/yZVFkj6I8CFXM4U6Pf1q0PXHMca70HoIwnCQ==",
+ "version": "1.45.0",
+ "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.45.0.tgz",
+ "integrity": "sha512-JmGESfFR8xTjAYQzECYO00yBbSSnu4dBImsrmJVeOXTvT+i9p1dpVUaxKz6lTFMI/xzYROqB4E4Km8NBiOgslw==",
"requires": {
- "playwright-core": "1.40.1"
+ "playwright-core": "1.45.0"
}
},
"prelude-ls": {
@@ -2668,9 +2763,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"steno": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/steno/-/steno-3.1.1.tgz",
- "integrity": "sha512-B7c6EVH7oEiaMRW36SjUnktkDwp/qd4pQiduylyiqvcZEZDeX0IIFZRBZdwO/RaVo60M0wkDwC0e8yeKaR4VGg=="
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz",
+ "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A=="
},
"strip-ansi": {
"version": "6.0.1",
@@ -2715,12 +2810,6 @@
"prelude-ls": "^1.2.1"
}
},
- "type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true
- },
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
diff --git a/package.json b/package.json
index 6c954cdd..2c64f46b 100644
--- a/package.json
+++ b/package.json
@@ -17,20 +17,20 @@
},
"type": "module",
"engines": {
- "node": ">=15"
+ "node": ">=17"
},
"dependencies": {
"chalk": "^5.3.0",
"cross-env": "^7.0.3",
- "dotenv": "^16.3.1",
+ "dotenv": "^16.4.5",
"enquirer": "^2.4.1",
- "lowdb": "^6.1.1",
+ "lowdb": "^7.0.1",
"otplib": "^12.0.1",
- "playwright-firefox": "^1.40.1",
+ "playwright-firefox": "^1.45.0",
"puppeteer-extra-plugin-stealth": "^2.11.2"
},
"devDependencies": {
- "@stylistic/eslint-plugin-js": "^1.5.1",
- "eslint": "^8.56.0"
+ "@stylistic/eslint-plugin-js": "^2.2.2",
+ "eslint": "^9.5.0"
}
}
diff --git a/prime-gaming.js b/prime-gaming.js
index 7b7f18b2..adfdbd44 100644
--- a/prime-gaming.js
+++ b/prime-gaming.js
@@ -19,7 +19,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, {
viewport: { width: cfg.width, height: cfg.height },
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
- recordHar: cfg.record ? { path: `data/record/pg-${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/pg-${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
});
@@ -31,6 +31,7 @@ await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
+await page.setViewportSize({ width: cfg.width, height: cfg.height }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
const notify_games = [];
@@ -52,6 +53,7 @@ try {
const password = email && (cfg.pg_password || await prompt({ type: 'password', message: 'Enter password' }));
if (email && password) {
await page.fill('[name=email]', email);
+ await page.click('input[type="submit"]');
await page.fill('[name=password]', password);
await page.check('[name=rememberMe]');
await page.click('input[type="submit"]');
@@ -96,27 +98,70 @@ try {
process.exit(1);
}
+ const waitUntilStable = async (f, act) => {
+ let v;
+ while (true) {
+ const v2 = await f();
+ console.log('waitUntilStable', v2);
+ if (v == v2) break;
+ v = v2;
+ await act();
+ }
+ };
+ const scrollUntilStable = async f => await waitUntilStable(f, async () => {
+ // await page.keyboard.press('End'); // scroll to bottom to show all games
+ // loading all games became flaky; see https://github.com/vogler/free-games-claimer/issues/357
+ await page.keyboard.press('PageDown'); // scrolling to straight to the bottom started to skip loading some games
+ await page.waitForLoadState('networkidle'); // wait for all games to be loaded
+ await page.waitForTimeout(3000); // TODO networkidle wasn't enough to load all already collected games
+ // do it again since once wasn't enough...
+ await page.keyboard.press('PageDown');
+ await page.waitForTimeout(3000);
+ });
+
await page.click('button[data-type="Game"]');
- await page.keyboard.press('End'); // scroll to bottom to show all games
- await page.waitForLoadState('networkidle'); // wait for all games to be loaded
- await page.waitForTimeout(2000); // TODO networkidle wasn't enough to load all already collected games
const games = page.locator('div[data-a-target="offer-list-FGWP_FULL"]');
await games.waitFor();
+ // await scrollUntilStable(() => games.locator('.item-card__action').count()); // number of games
+ await scrollUntilStable(() => page.evaluate(() => document.querySelector('.tw-full-width').scrollHeight)); // height may change during loading while number of games is still the same?
console.log('Number of already claimed games (total):', await games.locator('p:has-text("Collected")').count());
// can't use .all() since the list of elements via locator will change after click while we iterate over it
- const internal = await games.locator('.item-card__action:has([data-a-target="FGWPOffer"])').elementHandles();
- const external = await games.locator('.item-card__action:has([data-a-target="ExternalOfferClaim"])').all();
+ const internal = await games.locator('.item-card__action:has(button[data-a-target="FGWPOffer"])').elementHandles();
+ const external = await games.locator('.item-card__action:has(a[data-a-target="FGWPOffer"])').all();
+ // bottom to top: oldest to newest games
+ internal.reverse();
+ external.reverse();
+ const checkTimeLeft = async url => {
+ // console.log(' Checking time left for game:', url);
+ const check = async p => {
+ console.log(' ', await p.locator('.availability-date').innerText());
+ const dueDateOrg = await p.locator('.availability-date .tw-bold').innerText();
+ const dueDate = datetime(new Date(Date.parse(dueDateOrg + ' 17:00')));
+ console.log(' Due date:', dueDate);
+ };
+ if (page.url() == url) {
+ await check(page);
+ } else {
+ const p = await context.newPage();
+ await p.goto(url, { waitUntil: 'domcontentloaded' });
+ await check(p);
+ p.close();
+ }
+ };
console.log('Number of free unclaimed games (Prime Gaming):', internal.length);
// claim games in internal store
for (const card of internal) {
await card.scrollIntoViewIfNeeded();
const title = await (await card.$('.item-card-details__body__primary')).innerText();
+ const slug = await (await card.$('a')).getAttribute('href');
+ const url = 'https://gaming.amazon.com' + slug.split('?')[0];
console.log('Current free game:', title);
+ if (cfg.pg_timeLeft) await checkTimeLeft(url);
if (cfg.dryrun) continue;
if (cfg.interactive && !await confirm()) continue;
await (await card.$('.tw-button:has-text("Claim")')).click();
- db.data[user][title] ||= { title, time: datetime(), store: 'internal' };
- notify_games.push({ title, status: 'claimed', url: URL_CLAIM });
+ db.data[user][title] ||= { title, time: datetime(), url, store: 'internal' };
+ notify_games.push({ title, status: 'claimed', url });
// const img = await (await card.$('img.tw-image')).getAttribute('src');
// console.log('Image:', img);
await card.screenshot({ path: screenshot('internal', `${filenamify(title)}.png`) });
@@ -131,34 +176,18 @@ try {
// await (await card.$('text=Claim')).click(); // goes to URL of game, no need to wait
external_info.push({ title, url });
}
+ // external_info = [ { title: 'Fallout 76 (XBOX)', url: 'https://gaming.amazon.com/fallout-76-xbox-fgwp/dp/amzn1.pg.item.9fe17d7b-b6c2-4f58-b494-cc4e79528d0b?ingress=amzn&ref_=SM_Fallout76XBOX_S01_FGWP_CRWN' } ];
for (const { title, url } of external_info) {
console.log('Current free game:', title); // , url);
await page.goto(url, { waitUntil: 'domcontentloaded' });
if (cfg.debug) await page.pause();
+ const item_text = await page.innerText('[data-a-target="DescriptionItemDetails"]');
+ const store = item_text.toLowerCase().replace(/.* on /, '').slice(0, -1);
+ console.log(' External store:', store);
+ if (cfg.pg_timeLeft) await checkTimeLeft(url);
if (cfg.dryrun) continue;
if (cfg.interactive && !await confirm()) continue;
- await Promise.any([page.click('.tw-button:has-text("Get game")'), page.click('.tw-button:has-text("Claim")'), page.click('.tw-button:has-text("Complete Claim")'), page.waitForSelector('div:has-text("Link game account")'), page.waitForSelector('.thank-you-title:has-text("Success")')]); // waits for navigation
-
- // TODO would be simpler than the below, but will block for linked stores without code
- // const redeem_text = await page.textContent('text=/ code on /'); // FAQ: How do I redeem my code?
- // console.log(' ', redeem_text);
- // // Before July 29, 2023, redeem your offer code on GOG.com.
- // // Before July 1, 2023, redeem your product code on Legacy Games.
- // let store = redeem_text.toLowerCase().replace(/.* on /, '').slice(0, -1);
-
- let store = '';
- const store_text = await page.$('[data-a-target="hero-header-subtitle"]'); // worked fine for every store, but now no longer works for gog.com
- if (store_text) { // legacy games, ?
- const store_texts = await store_text.innerText();
- // Full game for PC [and MAC] on: Legacy Games, Origin, EPIC GAMES, Battle.net; alt: 3 Full PC Games on Legacy Games
- store = store_texts.toLowerCase().replace(/.* on /, '');
- } else { // gog.com, ?
- // $('[data-a-target="DescriptionItemDetails"]').innerText is e.g. 'Prey for PC on GOG.com.' but does not work for Legacy Games
- const item_text = await page.innerText('[data-a-target="DescriptionItemDetails"]');
- store = item_text.toLowerCase().replace(/.* on /, '').slice(0, -1);
- }
- console.log(' External store:', store);
-
+ await Promise.any([page.click('[data-a-target="buy-box"] .tw-button:has-text("Get game")'), page.click('[data-a-target="buy-box"] .tw-button:has-text("Claim")'), page.click('.tw-button:has-text("Complete Claim")'), page.waitForSelector('div:has-text("Link game account")'), page.waitForSelector('.thank-you-title:has-text("Success")')]); // waits for navigation
db.data[user][title] ||= { title, time: datetime(), url, store };
const notify_game = { title, url };
notify_games.push(notify_game); // status is updated below
@@ -178,7 +207,8 @@ try {
const redeem = {
// 'origin': 'https://www.origin.com/redeem', // TODO still needed or now only via account linking?
'gog.com': 'https://www.gog.com/redeem',
- 'microsoft games': 'https://redeem.microsoft.com',
+ 'microsoft store': 'https://account.microsoft.com/billing/redeem',
+ xbox: 'https://account.microsoft.com/billing/redeem',
'legacy games': 'https://www.legacygames.com/primedeal',
};
if (store in redeem) { // did not work for linked origin: && !await page.locator('div:has-text("Successfully Claimed")').count()
@@ -187,7 +217,9 @@ try {
if (store == 'legacy games') { // may be different URL like https://legacygames.com/primeday/puzzleoftheyear/
redeem[store] = await (await page.$('li:has-text("Click here") a')).getAttribute('href'); // full text: Click here to enter your redemption code.
}
- console.log(' URL to redeem game:', redeem[store]);
+ let redeem_url = redeem[store];
+ if (store == 'gog.com') redeem_url += '/' + code; // to log and notify, but can't use for goto below (captcha)
+ console.log(' URL to redeem game:', redeem_url);
db.data[user][title].code = code;
let redeem_action = 'redeem';
if (cfg.pg_redeem) { // try to redeem keys on external stores
@@ -233,33 +265,50 @@ try {
console.log(' Unknown Response 2 - please report in https://github.com/vogler/free-games-claimer/issues/5');
}
}
- } else if (store == 'microsoft games') {
- console.error(` Redeem on ${store} not yet implemented!`);
+ } else if (store == 'microsoft store' || store == 'xbox') {
+ console.error(` Redeem on ${store} is experimental!`);
+ // await page2.pause();
if (page2.url().startsWith('https://login.')) {
- console.error(' Not logged in! Use the browser to login manually.');
+ console.error(' Not logged in! Use the browser to login manually. Waiting for 60s.');
+ await page2.waitForTimeout(60 * 1000);
redeem_action = 'redeem (login)';
} else {
- const r = page2.waitForResponse(r => r.url().startsWith('https://purchase.mp.microsoft.com/'));
- await page2.fill('[name=tokenString]', code);
+ const iframe = page2.frameLocator('#redeem-iframe');
+ const input = iframe.locator('[name=tokenString]');
+ await input.waitFor();
+ await input.fill(code);
+ const r = page2.waitForResponse(r => r.url().startsWith('https://cart.production.store-web.dynamics.com/v1.0/Redeem/PrepareRedeem'));
// console.log(await page2.locator('.redeem_code_error').innerText());
const rt = await (await r).text();
- console.debug(` Response: ${rt}`);
// {"code":"NotFound","data":[],"details":[],"innererror":{"code":"TokenNotFound",...
- const reason = JSON.parse(rt).code;
- if (reason == 'NotFound') {
+ const j = JSON.parse(rt);
+ const reason = j?.events?.cart.length && j.events.cart[0]?.data?.reason;
+ if (reason == 'TokenNotFound') {
redeem_action = 'redeem (not found)';
console.error(' Code was not found!');
+ } else if (j?.productInfos?.length && j.productInfos[0]?.redeemable) {
+ await iframe.locator('button:has-text("Next")').click();
+ await iframe.locator('button:has-text("Confirm")').click();
+ const r = page2.waitForResponse(r => r.url().startsWith('https://cart.production.store-web.dynamics.com/v1.0/Redeem/RedeemToken'));
+ const j = JSON.parse(await (await r).text());
+ if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') {
+ redeem_action = 'already redeemed';
+ console.error(' error: UserAlreadyOwnsContent');
+ } else if (true) { // TODO what's returned on success?
+ redeem_action = 'redeemed';
+ db.data[user][title].status = 'claimed and redeemed?';
+ console.log(' Redeemed successfully? Please report if not in https://github.com/vogler/free-games-claimer/issues/5');
+ }
} else { // TODO find out other responses
- await page2.click('#nextButton');
- redeem_action = 'redeemed?';
+ redeem_action = 'unknown';
+ console.debug(` Response: ${rt}`);
console.log(' Redeemed successfully? Please report your Response from above (if it is new) in https://github.com/vogler/free-games-claimer/issues/5');
- db.data[user][title].status = 'claimed and redeemed?';
}
}
} else if (store == 'legacy games') {
await page2.fill('[name=coupon_code]', code);
- await page2.fill('[name=email]', cfg.pg_email); // TODO option for sep. email?
- await page2.fill('[name=email_validate]', cfg.pg_email);
+ await page2.fill('[name=email]', cfg.lg_email);
+ await page2.fill('[name=email_validate]', cfg.lg_email);
await page2.uncheck('[name=newsletter_sub]');
await page2.click('[type="submit"]');
try {
@@ -279,7 +328,7 @@ try {
if (cfg.debug) await page2.pause();
await page2.close();
}
- notify_game.status = `${redeem_action} ${code} on ${store}`;
+ notify_game.status = `${redeem_action} ${code} on ${store}`;
} else {
notify_game.status = `claimed on ${store}`;
db.data[user][title].status = 'claimed';
@@ -296,8 +345,7 @@ try {
if (notify_games.length) { // make screenshot of all games if something was claimed
const p = screenshot(`${filenamify(datetime())}.png`);
// await page.screenshot({ path: p, fullPage: true }); // fullPage does not make a difference since scroll not on body but on some element
- await page.keyboard.press('End'); // scroll to bottom to show all games
- await page.waitForTimeout(1000); // wait for fade in animation
+ await scrollUntilStable(() => games.locator('.item-card__action').count());
const viewportSize = page.viewportSize(); // current viewport size
await page.setViewportSize({ ...viewportSize, height: 3000 }); // increase height, otherwise element screenshot is cut off at the top and bottom
await games.screenshot({ path: p }); // screenshot of all claimed games
@@ -311,17 +359,7 @@ try {
await loot.waitFor();
process.stdout.write('Loading all DLCs on page...');
- let n1 = 0;
- let n2 = 0;
- do {
- n1 = n2;
- n2 = await loot.locator('[data-a-target="item-card"]').count();
- // console.log(n2);
- process.stdout.write(` ${n2}`);
- await page.keyboard.press('End'); // scroll to bottom to show all dlcs
- await page.waitForLoadState('networkidle'); // did not wait for dlcs to be loaded
- await page.waitForTimeout(1000);
- } while (n2 > n1);
+ await scrollUntilStable(() => loot.locator('[data-a-target="item-card"]').count())
console.log('\nNumber of already claimed DLC:', await loot.locator('p:has-text("Collected")').count());
diff --git a/src/config.js b/src/config.js
index bd41c7d1..4a384b89 100644
--- a/src/config.js
+++ b/src/config.js
@@ -41,12 +41,13 @@ export const cfg = {
gog_email: process.env.GOG_EMAIL || process.env.EMAIL,
gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD,
gog_newsletter: process.env.GOG_NEWSLETTER == '1', // do not unsubscribe from newsletter after claiming a game
+ // auth AliExpress
+ ae_email: process.env.AE_EMAIL || process.env.EMAIL,
+ ae_password: process.env.AE_PASSWORD || process.env.PASSWORD,
// OTP only via GOG_EMAIL, can't add app...
- // auth xbox
- xbox_email: process.env.XBOX_EMAIL || process.env.EMAIL,
- xbox_password: process.env.XBOX_PASSWORD || process.env.PASSWORD,
- xbox_otpkey: process.env.XBOX_OTPKEY,
- // experimmental - likely to change
+ // experimmental
pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores
+ lg_email: process.env.LG_EMAIL || process.env.PG_EMAIL || process.env.EMAIL, // prime-gaming: external: legacy-games: email to use for redeeming
pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content
+ pg_timeLeft: process.env.PG_TIMELEFT == '1', // prime-gaming: list time left to claim
};
diff --git a/src/util.js b/src/util.js
index f82102b5..308952d5 100644
--- a/src/util.js
+++ b/src/util.js
@@ -11,8 +11,8 @@ export const dataDir = s => path.resolve(__dirname, '..', 'data', s);
export const resolve = (...a) => a.length && a[0] == '0' ? null : path.resolve(...a);
// json database
-import { JSONPreset } from 'lowdb/node';
-export const jsonDb = (file, defaultData) => JSONPreset(dataDir(file), defaultData);
+import { JSONFilePreset } from 'lowdb/node';
+export const jsonDb = (file, defaultData) => JSONFilePreset(dataDir(file), defaultData);
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// date and time as UTC (no timezone offset) in nicely readable and sortable format, e.g., 2022-10-06 12:05:27.313
@@ -81,7 +81,7 @@ export const stealth = async context => {
const evasion = await import(`puppeteer-extra-plugin-stealth/evasions/${e}/index.js`);
evasion.default().onPageCreated(stealth);
}
- for (let evasion of stealth.callbacks) {
+ for (const evasion of stealth.callbacks) {
await context.addInitScript(evasion.cb, evasion.a);
}
};
@@ -116,7 +116,7 @@ export const notify = html => new Promise((resolve, reject) => {
return resolve();
}
// const cmd = `apprise '${cfg.notify}' ${title} -i html -b '${html}'`; // this had problems if e.g. ' was used in arg; could have `npm i shell-escape`, but instead using safer execFile which takes args as array instead of exec which spawned a shell to execute the command
- const args = [cfg.notify, '-i', 'html', '-b', html];
+ const args = [cfg.notify, '-i', 'html', '-b', `'${html}'`];
if (cfg.notify_title) args.push(...['-t', cfg.notify_title]);
if (cfg.debug) console.debug(`apprise ${args.map(a => `'${a}'`).join(' ')}`); // this also doesn't escape, but it's just for info
execFile('apprise', args, (error, stdout, stderr) => {
diff --git a/test/notify.js b/test/notify.js
index 3479076d..6d89086a 100644
--- a/test/notify.js
+++ b/test/notify.js
@@ -1,6 +1,6 @@
/* eslint-disable no-constant-condition */
-import { delay, html_game_list, notify } from '../util.js';
-import { cfg } from '../config.js';
+import { delay, html_game_list, notify } from '../src/util.js';
+import { cfg } from '../src/config.js';
const URL_CLAIM = 'https://gaming.amazon.com/home'; // dummy URL
diff --git a/test/sigint-enquirer-raw.js b/test/sigint-enquirer-raw.js
index 0459aa77..0a95892d 100644
--- a/test/sigint-enquirer-raw.js
+++ b/test/sigint-enquirer-raw.js
@@ -1,5 +1,5 @@
// https://github.com/enquirer/enquirer/issues/372
-import { prompt } from '../util.js';
+import { prompt } from '../src/util.js';
const handleSIGINT = () => process.on('SIGINT', () => { // e.g. when killed by Ctrl-C
console.log('\nInterrupted by SIGINT. Exit!');
diff --git a/unrealengine.js b/unrealengine.js
index bfd34170..2bb8ee97 100644
--- a/unrealengine.js
+++ b/unrealengine.js
@@ -25,7 +25,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, {
// userAgent for firefox: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.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
- recordHar: cfg.record ? { path: `data/record/ue-${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/ue-${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
});
@@ -36,6 +36,7 @@ await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
+await page.setViewportSize({ width: cfg.width, height: cfg.height }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
const notify_games = [];
diff --git a/xbox.js b/xbox.js
deleted file mode 100644
index 008388cf..00000000
--- a/xbox.js
+++ /dev/null
@@ -1,250 +0,0 @@
-import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
-import { authenticator } from 'otplib';
-import {
- datetime,
- handleSIGINT,
- html_game_list,
- jsonDb,
- notify,
- prompt,
-} from './src/util.js';
-import { cfg } from './src/config.js';
-
-// ### SETUP
-const URL_CLAIM = 'https://www.xbox.com/en-US/live/gold'; // #gameswithgold";
-
-console.log(datetime(), 'started checking xbox');
-
-const db = await jsonDb('xbox.json');
-db.data ||= {};
-
-handleSIGINT();
-
-// https://playwright.dev/docs/auth#multi-factor-authentication
-const context = await firefox.launchPersistentContext(cfg.dir.browser, {
- headless: cfg.headless,
- viewport: { width: cfg.width, height: cfg.height },
- locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
-});
-
-if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
-
-const page = context.pages().length
- ? context.pages()[0]
- : await context.newPage(); // should always exist
-
-const notify_games = [];
-let user;
-
-main();
-
-async function main() {
- try {
- await performLogin();
- await getAndSaveUser();
- await redeemFreeGames();
- } catch (error) {
- console.error(error);
- process.exitCode ||= 1;
- if (error.message && process.exitCode != 130) notify(`xbox failed: ${error.message.split('\n')[0]}`);
- } finally {
- await db.write(); // write out json db
- if (notify_games.filter(g => g.status != 'existed').length) {
- // don't notify if all were already claimed
- notify(`xbox (${user}):
${html_game_list(notify_games)}`);
- }
- await context.close();
- }
-}
-
-async function performLogin() {
- await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
-
- const signInLocator = page
- .getByRole('link', {
- name: 'Sign in to your account',
- })
- .first();
- const usernameLocator = page
- .getByRole('button', {
- name: 'Account manager for',
- })
- .first();
-
- await Promise.any([signInLocator.waitFor(), usernameLocator.waitFor()]);
-
- if (await usernameLocator.isVisible()) {
- return; // logged in using saved cookie
- } else if (await signInLocator.isVisible()) {
- console.error('Not signed in anymore.');
- await signInLocator.click();
- await signInToXbox();
- } else {
- console.error('lost! where am i?');
- }
-}
-
-async function signInToXbox() {
- page.waitForLoadState('domcontentloaded');
- if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in
- console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
-
- // ### FETCH EMAIL/PASS
- if (cfg.xbox_email && cfg.xbox_password) console.info('Using email and password from environment.');
- else console.info(
- 'Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).',
- );
- const email = cfg.xbox_email || await prompt({ message: 'Enter email' });
- const password =
- email &&
- (cfg.xbox_password ||
- await prompt({
- type: 'password',
- message: 'Enter password',
- }));
- // ### FILL IN EMAIL/PASS
- if (email && password) {
- const usernameLocator = page
- .getByPlaceholder('Email, phone, or Skype')
- .first();
- const passwordLocator = page.getByPlaceholder('Password').first();
-
- await Promise.any([
- usernameLocator.waitFor(),
- passwordLocator.waitFor(),
- ]);
-
- // username may already be saved from before, if so, skip to filling in password
- if (await page.getByPlaceholder('Email, phone, or Skype').isVisible()) {
- await usernameLocator.fill(email);
- await page.getByRole('button', { name: 'Next' }).click();
- }
-
- await passwordLocator.fill(password);
- await page.getByRole('button', { name: 'Sign in' }).click();
-
- // handle MFA, but don't await it
- page.locator('input[name="otc"]')
- .waitFor()
- .then(async () => {
- console.log('Two-Step Verification - Enter security code');
- console.log(
- await page
- .locator('div[data-bind="text: description"]')
- .innerText(),
- );
- const otp =
- cfg.xbox_otpkey &&
- authenticator.generate(cfg.xbox_otpkey) ||
- await prompt({
- type: 'text',
- message: 'Enter two-factor sign in code',
- validate: n => n.toString().length == 6 ||
- 'The code must be 6 digits!',
- }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them
- await page.type('input[name="otc"]', otp.toString());
- await page
- .getByLabel('Don\'t ask me again on this device')
- .check(); // Trust this Browser
- await page.getByRole('button', { name: 'Verify' }).click();
- })
- .catch(_ => {});
-
- // Trust this browser, but don't await it
- page.getByLabel('Don\'t show this again')
- .waitFor()
- .then(async () => {
- await page.getByLabel('Don\'t show this again').check();
- await page.getByRole('button', { name: 'Yes' }).click();
- })
- .catch(_ => {});
- } else {
- console.log('Waiting for you to login in the browser.');
- await notify(
- 'xbox: no longer signed in and not enough options set for automatic login.',
- );
- if (cfg.headless) {
- console.log(
- 'Run `SHOW=1 node xbox` to login in the opened browser.',
- );
- await context.close();
- process.exit(1);
- }
- }
-
- // ### VERIFY SIGNED IN
- await page.waitForURL(`${URL_CLAIM}**`);
-
- if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
-}
-
-async function getAndSaveUser() {
- user = await page.locator('#mectrl_currentAccount_primary').innerHTML();
- console.log(`Signed in as '${user}'`);
- db.data[user] ||= {};
-}
-
-async function redeemFreeGames() {
- const monthlyGamesLocator = await page.locator('.f-size-large').all();
-
- const monthlyGamesPageLinks = await Promise.all(
- monthlyGamesLocator.map(
- async el => await el.locator('a').getAttribute('href'),
- ),
- );
- console.log('Free games:', monthlyGamesPageLinks);
-
- for (const url of monthlyGamesPageLinks) {
- await page.goto(url);
-
- const title = await page.locator('h1').first().innerText();
- const game_id = page.url().split('/').pop();
- db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only!
- console.log('Current free game:', title);
- const notify_game = { title, url, status: 'failed' };
- notify_games.push(notify_game); // status is updated below
-
- // SELECTORS
- const getBtnLocator = page.getByText('GET', { exact: true }).first();
- const installToLocator = page
- .getByText('INSTALL TO', { exact: true })
- .first();
-
- await Promise.any([
- getBtnLocator.waitFor(),
- installToLocator.waitFor(),
- ]);
-
- if (await installToLocator.isVisible()) {
- console.log(' Already in library! Nothing to claim.');
- notify_game.status = 'existed';
- db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed
- } else if (await getBtnLocator.isVisible()) {
- console.log(' Not in library yet! Click GET.');
- await getBtnLocator.click();
-
- // wait for popup
- await page
- .locator('iframe[name="purchase-sdk-hosted-iframe"]')
- .waitFor();
- const popupLocator = page.frameLocator(
- '[name=purchase-sdk-hosted-iframe]',
- );
-
- const finalGetBtnLocator = popupLocator.getByText('GET');
- await finalGetBtnLocator.waitFor();
- await finalGetBtnLocator.click();
-
- await page.getByText('Thank you for your purchase.').waitFor();
- notify_game.status = 'claimed';
- db.data[user][game_id].status = 'claimed';
- db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time
- console.log(' Claimed successfully!');
- }
-
- // notify_game.status = db.data[user][game_id].status; // claimed or failed
-
- // const p = path.resolve(cfg.dir.screenshots, playstation-plus', `${game_id}.png`);
- // if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long...
- }
-}