UNPKG

wm-pricer

Version:

Find stuff and things at good prices

216 lines (206 loc) 7.01 kB
'use strict' // Setup const util = require('util') const Event = require('events').EventEmitter const wmApi = require('walmart') const http = require('http') const cheerio = require('cheerio') const isNumber = (n) => Number.isFinite(n) && !Number.isNaN(n) const keepProps = ['itemId', 'name', 'msrp', 'salePrice', 'modelNumber', 'upc'] const merge = (a, b, preferA) => { if (!a || typeof a !== 'object') a = {} if (!b) return a if (typeof b !== 'object') b = {} Object.keys(b).forEach((k) => { a[k] = preferA ? a[k] || b[k] : b[k] }) return a } const model = [ 'name', 'itemId', 'msrp', 'salePrice', 'maxPrice', 'minPrice', 'variancePercent', 'modelNumber', 'upc', 'url', 'bsUrl', 'inventory', 'thumbnail' ] const storesByZip = {} module.exports = WmPricer // Constructor function WmPricer (opts) { let self = this let noArgErr = 'Missing required argument (opts Object or apiKey String)' if (!opts) throw new Error(noArgErr) if (typeof opts === 'string') opts = {apiKey: opts} self.apiKey = opts.apiKey if (!self.apiKey) throw new Error('API key is required in opts') self.zip = opts.zip || 33803 self.query = opts.query || '4k tv' self.page = opts.page || 1 self.limit = opts.limit self.wmOnline = {} self.wmStore = {} self.wm = wmApi(self.apiKey) return self } util.inherits(WmPricer, Event) // Class methods WmPricer.new = (opts) => new WmPricer(opts) // Instance methods WmPricer.prototype.storeQuery = function (opts) { let self = this let handleErr = (err) => self.emit('error', err) opts = merge(opts || {}, self, true) let diff = opts.minDiff let tmpList = [] let itemsObj = {} let processThenPush = () => { tmpList.forEach((e) => { if (!e.inventory || e.inventory.quantity < 1) return let id = (e.productId || {}).WWWItemId let tmpObj = itemsObj[id] = itemsObj[id] || {} tmpObj.itemId = id tmpObj.url = `http://www.walmart.com/ip/${id}` tmpObj.bsUrl = `http://brickseek.com/walmart-inventory-checker?sku=${id}` tmpObj.thumbnail = (e.images || {}).thumbnailUrl tmpObj.inventory = tmpObj.inventory || [] let location = (e.location.detailed || [])[0] let inventory = { address: [ e.store.name, e.store.streetAddress, `${e.store.city} ${e.store.stateProvCode} ${e.store.zip}`, e.store.phoneNumber, 'distance uknown' ], price: parseFloat((e.price.priceInCents || 100) / 100), stock: e.inventory.quantity || 0, location: !location ? null : `${location.zone}-${location.aisle}-${location.section}` } tmpObj.inventory.push(inventory) model.forEach((k) => { tmpObj[k] = tmpObj[k] || e[k] }) }) tmpList = Object.keys(itemsObj).map((i) => itemsObj[i]) let aryOut = [] tmpList.forEach((e, i) => { let priceMap = (e.inventory || []).map((s) => s.price) e.maxPrice = Math.max(...priceMap) e.minPrice = Math.min(...priceMap) e.variancePercent = parseInt(100 - ((e.minPrice / e.maxPrice) * 100), 10) if (diff && (!isNumber(e.variancePercent) || e.variancePercent < diff)) { return null } e.inventory.sort((a, b) => a.price - b.price) aryOut[i] = {} model.forEach((k) => { aryOut[i][k] = e[k] }) }) aryOut = aryOut.filter((i) => i) aryOut.sort((a, b) => b.variancePercent - a.variancePercent) self.emit('store-query-done', aryOut) } let storesSearched = 0 if (storesByZip[opts.zip]) storesProcessor(storesByZip[opts.zip], false) else self.wm.stores.byZip(opts.zip).then(storesProcessor).catch(handleErr) function storesProcessor (stores, setStores) { if (setStores !== false) storesByZip[opts.zip] = stores stores.forEach((s) => { let storeCount = 0 let offset = 0 function storeSearchCb (count, totalCount) { offset = offset + count storeCount = storeCount + count // totalCount = totalCount > 100 ? 100 : totalCount if (storeCount < totalCount) return storeSearch(s, offset, storeSearchCb) storesSearched = storesSearched + 1 if (storesSearched === stores.length) return processThenPush() } storeSearch(s, offset, storeSearchCb) }) } function storeSearch (s, offset, cb) { self.wm.stores.search(s.no, opts.query, {size: 50, offset}).then((data) => { data.results.forEach((r) => { r.store = s }) tmpList = tmpList.concat(data.results) cb(data.count, data.totalCount) }).catch((e) => { handleErr(e) let z = 0 cb(z, z) }) } return self } // Search via Wlmart Open API then run results through BrickSeek for local info WmPricer.prototype.brickseek = function (opts, cb) { let self = this let apiKeyErr = 'API key is required' if (!opts.apiKey) { cb ? cb(apiKeyErr) : console.error(apiKeyErr) return self } let zip = opts.zip || 33803 let query = opts.query || '4k tv' let limit = opts.limit || 25 let start = opts.start || 1 let wm = wmApi(opts.apiKey) let out = [] let listLength wm.search(query, {numItems: limit, start}).then((list) => { listLength = list.items.length list.items.forEach((item) => getBsHtml(item)) }).catch(console.error) // get brickseek html function getBsHtml (item) { let reqBody = `zip=${zip}&item_id=${item.itemId}` let reqOpts = { hostname: 'brickseek.com', port: 80, path: `/walmart-inventory-checker?sku=${item.itemId}`, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(reqBody) } } let body = '' let req = http.request(reqOpts, (res) => { res.setEncoding('utf8') res.on('data', (chunk) => { body += chunk }) res.on('end', () => processBsResults(item, body)) }) req.on('error', (err) => cb ? cb(err) : console.error(err)) req.write(reqBody) req.end() } // process results function processBsResults (wmItem, htmlText) { let $ = cheerio.load(htmlText) let rows = $('#main').find('.bsapi-inventory-checker-stores').find('tr') let item = { bsUrl: `http://brickseek.com/walmart-inventory-checker?sku=${wmItem.itemId}` } keepProps.forEach((k) => { item[k] = wmItem[k] }) item.inventory = rows.map(function () { let addrHtml = $(this).find('.store-address').html() || '' let priceHtml = $(this).find('.store-price-number').html() || '' let stockHtml = $(this).find('.store-quan').html() || '' return { address: addrHtml.replace(/<(\/)?b>/g, '').split('<br>') || [], price: parseFloat(priceHtml.replace('$', '').trim()), stock: parseInt(((stockHtml.split('</b>:') || [])[1] || '').trim(), 10) } }).get().filter((i) => i.price) out.push(item) if (out.length === listLength) { cb ? cb(null, out) : console.log(out) return self } } return self }