remove xbox plugin, games with gold doesn't exist anymore
This commit is contained in:
parent
28bcceb285
commit
748bf41f46
3 changed files with 0 additions and 261 deletions
|
|
@ -10,7 +10,6 @@ Claims free games periodically on
|
||||||
- <img src="https://seeklogo.com/images/P/prime-gaming-logo-61A701B3F5-seeklogo.com.png" width="32"/> [Amazon Prime Gaming](https://gaming.amazon.com)
|
- <img src="https://seeklogo.com/images/P/prime-gaming-logo-61A701B3F5-seeklogo.com.png" width="32"/> [Amazon Prime Gaming](https://gaming.amazon.com)
|
||||||
- <img src="https://static.wikia.nocookie.net/this-war-of-mine/images/1/1a/Logo_GoG.png/revision/latest?cb=20160711062658" width="32"/> [GOG](https://www.gog.com)
|
- <img src="https://static.wikia.nocookie.net/this-war-of-mine/images/1/1a/Logo_GoG.png/revision/latest?cb=20160711062658" width="32"/> [GOG](https://www.gog.com)
|
||||||
- <img src="https://cdn2.unrealengine.com/ue-logo-white-e34b6ba9383f.svg" width="32"/> [Unreal Engine (Assets)](https://www.unrealengine.com/marketplace/en-US/assets?count=20&sortBy=effectiveDate&sortDir=DESC&start=0&tag=4910) ([experimental](https://github.com/vogler/free-games-claimer/issues/44), same login as Epic Games)
|
- <img src="https://cdn2.unrealengine.com/ue-logo-white-e34b6ba9383f.svg" width="32"/> [Unreal Engine (Assets)](https://www.unrealengine.com/marketplace/en-US/assets?count=20&sortBy=effectiveDate&sortDir=DESC&start=0&tag=4910) ([experimental](https://github.com/vogler/free-games-claimer/issues/44), same login as Epic Games)
|
||||||
<!-- - <img src="https://www.freepnglogos.com/uploads/xbox-logo-picture-png-14.png" width="32"/> [Xbox Live Games with Gold](https://www.xbox.com/en-US/live/gold#gameswithgold) ([experimental](https://github.com/vogler/free-games-claimer/issues/19)) -->
|
|
||||||
|
|
||||||
Pull requests welcome :)
|
Pull requests welcome :)
|
||||||
|
|
||||||
|
|
@ -119,7 +118,6 @@ To get the OTP key, it is easiest to follow the store's guide for adding an auth
|
||||||
- **Epic Games**: visit [password & security](https://www.epicgames.com/account/password), enable 'third-party authenticator app', copy the 'Manual Entry Key' and use it to set `EG_OTPKEY`.
|
- **Epic Games**: visit [password & security](https://www.epicgames.com/account/password), enable 'third-party authenticator app', copy the 'Manual Entry Key' and use it to set `EG_OTPKEY`.
|
||||||
- **Prime Gaming**: visit Amazon 'Your Account › Login & security', 2-step verification › Manage › Add new app › Can't scan the barcode, copy the bold key and use it to set `PG_OTPKEY`
|
- **Prime Gaming**: visit Amazon 'Your Account › Login & security', 2-step verification › Manage › Add new app › Can't scan the barcode, copy the bold key and use it to set `PG_OTPKEY`
|
||||||
- **GOG**: only offers OTP via email
|
- **GOG**: only offers OTP via email
|
||||||
<!-- - **Xbox**: visit [additional security](https://account.live.com/proofs/manage/additional) > Add a new way to sign in or verify > Use an app > Set up a different Authenticator app > I can't scan the bar code > copy the bold key and use it to set `XBOX_OTPKEY` -->
|
|
||||||
|
|
||||||
Beware that storing passwords and OTP keys as clear text may be a security risk. Use a unique/generated password! TODO: maybe at least offer to base64 encode for storage.
|
Beware that storing passwords and OTP keys as clear text may be a security risk. Use a unique/generated password! TODO: maybe at least offer to base64 encode for storage.
|
||||||
|
|
||||||
|
|
@ -137,16 +135,12 @@ Claiming the Amazon Games works out-of-the-box, however, for games on external s
|
||||||
Keys and URLs are printed to the console, included in notifications and saved in `data/prime-gaming.json`. A screenshot of the page with the key is also saved to `data/screenshots`.
|
Keys and URLs are printed to the console, included in notifications and saved in `data/prime-gaming.json`. A screenshot of the page with the key is also saved to `data/screenshots`.
|
||||||
[TODO](https://github.com/vogler/free-games-claimer/issues/5): ~~redeem keys on external stores.~~
|
[TODO](https://github.com/vogler/free-games-claimer/issues/5): ~~redeem keys on external stores.~~
|
||||||
|
|
||||||
<!-- ### Xbox Games With Gold -->
|
|
||||||
<!-- Run `node xbox` (locally or in docker). -->
|
|
||||||
|
|
||||||
### Run periodically
|
### Run periodically
|
||||||
#### How often?
|
#### How often?
|
||||||
Epic Games usually has two free games *every week*, before Christmas every day.
|
Epic Games usually has two free games *every week*, before Christmas every day.
|
||||||
Prime Gaming has new games *every month* or more often during Prime days.
|
Prime Gaming has new games *every month* or more often during Prime days.
|
||||||
GOG usually has one new game every couples of weeks.
|
GOG usually has one new game every couples of weeks.
|
||||||
Unreal Engine has new assets to claim *every first Tuesday of a month*.
|
Unreal Engine has new assets to claim *every first Tuesday of a month*.
|
||||||
<!-- Xbox usually has two games *every month*. -->
|
|
||||||
|
|
||||||
It is safe to run the scripts every day.
|
It is safe to run the scripts every day.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,6 @@ export const cfg = {
|
||||||
gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD,
|
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_newsletter: process.env.GOG_NEWSLETTER == '1', // do not unsubscribe from newsletter after claiming a game
|
||||||
// OTP only via GOG_EMAIL, can't add app...
|
// 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 - likely to change
|
||||||
pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores
|
pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores
|
||||||
pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content
|
pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content
|
||||||
|
|
|
||||||
251
xbox.js
251
xbox.js
|
|
@ -1,251 +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
|
|
||||||
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
|
|
||||||
|
|
||||||
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}):<br>${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...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue