From 3a03df9c014d4ab331f7e514bb18b4325b7bd882 Mon Sep 17 00:00:00 2001 From: Edward Peterson Date: Fri, 16 Dec 2022 14:58:00 -0500 Subject: [PATCH] Initial commit --- LotListing.js | 70 ++++++++++++++ bricklink.js | 54 +++++++++++ index.js | 100 ++++++++++++++++++++ package-lock.json | 233 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 15 +++ 5 files changed, 472 insertions(+) create mode 100644 LotListing.js create mode 100644 bricklink.js create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/LotListing.js b/LotListing.js new file mode 100644 index 0000000..14dab78 --- /dev/null +++ b/LotListing.js @@ -0,0 +1,70 @@ + +module.exports = class { + constructor(listing) { + this.listingId = listing.idInv; + this.description = listing.strDesc; + this.condition = listing.codeNew === 'U' ? 'USED' : 'NEW'; + this.complete = listing.codeComplete; + this.quantity = listing.n4Qty; + this.colorDesc = listing.strColor; + this.colorId = listing.idColor; + this.minBuy = parseMinBuy(listing.mMinBuy); + this.sellerUsername = listing.strSellerUsername; + this.sellerStoreName = listing.strStorename; + this.instantCheckout = listing.instantCheckout; + this.prices = [ + { + quantity: 1, + value: parseDollars(listing.mDisplaySalePrice) + } + ] + if(listing.nTier1Qty) { + //add bulk pricing tier + this.prices.push({ + quantity: listing.nTier1Qty, + value: parseDollars(listing.nTier1DisplayPrice) + }) + } + if(listing.nTier2Qty) { + //add bulk pricing tier + this.prices.push({ + quantity: listing.nTier2Qty, + value: parseDollars(listing.nTier2DisplayPrice) + }) + } + if(listing.nTier3Qty) { + //add bulk pricing tier + this.prices.push({ + quantity: listing.nTier3Qty, + value: parseDollars(listing.nTier3DisplayPrice) + }) + } + } + + getPrice(quantity) { + for(var i = this.prices.length - 1;i >= 0;i--){ + const price = this.prices[i]; + console.log(price, quantity, i) + + if(quantity >= price.quantity){ + return price.value * quantity + } + } + return null; + } + +} +const priceRegex = /US \$(?\d+\.\d+)/; +function parseMinBuy(value){ + if(value === 'None') return 0; + return parseDollars(value); +} +function parseDollars(val) { + const regexed = priceRegex.exec(val); + const stringPrice = regexed?.groups?.price; + if(!stringPrice){ + return 999999999; + } else { + return parseFloat(stringPrice); + } +} \ No newline at end of file diff --git a/bricklink.js b/bricklink.js new file mode 100644 index 0000000..1109dbd --- /dev/null +++ b/bricklink.js @@ -0,0 +1,54 @@ +const superagent = require('superagent'); +const LotListing = require('./LotListing') +const brickLinkAgent = superagent.agent(); +module.exports = { + getInternalPartId(legoPartNumber) { + return new Promise((resolve, reject) => { + brickLinkAgent.get(`https://www.bricklink.com/v2/catalog/catalogitem.page?P=${legoPartNumber}`) + // .set('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0') + .then((response => { + const partIdRegex = /idItem:\s+(?\d+)/gm; + const matches = partIdRegex.exec(response.text); + resolve(matches.groups?.idItem) + })).catch(error => { + console.error('HTTP error from internalPartId', error); + reject(error); + }) + + }) + }, + getListings(internalPartId) { + return new Promise((resolve, reject) => { + brickLinkAgent.get(`https://www.bricklink.com/ajax/clone/catalogifs.ajax`) + .query({ + itemid: internalPartId, + loc: 'US' + }) + // .set('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0') + .set('Accept', 'application/json').then(response => { + if(!response.body.list) { + return reject('no list found'); + } + resolve(response.body.list.map(l => new LotListing(l))); + }).catch(error => { + console.error('HTTP error from getListings', error) + reject(error); + }) + }) + + }, + getStoreId(sellerUsername) { + return new Promise((resolve, reject) => { + brickLinkAgent.get(`https://store.bricklink.com/${sellerUsername}`) + .then((response => { + const storeIdRegex = /username:\s+'.+',\s+id:\s+(?\d+)/gm; + const matches = storeIdRegex.exec(response.text); + resolve(matches.groups?.storeId) + })).catch(error => { + console.error('HTTP error from getStoreId', error) + reject(error); + }) + + }) + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..4386abf --- /dev/null +++ b/index.js @@ -0,0 +1,100 @@ +const brickLinkAPI = require('./bricklink'); +const _ = require('lodash'); +const cart = [ + { + partNumber: '4592c01', + quantity: 2 + }, + { + partNumber: '3962b', + quantity: 1 + }, + { + partNumber: '3022', + quantity: 1 + }, + { + partNumber: '4081b', + quantity: 4 + }, + { + partNumber: '4740', + quantity: 1 + }, + { + partNumber: '3031', + quantity: 1 + }, + { + partNumber: '30303', + quantity: 1 + }, + { + partNumber: '3049b', + quantity: 1 + }, + { + partNumber: '3069bpc1', + quantity: 1 + }, + { + partNumber: '3004', + quantity: 1 + }, + { + partNumber: '2342', + quantity: 1 + }, +] + Promise.each = async function(arr, fn) { // take an array and a function + const results = []; + for(const item of arr) results.push(await item); + return results; + } + + +Promise.each(cart.map(lot => { + return new Promise((resolve, reject) => { + brickLinkAPI.getInternalPartId(lot.partNumber).then((blId) => { + brickLinkAPI.getListings(blId).then(listings => { + console.log(listings.length + 'Listings found'); + const lotsWithEnoughItems = listings.filter(l => l.quantity >= lot.quantity); + // const lotsWithEnoughItemsAndAboveMinBuy = lotsWithEnoughItems.filter(lotf => lotf.minBuy === 'None' || (lotf.getPrice(lot.quantity) > lotf.minBuy)).map(lotf => { + // lotf.calculatedPrice = lotf.getPrice(lot.quantity); + // return lotf; + // }) + setTimeout(() => { + resolve(lotsWithEnoughItems); + + }, 1000) + }).catch(error => { + console.error('Failed to fetch listings', error); + reject(error); + }) + }).catch(error => { + console.error('Failed to fetch internal id', error); + reject(error); + }) + }); +})).then(parts => { + var groupedLots = []; + const possibleLots = _.flatten(parts); + + possibleLots.forEach(lot => { + const group = _.findIndex(groupedLots, l => l.sellerUsername === lot.sellerUsername); + if(group === -1) { + const newGroup = { + sellerUsername: lot.sellerUsername, + minBuy: lot.minBuy, + lots: [lot] + } + groupedLots.push(newGroup); + + } else { + groupedLots[group].lots.push(lot); + } + }) + + console.log(groupedLots.filter(gl => gl.lots.length > 1)); +}) + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5560c56 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,233 @@ +{ + "name": "bricklinkaggregator", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "cookiejar": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e87cec --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "bricklinkaggregator", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.21", + "superagent": "^8.0.6" + } +}