diff --git a/src/store/lookup.ts b/src/store/lookup.ts index 73bf8af1f2..9cddb10f4c 100644 --- a/src/store/lookup.ts +++ b/src/store/lookup.ts @@ -1,8 +1,8 @@ -import {Browser, Response} from 'puppeteer'; +import {Browser, Page, Response} from 'puppeteer'; +import {Link, Store} from './model'; import {closePage, delay, getSleepTime} from '../util'; import {Config} from '../config'; import {Logger} from '../logger'; -import {Store} from './model'; import {includesLabels} from './includes-labels'; import open from 'open'; import {sendNotification} from '../notification'; @@ -57,63 +57,97 @@ async function lookup(browser: Browser, store: Store) { page.setDefaultNavigationTimeout(Config.page.navigationTimeout); await page.setUserAgent(Config.page.userAgent); - const graphicsCard = `${link.brand} ${link.model} ${link.series}`; - - let response: Response | null; try { - response = await page.goto(link.url, {waitUntil: 'networkidle0'}); - } catch { - Logger.error(`✖ [${store.name}] ${graphicsCard} skipping; timed out`); - await closePage(page); - continue; + await lookupCard(browser, store, page, link); + } catch (error) { + Logger.error(`✖ [${store.name}] ${link.brand} ${link.model} - ${error.message as string}`); } - const bodyHandle = await page.$('body'); - const textContent = await page.evaluate(body => body.textContent, bodyHandle); + await closePage(page); + } + /* eslint-enable no-await-in-loop */ +} - Logger.debug(textContent); +async function lookupCard(browser: Browser, store: Store, page: Page, link: Link) { + const response: Response | null = await page.goto(link.url, {waitUntil: 'networkidle0'}); + const graphicsCard = `${link.brand} ${link.model}`; + + if (await lookupCardInStock(store, page)) { + Logger.info(`🚀🚀🚀 [${store.name}] ${graphicsCard} IN STOCK 🚀🚀🚀`); + Logger.info(link.url); + if (Config.page.inStockWaitTime) { + inStock[store.name] = true; + setTimeout(() => { + inStock[store.name] = false; + }, 1000 * Config.page.inStockWaitTime); + } - if (includesLabels(textContent, store.labels.outOfStock)) { - Logger.info(`✖ [${store.name}] still out of stock: ${graphicsCard}`); - } else if (store.labels.bannedSeller && includesLabels(textContent, store.labels.bannedSeller)) { - Logger.warn(`✖ [${store.name}] banned seller detected: ${graphicsCard}. skipping...`); - } else if (store.labels.captcha && includesLabels(textContent, store.labels.captcha)) { - Logger.warn(`✖ [${store.name}] CAPTCHA from: ${graphicsCard}. Waiting for a bit with this store...`); - await delay(getSleepTime()); - } else if (response && response.status() === 429) { - Logger.warn(`✖ [${store.name}] Rate limit exceeded: ${graphicsCard}`); - } else { - Logger.info(`🚀🚀🚀 [${store.name}] ${graphicsCard} IN STOCK 🚀🚀🚀`); - Logger.info(link.url); - if (Config.page.inStockWaitTime) { - inStock[store.name] = true; - setTimeout(() => { - inStock[store.name] = false; - }, 1000 * Config.page.inStockWaitTime); - } + if (Config.page.capture) { + Logger.debug('ℹ saving screenshot'); + link.screenshot = `success-${Date.now()}.png`; + await page.screenshot({path: link.screenshot}); + } + + const givenUrl = link.cartUrl ? link.cartUrl : link.url; - if (Config.page.capture) { - Logger.debug('ℹ saving screenshot'); - link.screenshot = `success-${Date.now()}.png`; - await page.screenshot({path: link.screenshot}); + if (Config.browser.open) { + if (link.openCartAction === undefined) { + await open(givenUrl); + } else { + link.openCartAction(browser); } + } - const givenUrl = link.cartUrl ? link.cartUrl : link.url; + sendNotification(givenUrl, link); + return; + } - if (Config.browser.open) { - if (link.openCartAction === undefined) { - await open(givenUrl); - } else { - link.openCartAction(browser); - } - } + if (await lookupPageHasCaptcha(store, page)) { + Logger.warn(`✖ [${store.name}] CAPTCHA from: ${graphicsCard}. Waiting for a bit with this store...`); + await delay(getSleepTime()); + return; + } - sendNotification(givenUrl, link); - } + if (response && response.status() === 429) { + Logger.warn(`✖ [${store.name}] Rate limit exceeded: ${graphicsCard}`); + return; + } - await closePage(page); + Logger.info(`✖ [${store.name}] still out of stock: ${graphicsCard}`); +} + +async function lookupCardInStock(store: Store, page: Page) { + const stockHandle = await page.$(store.labels.inStock.container); + + const visible = await page.evaluate(element => element && element.offsetWidth > 0 && element.offsetHeight > 0, stockHandle); + if (!visible) { + return false; } - /* eslint-enable no-await-in-loop */ + + const stockContent = await page.evaluate(element => element.outerHTML, stockHandle); + + Logger.debug(stockContent); + + if (includesLabels(stockContent, store.labels.inStock.text)) { + return true; + } + + return false; +} + +async function lookupPageHasCaptcha(store: Store, page: Page) { + if (!store.labels.captcha) { + return false; + } + + const captchaHandle = await page.$(store.labels.captcha.container); + const captchaContent = await page.evaluate(element => element.textContent, captchaHandle); + + if (includesLabels(captchaContent, store.labels.captcha.text)) { + return true; + } + + return false; } export async function tryLookupAndLoop(browser: Browser, store: Store) { diff --git a/src/store/model/adorama.ts b/src/store/model/adorama.ts index f1fab91c27..81a84e884f 100644 --- a/src/store/model/adorama.ts +++ b/src/store/model/adorama.ts @@ -2,8 +2,10 @@ import {Store} from './store'; export const Adorama: Store = { labels: { - captcha: ['please verify you are a human'], - outOfStock: ['temporarily not available', 'out of stock'] + inStock: { + container: '.buy-section.purchase', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/amazon-ca.ts b/src/store/model/amazon-ca.ts index 21c390507b..6db67f347f 100644 --- a/src/store/model/amazon-ca.ts +++ b/src/store/model/amazon-ca.ts @@ -2,8 +2,14 @@ import {Store} from './store'; export const AmazonCa: Store = { labels: { - captcha: ['enter the characters you see below'], - outOfStock: ['currently unavailable'] + captcha: { + container: 'body', + text: ['enter the characters you see below'] + }, + inStock: { + container: '#desktop_buybox', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/amazon.ts b/src/store/model/amazon.ts index 4cd6ddffd5..a8d4a09565 100644 --- a/src/store/model/amazon.ts +++ b/src/store/model/amazon.ts @@ -2,9 +2,14 @@ import {Store} from './store'; export const Amazon: Store = { labels: { - bannedSeller: ['sports authentics', 'raccoon capitalist', 'gigaparts'], - captcha: ['enter the characters you see below'], - outOfStock: ['currently unavailable', 'available from these sellers'] + captcha: { + container: 'body', + text: ['enter the characters you see below'] + }, + inStock: { + container: '#desktop_buybox', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/asus.ts b/src/store/model/asus.ts index 5d046c733a..dab606f1bd 100644 --- a/src/store/model/asus.ts +++ b/src/store/model/asus.ts @@ -2,9 +2,18 @@ import {Store} from './store'; export const Asus: Store = { labels: { - outOfStock: ['coming soon', 'temporarily sold out'] + inStock: { + container: '#item_add_cart', + text: ['add to cart'] + } }, links: [ + { + brand: 'TEST', + model: 'CARD', + series: 'debug', + url: 'https://store.asus.com/us/item/202003AM280000002/' + }, { brand: 'asus', model: 'tuf oc', diff --git a/src/store/model/bandh.ts b/src/store/model/bandh.ts index 8c3b0f02c7..781951891e 100644 --- a/src/store/model/bandh.ts +++ b/src/store/model/bandh.ts @@ -2,7 +2,10 @@ import {Store} from './store'; export const BAndH: Store = { labels: { - outOfStock: ['notify when available', 'try varying your search terms', 'sorry, an unexpected error has occurred'] + inStock: { + container: 'div[data-selenium="addToCartSection"]', + text: ['add to cart'] + } }, links: [ { @@ -61,7 +64,6 @@ export const BAndH: Store = { series: '3080', url: 'https://www.bhphotovideo.com/c/product/1593646-REG/msi_geforce_rtx_3080_ventus.html' } - ], name: 'bandh' }; diff --git a/src/store/model/bestbuy.ts b/src/store/model/bestbuy.ts index 9f7b8e4bd2..c59e991427 100644 --- a/src/store/model/bestbuy.ts +++ b/src/store/model/bestbuy.ts @@ -2,7 +2,10 @@ import {Store} from './store'; export const BestBuy: Store = { labels: { - outOfStock: ['sold out', 'coming soon'] + inStock: { + container: '.v-m-bottom-g', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/evga-eu.ts b/src/store/model/evga-eu.ts index da3a70c02a..ae2e89b575 100644 --- a/src/store/model/evga-eu.ts +++ b/src/store/model/evga-eu.ts @@ -2,7 +2,10 @@ import {Store} from './store'; export const EvgaEu: Store = { labels: { - outOfStock: ['tbd', 'out of stock', 'error reaching the evga website', 'oops! something broke.'] + inStock: { + container: '.product-buy-specs', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/evga.ts b/src/store/model/evga.ts index db98eea75e..58ea92c074 100644 --- a/src/store/model/evga.ts +++ b/src/store/model/evga.ts @@ -2,7 +2,10 @@ import {Store} from './store'; export const Evga: Store = { labels: { - outOfStock: ['out of stock', 'error reaching the evga website', 'oops! something broke.'] + inStock: { + container: '.product-buy-specs', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/microcenter.ts b/src/store/model/microcenter.ts index 91dde35d6e..4baab20a93 100644 --- a/src/store/model/microcenter.ts +++ b/src/store/model/microcenter.ts @@ -2,7 +2,10 @@ import {Store} from './store'; export const MicroCenter: Store = { labels: { - outOfStock: ['sold out'] + inStock: { + container: '#cart-options', + text: ['(in stock)'] + } }, links: [ { diff --git a/src/store/model/newegg-ca.ts b/src/store/model/newegg-ca.ts index 3940f00955..aaedf49947 100644 --- a/src/store/model/newegg-ca.ts +++ b/src/store/model/newegg-ca.ts @@ -2,8 +2,14 @@ import {Store} from './store'; export const NewEggCa: Store = { labels: { - captcha: ['are you a human?'], - outOfStock: ['auto notify', 'item is currently out of stock', 'service unavailable'] + captcha: { + container: 'body', + text: ['are you a human?'] + }, + inStock: { + container: '#landingpage-cart .btn-primary span', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/newegg.ts b/src/store/model/newegg.ts index fd442ca9d6..38ccb2b623 100644 --- a/src/store/model/newegg.ts +++ b/src/store/model/newegg.ts @@ -2,8 +2,14 @@ import {Store} from './store'; export const NewEgg: Store = { labels: { - captcha: ['are you a human?'], - outOfStock: ['auto notify', 'item is currently out of stock', 'service unavailable', 'we are currently experiencing problems on our server'] + captcha: { + container: 'body', + text: ['are you a human?'] + }, + inStock: { + container: '#landingpage-cart .btn-primary span', + text: ['add to cart'] + } }, links: [ { diff --git a/src/store/model/nvidia.ts b/src/store/model/nvidia.ts index d27d203b27..f588a26abd 100644 --- a/src/store/model/nvidia.ts +++ b/src/store/model/nvidia.ts @@ -35,7 +35,10 @@ export const regionInfos = new Map([ export const Nvidia: Store = { labels: { - outOfStock: ['product_inventory_out_of_stock', 'rate limit exceeded', 'request timeout'] + inStock: { + container: 'body', + text: ['product_inventory_in_stock'] + } }, links: generateLinks(), name: 'nvidia', diff --git a/src/store/model/officedepot.ts b/src/store/model/officedepot.ts index 5dd62c646c..e02a02ce6d 100644 --- a/src/store/model/officedepot.ts +++ b/src/store/model/officedepot.ts @@ -2,15 +2,21 @@ import {Store} from './store'; export const OfficeDepot: Store = { labels: { - captcha: ['please verify you are a human'], - outOfStock: ['out of stock for delivery', 'out of stock', 'we are unable to process your last request'] + captcha: { + container: 'body', + text: ['please verify you are a human'] + }, + inStock: { + container: '#productPurchase', + text: ['add to cart'] + } }, links: [ { brand: 'TEST', model: 'CARD', series: 'debug', - url: 'https://www.officedepot.com/a/products/7189374/PNY-GeForce-RTX-3080-10GB-GDDR6X/' + url: 'https://www.officedepot.com/a/products/4652239/EVGA-GeForce-RTX-2060-Graphic-Card/' }, { brand: 'pny', diff --git a/src/store/model/store.ts b/src/store/model/store.ts index d3fec778ea..1c25921775 100644 --- a/src/store/model/store.ts +++ b/src/store/model/store.ts @@ -1,5 +1,10 @@ import {Browser} from 'puppeteer'; +export interface Element { + container: string; + text: string[]; +} + export interface Link { series: string; brand: string; @@ -11,9 +16,8 @@ export interface Link { } export interface Labels { - outOfStock: string[]; - captcha?: string[]; - bannedSeller?: string[]; + captcha?: Element; + inStock: Element; } export interface Store { diff --git a/src/store/model/zotac.ts b/src/store/model/zotac.ts index 77542da10c..da8ff01ce7 100644 --- a/src/store/model/zotac.ts +++ b/src/store/model/zotac.ts @@ -2,9 +2,18 @@ import {Store} from './store'; export const Zotac: Store = { labels: { - outOfStock: ['out of stock', 'this process is automatic'] + inStock: { + container: '.add-to-cart-wrapper', + text: ['add to cart'] + } }, links: [ + { + brand: 'TEST', + model: 'CARD', + series: 'debug', + url: 'https://store.zotac.com/zotac-gaming-geforce-rtx-2060-twin-fan-zt-t20600f-10m' + }, { brand: 'zotac', model: 'trinity',