Skip to content

Commit

Permalink
feat: max price filtering (#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
xninjax committed Oct 2, 2020
1 parent 02f9ed5 commit fd294d2
Show file tree
Hide file tree
Showing 22 changed files with 107 additions and 5 deletions.
1 change: 1 addition & 0 deletions .env-example
Expand Up @@ -13,6 +13,7 @@ HEADLESS=""
IN_STOCK_WAIT_TIME=""
LOG_LEVEL=""
LOW_BANDWIDTH=""
MAX_PRICE=“”
MICROCENTER_LOCATION=""
NVIDIA_ADD_TO_CART_ATTEMPTS=""
NVIDIA_SESSION_TTL=""
Expand Down
7 changes: 5 additions & 2 deletions .gitignore
Expand Up @@ -4,10 +4,13 @@
build/
node_modules/

.env
.env*
.*env
!.env-example
success-*.png

*.wav
*.mp3
*.flac
*.exe
*.exe
desktop.ini
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -81,7 +81,7 @@ Here is a list of variables that you can use to customize your newly copied `.en
| `LOW_BANDWIDTH` | Blocks images/fonts to reduce traffic | Disables ad blocker, default: `false` |
| `MICROCENTER_LOCATION` | Specific MicroCenter location to search | Default : `web` |
| `NVIDIA_ADD_TO_CART_ATTEMPTS` | The maximum number of times the `nvidia-api` add to cart feature will be attempted before failing | Default: `10` |
| `NVIDIA_SESSION_TTL` | The time in seconds to keep the cart active while using `nvidia-api` | Default: `60000` |
| `NVIDIA_SESSION_TTL` | The time in milliseconds to keep the cart active while using `nvidia-api` | Default: `60000` |
| `OPEN_BROWSER` | Toggle for whether or not the browser should open when item is found | Default: `true` |
| `PAGE_TIMEOUT` | Navigation Timeout in milliseconds | `0` for infinite, default: `30000` |
| `PHONE_NUMBER` | 10 digit phone number | E.g.: `1234567890`, email configuration required |
Expand All @@ -104,6 +104,7 @@ Here is a list of variables that you can use to customize your newly copied `.en
| `SLACK_CHANNEL` | Slack channel for posting | E.g.: `update`, no need for `#` |
| `SLACK_TOKEN` | Slack API token | |
| `STORES` | [Supported stores](#supported-stores) you want to be scraped | Comma separated, default: `nvidia` |
| `MAX_PRICE` | Maximum price allowed for a match, applies to all cards (does not apply to these sites: Nvidia, Asus, EVGA) | Default: leave empty for no limit, otherwise enter a price (enter whole dollar amounts only, avoid use of: dollar symbols, commas, and periods.) e.g.: `1234` - Cards above `1234` will be skipped. |
| `COUNTRY` | [Supported country](#supported-countries) you want to be scraped | Currently only used by Nvidia, default: `usa` |
| `SCREENSHOT` | Capture screenshot of page if a card is found | Default: `true` |
| `TELEGRAM_ACCESS_TOKEN` | Telegram access token | |
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Expand Up @@ -140,6 +140,7 @@ const proxy = {

const store = {
country: envOrString(process.env.COUNTRY, 'usa'),
maxPrice: envOrNumber(process.env.MAX_PRICE),
microCenterLocation: envOrString(process.env.MICROCENTER_LOCATION, 'web'),
showOnlyBrands: envOrArray(process.env.SHOW_ONLY_BRANDS),
showOnlyModels: envOrArray(process.env.SHOW_ONLY_MODELS),
Expand Down
7 changes: 7 additions & 0 deletions src/logger.ts
Expand Up @@ -74,6 +74,13 @@ export const Print = {

return `ℹ ${buildProductString(link, store)} :: IN STOCK, WAITING`;
},
maxPrice(link: Link, store: Store, price: number, color?: boolean): string {
if (color) {
return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow(`PRICE ${price} EXCEEDS LIMIT ${Config.store.maxPrice}`);
}

return `✖ ${buildProductString(link, store)} :: PRICE ${price} EXCEEDS LIMIT ${Config.store.maxPrice}`;
},
message(message: string, topic: string, store: Store, color?: boolean): string {
if (color) {
return '✖ ' + buildSetupString(topic, store, true) + ' :: ' + chalk.yellow(message);
Expand Down
21 changes: 20 additions & 1 deletion src/store/includes-labels.ts
@@ -1,4 +1,4 @@
import {Element, LabelQuery} from './model';
import {Element, LabelQuery, Pricing} from './model';
import {Logger} from '../logger';
import {Page} from 'puppeteer';

Expand Down Expand Up @@ -90,3 +90,22 @@ export function includesLabels(domText: string, searchLabels: string[]): boolean
const domTextLowerCase = domText.toLowerCase();
return searchLabels.some(label => domTextLowerCase.includes(label));
}

export async function cardPriceLimit(page: Page, query: Pricing, max: number, options: Selector) {
if (!max) {
return null;
}

const selector = {...options, selector: query.container};
const cardPrice = await extractPageContents(page, selector);

if (cardPrice) {
const priceSeperator = query.euroFormat ? /\./g : /,/g;
const cardpriceNumber = Number.parseFloat(cardPrice.replace(priceSeperator, '').match(/\d+/g)!.join('.'));

Logger.debug(`Raw card price: ${cardPrice} | Limit: ${max}`);
return cardpriceNumber > max ? cardpriceNumber : null;
}

return null;
}
10 changes: 9 additions & 1 deletion src/store/lookup.ts
@@ -1,7 +1,7 @@
import {Browser, Page, Response} from 'puppeteer';
import {Link, Store} from './model';
import {Logger, Print} from '../logger';
import {Selector, pageIncludesLabels} from './includes-labels';
import {Selector, cardPriceLimit, pageIncludesLabels} from './includes-labels';
import {closePage, delay, getSleepTime, isStatusCodeInRange} from '../util';
import {Config} from '../config';
import {disableBlockerInPage} from '../adblocker';
Expand Down Expand Up @@ -145,6 +145,14 @@ async function lookupCardInStock(store: Store, page: Page, link: Link) {
}
}

if (store.labels.maxPrice) {
const priceLimit = await cardPriceLimit(page, store.labels.maxPrice, Config.store.maxPrice, baseOptions);
if (priceLimit) {
Logger.info(Print.maxPrice(link, store, priceLimit, true));
return false;
}
}

if (store.labels.captcha) {
if (await pageIncludesLabels(page, store.labels.captcha, baseOptions)) {
Logger.warn(Print.captcha(link, store, true));
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/adorama.ts
Expand Up @@ -9,6 +9,10 @@ export const Adorama: Store = {
inStock: {
container: '.buy-section.purchase',
text: ['add to cart']
},
maxPrice: {
container: '.your-price',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/amazon-ca.ts
Expand Up @@ -9,6 +9,10 @@ export const AmazonCa: Store = {
inStock: {
container: '#desktop_buybox',
text: ['add to cart']
},
maxPrice: {
container: 'span[class*="PriceString"]',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/amazon-de.ts
Expand Up @@ -9,6 +9,10 @@ export const AmazonDe: Store = {
inStock: {
container: '#desktop_buybox',
text: ['in den einkaufswagen']
},
maxPrice: {
container: 'span[class*="PriceString"]',
euroFormat: true
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/amazon-nl.ts
Expand Up @@ -9,6 +9,10 @@ export const AmazonNl: Store = {
inStock: {
container: '#desktop_buybox',
text: ['in winkelwagen plaatsen']
},
maxPrice: {
container: 'span[class*="PriceString"]',
euroFormat: true
}
},
links: [
Expand Down
3 changes: 3 additions & 0 deletions src/store/model/amazon.ts
Expand Up @@ -9,6 +9,9 @@ export const Amazon: Store = {
inStock: {
container: '#desktop_buybox',
text: ['add to cart']
},
maxPrice: {
container: 'span[class*="PriceString"]'
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/bandh.ts
Expand Up @@ -6,6 +6,10 @@ export const BAndH: Store = {
inStock: {
container: 'div[data-selenium="addToCartSection"]',
text: ['add to cart']
},
maxPrice: {
container: 'div[data-selenium="pricingPrice"]',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/bestbuy-ca.ts
Expand Up @@ -5,6 +5,10 @@ export const BestBuyCa: Store = {
inStock: {
container: '#root',
text: ['available online']
},
maxPrice: {
container: 'div[class^="productPricingContainer"] span[class^="screenReaderOnly_"',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/bestbuy.ts
Expand Up @@ -5,6 +5,10 @@ export const BestBuy: Store = {
inStock: {
container: '.v-m-bottom-g',
text: ['add to cart']
},
maxPrice: {
container: 'div[class="priceView-hero-price priceView-customer-price"] > span',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/microcenter.ts
Expand Up @@ -43,6 +43,10 @@ export const MicroCenter: Store = {
inStock: {
container: '#cart-options',
text: ['in stock']
},
maxPrice: {
container: 'span[id="pricing"]',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/newegg-ca.ts
Expand Up @@ -9,6 +9,10 @@ export const NeweggCa: Store = {
inStock: {
container: '#landingpage-cart .btn-primary span',
text: ['add to cart']
},
maxPrice: {
container: '#landingpage-price > div > div > ul > li.price-current > strong',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/newegg.ts
Expand Up @@ -9,6 +9,10 @@ export const Newegg: Store = {
inStock: {
container: '#landingpage-cart .btn-primary span',
text: ['add to cart']
},
maxPrice: {
container: '#landingpage-price > div > div > ul > li.price-current > strong',
euroFormat: false
}
},
links: [
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/officedepot.ts
Expand Up @@ -9,6 +9,10 @@ export const OfficeDepot: Store = {
inStock: {
container: '#productPurchase',
text: ['add to cart']
},
maxPrice: {
container: 'span[class^="price_column right"]',
euroFormat: false
}
},
links: [
Expand Down
5 changes: 5 additions & 0 deletions src/store/model/pny.ts
Expand Up @@ -5,7 +5,12 @@ export const Pny: Store = {
inStock: {
container: '#ctl01_lbtnAddToCart',
text: ['add to cart']
},
maxPrice: {
container: 'span[itemprop="price"]',
euroFormat: false
}

},
links: [
{
Expand Down
6 changes: 6 additions & 0 deletions src/store/model/store.ts
Expand Up @@ -5,6 +5,11 @@ export type Element = {
text: string[];
};

export type Pricing = {
container: string;
euroFormat?: boolean;
};

export type Series = 'test:series' | '3070' | '3080' | '3090';

export type Link = {
Expand All @@ -25,6 +30,7 @@ export type Labels = {
container?: string;
inStock?: LabelQuery;
outOfStock?: LabelQuery;
maxPrice?: Pricing;
};

export type StatusCodeRangeArray = Array<(number | [number, number])>;
Expand Down
4 changes: 4 additions & 0 deletions src/store/model/zotac.ts
Expand Up @@ -6,6 +6,10 @@ export const Zotac: Store = {
inStock: {
container: '.add-to-cart-wrapper',
text: ['add to cart']
},
maxPrice: {
container: 'div[class="product-shop"] span[class="price"]',
euroFormat: false
}
},
links: [
Expand Down

0 comments on commit fd294d2

Please sign in to comment.