Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added possibility to claim other freegames from GOG #206

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I want to add vscode configurations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it's maybe good to have it if someone uses codespaces to fix something quickly

"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch GOG",
"outputCapture": "std",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/gog.js",
"env": {
"GOG_GIVEAWAY": "1",
"GOG_FREEGAMES": "1"
}
}
]
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_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) | 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.

Expand Down
3 changes: 3 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
182 changes: 169 additions & 13 deletions gog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 { existsSync } from "fs";

const screenshot = (...a) => resolve(cfg.dir.screenshots, 'gog', ...a);

Expand Down Expand Up @@ -59,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();
Expand All @@ -72,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.');
Expand All @@ -89,6 +90,27 @@ try {
console.log(`Signed in as ${user}`);
db.data[user] ||= {};

if (cfg.gog_giveaway) {
await claimGiveaway();
}
if (cfg.gog_freegames) {
await claimFreegames();
}
} catch (error) {
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
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}):<br>${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!');
Expand Down Expand Up @@ -136,17 +158,151 @@ try {
await page.locator('li:has-text("Promotions and hot deals") label').uncheck();
}
}
} catch (error) {
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
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}):<br>${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(){
var allLinks = [];
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 (!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("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('.catalog__empty').isVisible()){
hasMorePages = false;
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");
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 (!isClaimedUrl(url))
{
console.log(url);
await claimGame(url);
}
}
}
if (page.video()) console.log('Recorded video:', await page.video().path())

function isClaimedUrl(url) {
try {
var status = db.data[user][url.split("/").filter((x) => !!x).pop()]["status"];
return status === "existed" || status === "claimed";
} catch (error) {
return false;
}
}

if (page.video()) console.log('Recorded video:', await page.video().path());
await context.close();
Loading