Skip to content

Commit 2608f74

Browse files
eg: Add EG_CHECK_GP
1 parent 5938bf4 commit 2608f74

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Available options/variables and their default values:
9999
| EG_PASSWORD | | Epic Games password for login. Overrides PASSWORD. |
100100
| EG_OTPKEY | | Epic Games MFA OTP key. |
101101
| EG_PARENTALPIN | | Epic Games Parental Controls PIN. |
102+
| EG_CHECK_GP | 0 | Check GamerPower API before opening browser. Exits early if no unclaimed giveaways. |
102103
| PG_EMAIL | | Prime Gaming email for login. Overrides EMAIL. |
103104
| PG_PASSWORD | | Prime Gaming password for login. Overrides PASSWORD. |
104105
| PG_OTPKEY | | Prime Gaming MFA OTP key. |

epic-games.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,74 @@ import { existsSync, writeFileSync } from 'fs';
77
import { resolve, jsonDb, datetime, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
88
import { cfg } from './src/config.js';
99
import { getMobileGames } from './src/epic-games-mobile.js';
10+
import { gpUrlToStoreUrls } from './src/gp.js';
1011

1112
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
1213

1314
const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games';
1415
const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM;
16+
const GAMERPOWER_API_URL = 'https://www.gamerpower.com/api/giveaways?platform=epic-games-store&type=game';
1517

1618
console.log(datetime(), 'started checking epic-games');
1719

1820
const db = await jsonDb('epic-games.json', {});
1921

22+
function extractGameIdFromUrl(url) {
23+
// Epic Games URLs look like: https://store.epicgames.com/en-US/p/game-name
24+
const match = url.match(/\/p\/([^/?]+)/);
25+
return match ? match[1] : url.split('/').pop();
26+
}
27+
28+
function isGpGameAlreadyClaimed(storeUrl) {
29+
const game_id = extractGameIdFromUrl(storeUrl);
30+
31+
// Check if any user has claimed this game
32+
for (const [username, games] of Object.entries(db.data)) {
33+
if (games[game_id]?.status === 'claimed' || games[game_id]?.status === 'existed') {
34+
console.log(`[GamerPower] Already claimed by ${username}: ${storeUrl} -> ${game_id}`);
35+
return true;
36+
}
37+
}
38+
39+
return false;
40+
}
41+
42+
// ============================================================================
43+
// Check GamerPower for unclaimed games (before starting browser)
44+
// ============================================================================
45+
46+
async function getUnclaimedGpUrls() {
47+
if (!cfg.eg_check_gp) return [];
48+
49+
// gpUrlToStoreUrls handles fetching API and resolving URLs (opens browser only if needed)
50+
const allGpGames = await gpUrlToStoreUrls(GAMERPOWER_API_URL);
51+
52+
// Filter to Epic Games store URLs only
53+
const epicGames = allGpGames.filter(g => g.storeUrl.includes('store.epicgames.com'));
54+
console.log(`[GamerPower] ${epicGames.length} Epic Games store URLs`);
55+
56+
// Filter out already claimed games
57+
const unclaimed = epicGames.filter(g => !isGpGameAlreadyClaimed(g.storeUrl));
58+
console.log(`[GamerPower] ${unclaimed.length} unclaimed games`);
59+
60+
return unclaimed.map(g => g.storeUrl);
61+
}
62+
63+
// ============================================================================
64+
// Main Script
65+
// ============================================================================
66+
2067
if (cfg.time) console.time('startup');
2168

69+
// Check GamerPower first (before starting main browser)
70+
const gpUrls = await getUnclaimedGpUrls();
71+
72+
// If EG_CHECK_GP is enabled and no unclaimed games, exit early without opening browser
73+
if (cfg.eg_check_gp && gpUrls.length === 0) {
74+
console.log('No unclaimed GamerPower giveaways. Exiting.');
75+
process.exit(0);
76+
}
77+
2278
// https://playwright.dev/docs/auth#multi-factor-authentication
2379
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
2480
// channel: 'chrome', // recommended, but `npx patchright install chrome` clashes with system Chrome - https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-nodejs#best-practice----use-chrome-without-fingerprint-injection
@@ -158,6 +214,22 @@ try {
158214
urls.push(...mobileGames.map(x => x.url));
159215
}
160216

217+
// GamerPower games - verify they are included in the free games list
218+
if (cfg.eg_check_gp && gpUrls.length > 0) {
219+
console.log(`Verifying ${gpUrls.length} GamerPower giveaways are in Epic's free games list...`);
220+
for (const gpUrl of gpUrls) {
221+
// Normalize URLs for comparison (remove trailing slashes, query params)
222+
const gpUrlNormalized = gpUrl.split('?')[0].replace(/\/$/, '');
223+
const found = urls.some(url => url.split('?')[0].replace(/\/$/, '') === gpUrlNormalized);
224+
if (!found) {
225+
console.error(`[GamerPower] ERROR: ${gpUrl} is NOT in Epic's free games list!`);
226+
console.error(`[GamerPower] Epic's free games: ${urls.join(', ')}`);
227+
} else {
228+
console.log(`[GamerPower] OK: ${gpUrl}`);
229+
}
230+
}
231+
}
232+
161233
console.log('Free games:', urls);
162234

163235
for (const url of urls) {

src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const cfg = {
3535
eg_otpkey: process.env.EG_OTPKEY,
3636
eg_parentalpin: process.env.EG_PARENTALPIN,
3737
eg_mobile: process.env.EG_MOBILE != '0', // claim mobile games
38+
eg_check_gp: process.env.EG_CHECK_GP == '1', // check GamerPower for free Epic Games
3839
// auth prime-gaming
3940
pg_email: process.env.PG_EMAIL || process.env.EMAIL,
4041
pg_password: process.env.PG_PASSWORD || process.env.PASSWORD,

src/gp.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// GamerPower URL resolution module
2+
// Resolves giveaway URLs to store URLs (Epic Games, Steam, etc.)
3+
// Transparently handles caching and browser-based resolution
4+
5+
import { chromium } from 'patchright';
6+
import { jsonDb, datetime, handleSIGINT } from './util.js';
7+
import { cfg } from './config.js';
8+
9+
const gpCache = await jsonDb('gp-cache.json', {});
10+
11+
/**
12+
* Fetches giveaways from a GamerPower API URL
13+
* @param {string} apiUrl - The GamerPower API URL (e.g., https://www.gamerpower.com/api/giveaways?platform=steam&type=game)
14+
* @returns {Promise<Array>} - Array of giveaway objects from the API
15+
*/
16+
async function fetchGamerPowerGiveaways(apiUrl) {
17+
console.log('[GamerPower] Fetching giveaways from API...');
18+
const response = await fetch(apiUrl);
19+
20+
if (!response.ok) {
21+
throw new Error(`Failed to fetch GamerPower data: ${response.statusText}`);
22+
}
23+
24+
const items = await response.json();
25+
console.log(`[GamerPower] Fetched ${items.length} giveaways`);
26+
return items;
27+
}
28+
29+
/**
30+
* Checks if a giveaway URL is already cached
31+
* @param {string} giveawayUrl - The GamerPower open_giveaway_url
32+
* @returns {Object|null} - Cached entry or null if not cached
33+
*/
34+
function getCachedUrl(giveawayUrl) {
35+
return gpCache.data[giveawayUrl] || null;
36+
}
37+
38+
/**
39+
* Caches a resolved giveaway URL
40+
* @param {string} giveawayUrl - The GamerPower open_giveaway_url
41+
* @param {string} storeUrl - The resolved store URL
42+
*/
43+
function cacheUrl(giveawayUrl, storeUrl) {
44+
gpCache.data[giveawayUrl] = {
45+
storeUrl,
46+
time: datetime()
47+
};
48+
console.log(`[GamerPower] Cached: ${giveawayUrl} -> ${storeUrl}`);
49+
}
50+
51+
/**
52+
* Resolves giveaway URLs that aren't cached yet using a browser
53+
* @param {Array} urlsToResolve - Array of giveaway URLs to resolve
54+
* @returns {Promise<Object>} - Map of giveawayUrl -> storeUrl
55+
*/
56+
async function resolveUrlsWithBrowser(urlsToResolve) {
57+
if (urlsToResolve.length === 0) {
58+
return {};
59+
}
60+
61+
console.log(`[GamerPower] Resolving ${urlsToResolve.length} URLs with browser...`);
62+
63+
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
64+
headless: cfg.headless,
65+
viewport: { width: cfg.width, height: cfg.height },
66+
locale: 'en-US',
67+
handleSIGINT: false,
68+
args: ['--hide-crash-restore-bubble'],
69+
});
70+
71+
handleSIGINT(context);
72+
73+
const page = context.pages().length ? context.pages()[0] : await context.newPage();
74+
75+
const resolved = {};
76+
77+
try {
78+
for (const giveawayUrl of urlsToResolve) {
79+
console.log(`[GamerPower] Resolving: ${giveawayUrl}`);
80+
await page.goto(giveawayUrl, { waitUntil: 'domcontentloaded' });
81+
const storeUrl = page.url();
82+
83+
cacheUrl(giveawayUrl, storeUrl);
84+
resolved[giveawayUrl] = storeUrl;
85+
}
86+
87+
await gpCache.write();
88+
} finally {
89+
await context.close();
90+
}
91+
92+
return resolved;
93+
}
94+
95+
/**
96+
* Main function: Fetches giveaways from GamerPower API and returns resolved store URLs
97+
*
98+
* This function:
99+
* 1. Fetches giveaways from the API (no browser needed)
100+
* 2. Returns cached store URLs immediately for known giveaways
101+
* 3. Opens a browser to resolve any new giveaway URLs (transparent to caller)
102+
* 4. Returns a list of { giveawayUrl, storeUrl } objects
103+
*
104+
* @param {string} apiUrl - The GamerPower API URL
105+
* @returns {Promise<Array<{giveawayUrl: string, storeUrl: string}>>} - Array of resolved URLs
106+
*/
107+
export async function gpUrlToStoreUrls(apiUrl) {
108+
const giveaways = await fetchGamerPowerGiveaways(apiUrl);
109+
110+
const cachedResults = [];
111+
const urlsToResolve = [];
112+
113+
for (const giveaway of giveaways) {
114+
const giveawayUrl = giveaway.open_giveaway_url;
115+
const cached = getCachedUrl(giveawayUrl);
116+
117+
if (cached) {
118+
cachedResults.push({
119+
giveawayUrl,
120+
storeUrl: cached.storeUrl,
121+
title: giveaway.title
122+
});
123+
} else {
124+
urlsToResolve.push(giveawayUrl);
125+
}
126+
}
127+
128+
console.log(`[GamerPower] ${cachedResults.length} cached, ${urlsToResolve.length} need resolution`);
129+
130+
// Resolve any uncached URLs with browser
131+
const newlyResolved = await resolveUrlsWithBrowser(urlsToResolve);
132+
133+
// Combine cached and newly resolved
134+
const allResults = [...cachedResults];
135+
136+
for (const giveaway of giveaways) {
137+
const giveawayUrl = giveaway.open_giveaway_url;
138+
if (newlyResolved[giveawayUrl]) {
139+
allResults.push({
140+
giveawayUrl,
141+
storeUrl: newlyResolved[giveawayUrl],
142+
title: giveaway.title
143+
});
144+
}
145+
}
146+
147+
return allResults;
148+
}

0 commit comments

Comments
 (0)