diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..59f4875 --- /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 7cb4393..29f25fa 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 e604d0f..1f38f45 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 fa82c8a..9fc8a39 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();