🐛 fix(prime-gaming): update URL and selectors for claims
- change URL_CLAIM to point to new Luna claims home - update selectors for sign-in and user verification - improve handling of cookies acceptance ✨ feat(prime-gaming): enhance game claiming logic - add support for new layout and game list detection - implement flexible scrolling for loading all game cards - refine logic for internal and external game claims - improve store identification for external claims ♻️ refactor(prime-gaming): modularize game tab and list location - extract functions for opening games tab and locating games list - enhance code readability and maintainability 🐛 fix(prime-gaming): handle dynamic selectors for availability dates - support multiple selectors for availability date detection - improve error handling and logging for missing elements
This commit is contained in:
parent
99c1f05302
commit
faf22aafb1
1 changed files with 125 additions and 31 deletions
156
prime-gaming.js
156
prime-gaming.js
|
|
@ -7,7 +7,7 @@ import { cfg } from './src/config.js';
|
|||
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'prime-gaming', ...a);
|
||||
|
||||
// const URL_LOGIN = 'https://www.amazon.de/ap/signin'; // wrong. needs some session args to be valid?
|
||||
const URL_CLAIM = 'https://gaming.amazon.com/home';
|
||||
const URL_CLAIM = 'https://luna.amazon.com/claims/home';
|
||||
|
||||
console.log(datetime(), 'started checking prime-gaming');
|
||||
|
||||
|
|
@ -40,9 +40,14 @@ let user;
|
|||
try {
|
||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
||||
// need to wait for some elements to exist before checking if signed in or accepting cookies:
|
||||
await Promise.any(['button:has-text("Sign in")', '[data-a-target="user-dropdown-first-name-text"]'].map(s => page.waitForSelector(s)));
|
||||
await Promise.any([
|
||||
'button:has-text("Sign in")',
|
||||
'button:has-text("Anmelden")',
|
||||
'[data-a-target="user-dropdown-first-name-text"]',
|
||||
'[data-testid="user-dropdown-first-name-text"]',
|
||||
].map(s => page.waitForSelector(s)));
|
||||
page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")').catch(_ => { }); // to not waste screen space when non-headless, TODO does not work reliably, need to wait for something else first?
|
||||
while (await page.locator('button:has-text("Sign in")').count() > 0) {
|
||||
while (await page.locator('button:has-text("Sign in"), button:has-text("Anmelden")').count() > 0) {
|
||||
console.error('Not signed in anymore.');
|
||||
await page.click('button:has-text("Sign in")');
|
||||
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in
|
||||
|
|
@ -85,7 +90,7 @@ try {
|
|||
await page.waitForURL('https://gaming.amazon.com/home?signedIn=true');
|
||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||
}
|
||||
user = await page.locator('[data-a-target="user-dropdown-first-name-text"]').first().innerText();
|
||||
user = await page.locator('[data-a-target="user-dropdown-first-name-text"], [data-testid="user-dropdown-first-name-text"]').first().innerText();
|
||||
console.log(`Signed in as ${user}`);
|
||||
// await page.click('button[aria-label="User dropdown and more options"]');
|
||||
// const twitch = await page.locator('[data-a-target="TwitchDisplayName"]').first().innerText();
|
||||
|
|
@ -119,15 +124,83 @@ try {
|
|||
await page.waitForTimeout(3000);
|
||||
});
|
||||
|
||||
await page.click('button[data-type="Game"]');
|
||||
const games = page.locator('div[data-a-target="offer-list-FGWP_FULL"]');
|
||||
await games.waitFor();
|
||||
// await scrollUntilStable(() => games.locator('.item-card__action').count()); // number of games
|
||||
await scrollUntilStable(() => page.evaluate(() => document.querySelector('.tw-full-width').scrollHeight)); // height may change during loading while number of games is still the same?
|
||||
console.log('Number of already claimed games (total):', await games.locator('p:has-text("Collected")').count());
|
||||
// can't use .all() since the list of elements via locator will change after click while we iterate over it
|
||||
const internal = await games.locator('.item-card__action:has(button[data-a-target="FGWPOffer"])').elementHandles();
|
||||
const external = await games.locator('.item-card__action:has(a[data-a-target="FGWPOffer"])').all();
|
||||
const openGamesTab = async () => {
|
||||
const selectors = [
|
||||
'button[data-type="Game"]', // old layout
|
||||
'button:has-text("Games")',
|
||||
'[data-test-selector="category-picker"] button:has-text("Games")',
|
||||
'[data-testid="category-picker"] button:has-text("Games")',
|
||||
];
|
||||
for (const sel of selectors) {
|
||||
const btn = page.locator(sel).first();
|
||||
if (await btn.count()) {
|
||||
await btn.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// New Luna claims home already shows games list
|
||||
};
|
||||
|
||||
await openGamesTab();
|
||||
|
||||
const locateGamesList = async () => {
|
||||
const selectors = [
|
||||
'div[data-a-target="offer-list-FGWP_FULL"]', // old layout
|
||||
'[data-testid="offer-list"]',
|
||||
'[data-test-selector="offer-list"]',
|
||||
'section:has(h2:has-text("Games with Prime"))',
|
||||
'section:has(h2:has-text("Games"))',
|
||||
];
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first();
|
||||
if (await loc.count()) return loc;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const games = await locateGamesList();
|
||||
// Load all cards (old and new layout) by scrolling the container or the page
|
||||
if (games) await scrollUntilStable(() => games.evaluate(el => el.scrollHeight).catch(() => 0));
|
||||
await scrollUntilStable(() => page.evaluate(() => document.scrollingElement?.scrollHeight ?? 0));
|
||||
|
||||
const cards = [];
|
||||
const anchorClaims = page.locator('a[href*="/claims/"][href*="amzn1.pg.item"]');
|
||||
if (await anchorClaims.count()) {
|
||||
const hrefs = [...new Set(await anchorClaims.evaluateAll(anchors => anchors.map(a => a.getAttribute('href')).filter(Boolean)))];
|
||||
for (const href of hrefs) {
|
||||
let url = href;
|
||||
if (url.startsWith('/')) url = 'https://luna.amazon.com' + url;
|
||||
const title = url.split('/claims/')[1]?.split('/')[0] || (await anchorClaims.first().innerText()) || 'Unknown title';
|
||||
cards.push({ kind: 'external', title, url });
|
||||
}
|
||||
}
|
||||
|
||||
if (!cards.length && games) {
|
||||
const cardLocator = games.locator([
|
||||
'[data-testid="offer-card"]',
|
||||
'[data-test-selector="offer-card"]',
|
||||
'.item-card__action',
|
||||
].join(','));
|
||||
if (await cardLocator.count() === 0) {
|
||||
console.log('No games found in list.');
|
||||
} else {
|
||||
for (const handle of await cardLocator.elementHandles()) {
|
||||
const text = (await handle.textContent() || '').toLowerCase();
|
||||
if (text.includes('collected')) continue; // skip already claimed
|
||||
const title = await (await handle.$('h3, h4, [data-testid="item-card-title"], [data-test-selector="item-card-title"], .item-card-details__body__primary'))?.innerText() || 'Unknown title';
|
||||
const linkEl = await handle.$('a[href]');
|
||||
let url = linkEl && await linkEl.getAttribute('href');
|
||||
if (url?.startsWith('/')) url = 'https://gaming.amazon.com' + url;
|
||||
const hasLinkClaim = await handle.$('a:has-text("Claim"), a:has-text("Get"), a:has-text("Details")');
|
||||
const hasButtonClaim = await handle.$('button:has-text("Claim"), button:has-text("Get"), button:has-text("Get game"), button:has-text("Play")');
|
||||
if (hasLinkClaim) cards.push({ kind: 'external', title, url });
|
||||
else if (hasButtonClaim) cards.push({ kind: 'internal', title, url, handle });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const internal = cards.filter(c => c.kind == 'internal');
|
||||
const external = cards.filter(c => c.kind == 'external');
|
||||
// bottom to top: oldest to newest games
|
||||
internal.reverse();
|
||||
external.reverse();
|
||||
|
|
@ -143,39 +216,41 @@ try {
|
|||
const skipBasedOnTime = async url => {
|
||||
// console.log(' Checking time left for game:', url);
|
||||
const [p, isNew] = await sameOrNewPage(url);
|
||||
const dueDateOrg = await p.locator('.availability-date .tw-bold').innerText();
|
||||
const dueDateLoc = p.locator('.availability-date .tw-bold, [data-testid="availability-end-date"], [data-test-selector="availability-end-date"]');
|
||||
if (!await dueDateLoc.count()) {
|
||||
if (isNew) await p.close();
|
||||
return false;
|
||||
}
|
||||
const dueDateOrg = await dueDateLoc.first().innerText();
|
||||
const dueDate = new Date(Date.parse(dueDateOrg + ' 17:00'));
|
||||
const daysLeft = (dueDate.getTime() - Date.now())/1000/60/60/24;
|
||||
console.log(' ', await p.locator('.availability-date').innerText(), '->', daysLeft.toFixed(2));
|
||||
const availabilityText = await p.locator('.availability-date, [data-testid="availability-end-date"], [data-test-selector="availability-end-date"]').first().innerText().catch(() => dueDateOrg);
|
||||
console.log(' ', availabilityText, '->', daysLeft.toFixed(2));
|
||||
if (isNew) await p.close();
|
||||
return daysLeft > cfg.pg_timeLeft;
|
||||
}
|
||||
console.log('\nNumber of free unclaimed games (Prime Gaming):', internal.length);
|
||||
// claim games in internal store
|
||||
for (const card of internal) {
|
||||
await card.scrollIntoViewIfNeeded();
|
||||
const title = await (await card.$('.item-card-details__body__primary')).innerText();
|
||||
const slug = await (await card.$('a')).getAttribute('href');
|
||||
const url = 'https://gaming.amazon.com' + slug.split('?')[0];
|
||||
await card.handle.scrollIntoViewIfNeeded();
|
||||
const title = card.title;
|
||||
const url = card.url;
|
||||
console.log('Current free game:', chalk.blue(title));
|
||||
if (cfg.pg_timeLeft && await skipBasedOnTime(url)) continue;
|
||||
if (cfg.pg_timeLeft && url && await skipBasedOnTime(url)) continue;
|
||||
if (cfg.dryrun) continue;
|
||||
if (cfg.interactive && !await confirm()) continue;
|
||||
await (await card.$('.tw-button:has-text("Claim")')).click();
|
||||
await card.handle.locator('.tw-button:has-text("Claim"), .tw-button:has-text("Get"), button:has-text("Claim"), button:has-text("Get")').first().click();
|
||||
db.data[user][title] ||= { title, time: datetime(), url, store: 'internal' };
|
||||
notify_games.push({ title, status: 'claimed', url });
|
||||
// const img = await (await card.$('img.tw-image')).getAttribute('src');
|
||||
// console.log('Image:', img);
|
||||
await card.screenshot({ path: screenshot('internal', `${filenamify(title)}.png`) });
|
||||
await card.handle.screenshot({ path: screenshot('internal', `${filenamify(title)}.png`) });
|
||||
}
|
||||
console.log('\nNumber of free unclaimed games (external stores):', external.length);
|
||||
// claim games in external/linked stores. Linked: origin.com, epicgames.com; Redeem-key: gog.com, legacygames.com, microsoft
|
||||
const external_info = [];
|
||||
for (const card of external) { // need to get data incl. URLs in this loop and then navigate in another, otherwise .all() would update after coming back and .elementHandles() like above would lead to error due to page navigation: elementHandle.$: Protocol error (Page.adoptNode)
|
||||
const title = await card.locator('.item-card-details__body__primary').innerText();
|
||||
const slug = await card.locator('a:has-text("Claim")').first().getAttribute('href');
|
||||
const url = 'https://gaming.amazon.com' + slug.split('?')[0];
|
||||
// await (await card.$('text=Claim')).click(); // goes to URL of game, no need to wait
|
||||
const title = card.title;
|
||||
const url = card.url ? card.url.split('?')[0] : undefined;
|
||||
if (!url) continue;
|
||||
external_info.push({ title, url });
|
||||
}
|
||||
// external_info = [ { title: 'Fallout 76 (XBOX)', url: 'https://gaming.amazon.com/fallout-76-xbox-fgwp/dp/amzn1.pg.item.9fe17d7b-b6c2-4f58-b494-cc4e79528d0b?ingress=amzn&ref_=SM_Fallout76XBOX_S01_FGWP_CRWN' } ];
|
||||
|
|
@ -183,13 +258,32 @@ try {
|
|||
console.log('Current free game:', chalk.blue(title)); // , url);
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
if (cfg.debug) await page.pause();
|
||||
const item_text = await page.innerText('[data-a-target="DescriptionItemDetails"]');
|
||||
const store = item_text.toLowerCase().replace(/.* on /, '').slice(0, -1);
|
||||
let store = 'unknown';
|
||||
const detailLoc = page.locator('[data-a-target="DescriptionItemDetails"], [data-testid="DescriptionItemDetails"]');
|
||||
if (await detailLoc.count()) {
|
||||
const item_text = await detailLoc.first().innerText();
|
||||
store = item_text.toLowerCase().replace(/.* on /, '').slice(0, -1);
|
||||
} else if (url.includes('/claims/')) {
|
||||
const slug = url.split('/claims/')[1]?.split('/')[0] || '';
|
||||
if (slug.includes('gog')) store = 'gog.com';
|
||||
else if (slug.includes('epic')) store = 'epic-games';
|
||||
else if (slug.includes('origin')) store = 'origin';
|
||||
else if (slug.includes('xbox') || slug.includes('microsoft')) store = 'microsoft store';
|
||||
else if (slug.includes('legacy')) store = 'legacy games';
|
||||
}
|
||||
console.log(' External store:', store);
|
||||
if (cfg.pg_timeLeft && await skipBasedOnTime(url)) continue;
|
||||
if (cfg.dryrun) continue;
|
||||
if (cfg.interactive && !await confirm()) continue;
|
||||
await Promise.any([page.click('[data-a-target="buy-box"] .tw-button:has-text("Get game")'), page.click('[data-a-target="buy-box"] .tw-button:has-text("Claim")'), page.click('.tw-button:has-text("Complete Claim")'), page.waitForSelector('div:has-text("Link game account")'), page.waitForSelector('.thank-you-title:has-text("Success")')]); // waits for navigation
|
||||
await Promise.any([
|
||||
page.click('[data-a-target="buy-box"] .tw-button:has-text("Get game")'),
|
||||
page.click('[data-a-target="buy-box"] .tw-button:has-text("Claim")'),
|
||||
page.click('.tw-button:has-text("Complete Claim")'),
|
||||
page.click('[data-a-target="buy-box_call-to-action-text"]'),
|
||||
page.click('p[data-a-target="buy-box_call-to-action-text"]'),
|
||||
page.waitForSelector('div:has-text("Link game account")'),
|
||||
page.waitForSelector('.thank-you-title:has-text("Success")'),
|
||||
]); // waits for navigation
|
||||
db.data[user][title] ||= { title, time: datetime(), url, store };
|
||||
const notify_game = { title, url };
|
||||
notify_games.push(notify_game); // status is updated below
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue