From a8809fcd79ffb5874750ec9ab8fcee1c5e380716 Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Sun, 10 Sep 2023 18:50:06 +0000 Subject: [PATCH 1/6] Added possibility to claim other freegames from GOG --- .vscode/launch.json | 19 +++++ README.md | 4 ++ config.js | 3 + gog.js | 166 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..59f4875f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch GOG", + "outputCapture": "std", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/gog.js", + "env": { + "GOG_GIVEAWAY": "1", + "GOG_FREEGAMES": "1" + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 7cb43932..29f25faa 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,10 @@ 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. | +| GOG_GIVEAWAYS | 1 | Claims giveaway game(s). Is enabled by default. | +| GOG_FREEGAMES | 0 | Claims other free games that are not demos or prologue games. Is disabled by default. | +| GOG_FREEGAMES_URL | [freegames_url](https://www.gog.com/en/games?priceRange=0,0&languages=en&order=asc:title&hideDLCs=true&excludeTags=demo&excludeTags=freegame) | URL to get games to claim additionally. You can add filters for language or certain categories that you don't like. The filter to hide owned games (hideOwned=true) is automatically added so please do not add it.| + See `config.js` for all options. diff --git a/config.js b/config.js index e604d0fb..1f38f453 100644 --- a/config.js +++ b/config.js @@ -38,6 +38,9 @@ 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 + gog_giveaway: process.env.GOG_GIVEAWAY == '1', + gog_freegames: false || process.env.GOG_FREEGAMES == '1', + gog_freegames_url: process.env.GOG_FREEGAMES_URL || "https://www.gog.com/en/games?priceRange=0,0&languages=en&order=asc:title&hideDLCs=true&excludeTags=demo&excludeTags=freegame", // OTP only via GOG_EMAIL, can't add app... // experimmental - likely to change diff --git a/gog.js b/gog.js index fa82c8a9..9fc8a398 100644 --- a/gog.js +++ b/gog.js @@ -1,6 +1,8 @@ import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra import { resolve, jsonDb, datetime, filenamify, prompt, notify, html_game_list, handleSIGINT } from './util.js'; import { cfg } from './config.js'; +import path from "path"; +import { existsSync } from "fs"; const screenshot = (...a) => resolve(cfg.dir.screenshots, 'gog', ...a); @@ -87,6 +89,26 @@ try { console.log(`Signed in as ${user}`); db.data[user] ||= {}; + if (cfg.gog_giveaway) { + await claimGiveaway(); + } + if (cfg.gog_freegames) { + await claimFreegames(); + } +} catch (error) { + console.error(error); // .toString()? + process.exitCode ||= 1; + if (error.message && process.exitCode != 130) + notify(`gog 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(`gog (${user}):
${html_game_list(notify_games)}`); + } +} + +async function claimGiveaway() { + console.log("Claiming giveaway"); const banner = page.locator('#giveaway'); if (!await banner.count()) { console.log('Currently no free giveaway!'); @@ -130,19 +152,143 @@ try { if (status == 'claimed' && !cfg.gog_newsletter) { console.log("Unsubscribe from 'Promotions and hot deals' newsletter"); await page.goto('https://www.gog.com/en/account/settings/subscriptions'); - await page.locator('li:has-text("Marketing communications through Trusted Partners") label').uncheck(); await page.locator('li:has-text("Promotions and hot deals") label').uncheck(); } } -} catch (error) { - console.error(error); // .toString()? - process.exitCode ||= 1; - if (error.message && process.exitCode != 130) - notify(`gog 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(`gog (${user}):
${html_game_list(notify_games)}`); +} + +async function claimGame(url){ + await page.goto(url, { waitUntil: 'networkidle' }); + + const title = await page.locator("h1").first().innerText(); + + const ageGateButton = page.locator("button.age-gate__button").first(); + if (await ageGateButton.isVisible()) { + await ageGateButton.click(); } + + const game_id = page + .url() + .split("/") + .filter((x) => !!x) + .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 + + const playforFree = page + .locator('a.cart-button:visible') + .first(); + const addToCart = page + .locator('button.cart-button:visible') + .first(); + const inLibrary = page + .locator("button.go-to-library-button") + .first() + const inCart = page + .locator('.cart-button__state-in-cart:visible') + .first(); + + await Promise.any([playforFree.waitFor(), addToCart.waitFor(), inCart.waitFor(), inLibrary.waitFor()]); + + if (await inLibrary.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 + await db.write(); + } else if (await inCart.isVisible() || await addToCart.isVisible() || await playforFree.isVisible()) { + if (await inCart.isVisible()) { + console.log("Not in library yet! But in cart."); + await inCart.click(); + } else if (await addToCart.isVisible()) { + console.log("Not in library yet! Click ADD TO CART."); + + await addToCart.click(); + await inCart.isVisible(); + await inCart.click(); + } else if (await playforFree.isVisible()) { + console.log("Play For Free. Can't be added to library!" + url); + return; + } + + await page.waitForURL('**/checkout/**'); + if (await page.locator('.order-message--error').isVisible()) { + console.log("skipping : " + await page.locator('.order-message--error').innerText()); + await page.locator('span[data-cy="product-remove-button"]').click(); + return; + } + + await page.locator('button[data-cy="payment-checkout-button"]').click(); + + await page.waitForURL('**/order/status/**'); + await page.locator('p[data-cy="order-message"]').isVisible(); + + 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!"); + await db.write(); + } + + const p = path.resolve(cfg.dir.screenshots, 'gog', `${game_id}.png`); + if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long... +} + +async function claimFreegames(){ + console.log("claiming freegames from " + cfg.gog_freegames_url + ". (adding fitler for ownedgames manually)") + await page.goto(cfg.gog_freegames_url, { waitUntil: 'networkidle' }); + await page.locator('label[selenium-id="hideOwnedCheckbox"]').click(); // when you add it to url immediately it shows more results + await page.waitForTimeout(2500); + var allLinks = []; + var hasMorePages = true; + do { + const links = await page.locator(".product-tile").all() + const gameUrls = await Promise.all( + links.map(async (game) => { + var urlSlug = await game.getAttribute("href"); + return urlSlug; + }) + ); + for (const url of gameUrls) { + allLinks.push(url); + } + if (await page.locator('.small-pagination__item--next.disabled').isVisible()){ + hasMorePages = false + console.log("last page") + } else { + await page.locator(".small-pagination__item--next").first().click(); + console.log("next page - waiting") + await page.waitForTimeout(5000); // wait until page is loaded it takes some time with filters + } + + } while (hasMorePages) + console.log("Found total games: " + allLinks.length) + allLinks = allLinks.filter(function (str) { return !str.endsWith("_prologue") }); + allLinks = allLinks.filter(function (str) { return !str.endsWith("_demo") }); + console.log("Filtered count: " + allLinks.length) + + for (const url of allLinks) + { + if (isNotClaimedUrl(url)) + { + console.log(url) + await claimGame(url); + } + } +} + +function isNotClaimedUrl(url) { + try { + var status = db.data[user][url.split("/").filter((x) => !!x).pop()]["status"]; + if (status === "existed" || status === "claimed") { + return false; + } else { + return true + } + } catch (error) { + return true + } } + await context.close(); From e372f076b64f4851de2dc9aa688a5a5d2a57c2c0 Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Sun, 10 Sep 2023 18:52:48 +0000 Subject: [PATCH 2/6] Spelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 29f25faa..d9987fe6 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,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. | -| GOG_GIVEAWAYS | 1 | Claims giveaway game(s). Is enabled by default. | +| GOG_GIVEAWAY | 1 | Claims giveaway game(s). Is enabled by default. | | GOG_FREEGAMES | 0 | Claims other free games that are not demos or prologue games. Is disabled by default. | | GOG_FREEGAMES_URL | [freegames_url](https://www.gog.com/en/games?priceRange=0,0&languages=en&order=asc:title&hideDLCs=true&excludeTags=demo&excludeTags=freegame) | URL to get games to claim additionally. You can add filters for language or certain categories that you don't like. The filter to hide owned games (hideOwned=true) is automatically added so please do not add it.| From 804ecf98d08f19274391f6b0f034502295dff82b Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Fri, 22 Sep 2023 06:24:21 +0000 Subject: [PATCH 3/6] Updated GOG claiming - additional checks for freegames for filters - cleanup file - updated README --- README.md | 6 +++--- gog.js | 52 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d9987fe6..82455496 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,9 @@ 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. | -| GOG_GIVEAWAY | 1 | Claims giveaway game(s). Is enabled by default. | -| GOG_FREEGAMES | 0 | Claims other free games that are not demos or prologue games. Is disabled by default. | -| GOG_FREEGAMES_URL | [freegames_url](https://www.gog.com/en/games?priceRange=0,0&languages=en&order=asc:title&hideDLCs=true&excludeTags=demo&excludeTags=freegame) | URL to get games to claim additionally. You can add filters for language or certain categories that you don't like. The filter to hide owned games (hideOwned=true) is automatically added so please do not add it.| +| GOG_GIVEAWAY | 1 | Claims giveaway game(s). | +| GOG_FREEGAMES | 0 | Claims other free games that are not demos or prologue games. | +| GOG_FREEGAMES_URL | [freegames_url](https://www.gog.com/en/games?priceRange=0,0&languages=en&order=asc:title&hideDLCs=true&excludeTags=demo&excludeTags=freegame) | URL to get games to claim additionally. You can add filters for language or certain categories that you don't like. | See `config.js` for all options. diff --git a/gog.js b/gog.js index e8992bab..8978e8e7 100644 --- a/gog.js +++ b/gog.js @@ -2,7 +2,6 @@ import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdate import path from 'path'; import { resolve, jsonDb, datetime, filenamify, prompt, notify, html_game_list, handleSIGINT } from './util.js'; import { cfg } from './config.js'; -import path from "path"; import { existsSync } from "fs"; const screenshot = (...a) => resolve(cfg.dir.screenshots, 'gog', ...a); @@ -61,7 +60,7 @@ try { // handle MFA, but don't await it iframe.locator('form[name=second_step_authentication]').waitFor().then(async () => { console.log('Two-Step Verification - Enter security code'); - console.log(await iframe.locator('.form__description').innerText()) + console.log(await iframe.locator('.form__description').innerText()); const otp = await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 4 || 'The code must be 4 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them await iframe.locator('#second_step_authentication_token_letter_1').pressSequentially(otp.toString(), {delay: 10}); await iframe.locator('#second_step_authentication_send').click(); @@ -74,7 +73,7 @@ try { notify('gog: got captcha during login. Please check.'); // TODO solve reCAPTCHA? }).catch(_ => { }); - await page.waitForSelector('#menuUsername') + await page.waitForSelector('#menuUsername'); } else { console.log('Waiting for you to login in the browser.'); await notify('gog: no longer signed in and not enough options set for automatic login.'); @@ -189,7 +188,7 @@ async function claimGame(url){ .first(); const inLibrary = page .locator("button.go-to-library-button") - .first() + .first(); const inCart = page .locator('.cart-button__state-in-cart:visible') .first(); @@ -240,14 +239,23 @@ async function claimGame(url){ } async function claimFreegames(){ - console.log("claiming freegames from " + cfg.gog_freegames_url + ". (adding fitler for ownedgames manually)") - await page.goto(cfg.gog_freegames_url, { waitUntil: 'networkidle' }); + var freegames_url = cfg.gog_freegames_url; + if (freegames_url.includes("&hideOwned=true")) { + freegames_url = freegames_url.replace("&hideOwned=true", ""); + } + if (!freegames_url.includes("priceRange=0,0")) { + console.log("Filter for only free games not detected adding it manually."); + freegames_url = freegames_url + "&priceRange=0,0"; + } + console.log("claiming freegames from " + freegames_url); + + await page.goto(freegames_url, { waitUntil: 'networkidle' }); await page.locator('label[selenium-id="hideOwnedCheckbox"]').click(); // when you add it to url immediately it shows more results await page.waitForTimeout(2500); var allLinks = []; var hasMorePages = true; do { - const links = await page.locator(".product-tile").all() + const links = await page.locator(".product-tile").all(); const gameUrls = await Promise.all( links.map(async (game) => { var urlSlug = await game.getAttribute("href"); @@ -258,42 +266,42 @@ async function claimFreegames(){ allLinks.push(url); } if (await page.locator('.small-pagination__item--next.disabled').isVisible()){ - hasMorePages = false - console.log("last page") + hasMorePages = false; + console.log("last page"); } else { await page.locator(".small-pagination__item--next").first().click(); - console.log("next page - waiting") + console.log("next page - waiting"); await page.waitForTimeout(5000); // wait until page is loaded it takes some time with filters } - } while (hasMorePages) - console.log("Found total games: " + allLinks.length) - allLinks = allLinks.filter(function (str) { return !str.endsWith("_prologue") }); - allLinks = allLinks.filter(function (str) { return !str.endsWith("_demo") }); - console.log("Filtered count: " + allLinks.length) + } while (hasMorePages); + console.log("Found total games: " + allLinks.length); + allLinks = allLinks.filter(function (str) { return !str.endsWith("_prologue"); }); + allLinks = allLinks.filter(function (str) { return !str.endsWith("_demo"); }); + console.log("Filtered count: " + allLinks.length); for (const url of allLinks) { - if (isNotClaimedUrl(url)) + if (!isClaimedUrl(url)) { - console.log(url) + console.log(url); await claimGame(url); } } } -function isNotClaimedUrl(url) { +function isClaimedUrl(url) { try { var status = db.data[user][url.split("/").filter((x) => !!x).pop()]["status"]; if (status === "existed" || status === "claimed") { - return false; + return true; } else { - return true + return false; } } catch (error) { - return true + return false; } } -if (page.video()) console.log('Recorded video:', await page.video().path()) +if (page.video()) console.log('Recorded video:', await page.video().path()); await context.close(); From a2a83ebd8499ab4490d380c9ab2314b0d7414fff Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Fri, 22 Sep 2023 11:23:02 +0000 Subject: [PATCH 4/6] Updated logic based on comment --- gog.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gog.js b/gog.js index 8978e8e7..c3cb492f 100644 --- a/gog.js +++ b/gog.js @@ -293,11 +293,7 @@ async function claimFreegames(){ function isClaimedUrl(url) { try { var status = db.data[user][url.split("/").filter((x) => !!x).pop()]["status"]; - if (status === "existed" || status === "claimed") { - return true; - } else { - return false; - } + return status === "existed" || status === "claimed"; } catch (error) { return false; } From b4bec61b4f3c74206f79fdc2b106a85eac0b7304 Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Tue, 3 Oct 2023 18:14:53 +0000 Subject: [PATCH 5/6] * Added option to specify multiple urls - you can specify downloads for games and also dls for owned games etc --- README.md | 2 +- gog.js | 68 ++++++++++++++++++++++++++++--------------------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 82455496..0f3337ff 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Available options/variables and their default values: | GOG_NEWSLETTER | 0 | Do not unsubscribe from newsletter after claiming a game if 1. | | GOG_GIVEAWAY | 1 | Claims giveaway game(s). | | GOG_FREEGAMES | 0 | Claims other free games that are not demos or prologue games. | -| GOG_FREEGAMES_URL | [freegames_url](https://www.gog.com/en/games?priceRange=0,0&languages=en&order=asc:title&hideDLCs=true&excludeTags=demo&excludeTags=freegame) | URL to get games to claim additionally. You can add filters for language or certain categories that you don't like. | +| GOG_FREEGAMES_URL | [freegames_url](https://www.gog.com/en/games?priceRange=0,0&languages=en&order=asc:title&hideDLCs=true&excludeTags=demo&excludeTags=freegame) | URLs to additional games to claim. Multiple URLs can be added with ";". You can add filters for language, certain categories, DLCs that you like to include or exclude. | See `config.js` for all options. diff --git a/gog.js b/gog.js index c3cb492f..91bd1717 100644 --- a/gog.js +++ b/gog.js @@ -239,42 +239,44 @@ async function claimGame(url){ } async function claimFreegames(){ - var freegames_url = cfg.gog_freegames_url; - if (freegames_url.includes("&hideOwned=true")) { - freegames_url = freegames_url.replace("&hideOwned=true", ""); - } - if (!freegames_url.includes("priceRange=0,0")) { - console.log("Filter for only free games not detected adding it manually."); - freegames_url = freegames_url + "&priceRange=0,0"; - } - console.log("claiming freegames from " + freegames_url); - - await page.goto(freegames_url, { waitUntil: 'networkidle' }); - await page.locator('label[selenium-id="hideOwnedCheckbox"]').click(); // when you add it to url immediately it shows more results - await page.waitForTimeout(2500); var allLinks = []; - var hasMorePages = true; - do { - const links = await page.locator(".product-tile").all(); - const gameUrls = await Promise.all( - links.map(async (game) => { - var urlSlug = await game.getAttribute("href"); - return urlSlug; - }) - ); - for (const url of gameUrls) { - allLinks.push(url); + var freegames_urls = cfg.gog_freegames_url.split(";"); + for (var freegames_url of freegames_urls) { + if (freegames_url.includes("&hideOwned=true")) { + freegames_url = freegames_url.replace("&hideOwned=true", ""); } - if (await page.locator('.small-pagination__item--next.disabled').isVisible()){ - hasMorePages = false; - console.log("last page"); - } else { - await page.locator(".small-pagination__item--next").first().click(); - console.log("next page - waiting"); - await page.waitForTimeout(5000); // wait until page is loaded it takes some time with filters + if (!freegames_url.includes("priceRange=0,0")) { + console.log("Filter for only free games not detected adding it manually."); + freegames_url = freegames_url + "&priceRange=0,0"; } - - } while (hasMorePages); + console.log("collecting freegames from " + freegames_url); + + await page.goto(freegames_url, { waitUntil: 'networkidle' }); + await page.locator('label[selenium-id="hideOwnedCheckbox"]').click(); // when you add it to url immediately it shows more results + await page.waitForTimeout(2500); + var hasMorePages = true; + do { + const links = await page.locator(".product-tile").all(); + const gameUrls = await Promise.all( + links.map(async (game) => { + var urlSlug = await game.getAttribute("href"); + return urlSlug; + }) + ); + for (const url of gameUrls) { + allLinks.push(url); + } + if (await page.locator('.small-pagination__item--next.disabled').isVisible()){ + hasMorePages = false; + console.log("last page"); + } else { + await page.locator(".small-pagination__item--next").first().click(); + console.log("next page - waiting"); + await page.waitForTimeout(5000); // wait until page is loaded it takes some time with filters + } + + } while (hasMorePages); + } console.log("Found total games: " + allLinks.length); allLinks = allLinks.filter(function (str) { return !str.endsWith("_prologue"); }); allLinks = allLinks.filter(function (str) { return !str.endsWith("_demo"); }); From 1e9495c77723c4e61091dd788b66b125cb368023 Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Tue, 3 Oct 2023 18:25:14 +0000 Subject: [PATCH 6/6] * dealing with case that filters deliver no results --- gog.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gog.js b/gog.js index 91bd1717..82379f65 100644 --- a/gog.js +++ b/gog.js @@ -266,9 +266,12 @@ async function claimFreegames(){ for (const url of gameUrls) { allLinks.push(url); } - if (await page.locator('.small-pagination__item--next.disabled').isVisible()){ + if (await page.locator('.catalog__empty').isVisible()){ hasMorePages = false; - console.log("last page"); + console.log("no games could be found with your filter"); + } else if (await page.locator('.small-pagination__item--next.disabled').isVisible()){ + hasMorePages = false; + console.log("last page reached"); } else { await page.locator(".small-pagination__item--next").first().click(); console.log("next page - waiting");