├── .editorconfig ├── .env-example ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yaml ├── pull_request_template.md ├── workflows │ ├── cd.yaml │ └── ci.yaml └── xo-problem-matcher.json ├── .gitignore ├── .node-version ├── CHANGELOG.md ├── LICENSE ├── README.md ├── media └── screenshot.png ├── nodemon.json ├── package-lock.json ├── package.json ├── src ├── __test__ │ └── notification-test.ts ├── adblocker.ts ├── banner.ts ├── config.ts ├── index.ts ├── logger.ts ├── notification │ ├── desktop.ts │ ├── discord.ts │ ├── email.ts │ ├── index.ts │ ├── notification.ts │ ├── pushbullet.ts │ ├── pushover.ts │ ├── slack.ts │ ├── sms.ts │ ├── sound.ts │ ├── telegram.ts │ └── twitter.ts ├── store │ ├── fetch-links.ts │ ├── filter.ts │ ├── includes-labels.ts │ ├── index.ts │ ├── lookup.ts │ ├── model │ │ ├── adorama.ts │ │ ├── amazon-ca.ts │ │ ├── amazon-de.ts │ │ ├── amazon-nl.ts │ │ ├── amazon.ts │ │ ├── asus.ts │ │ ├── bandh.ts │ │ ├── bestbuy-ca.ts │ │ ├── bestbuy.ts │ │ ├── evga-eu.ts │ │ ├── evga.ts │ │ ├── helpers │ │ │ ├── card.ts │ │ │ └── nvidia.ts │ │ ├── index.ts │ │ ├── microcenter.ts │ │ ├── newegg-ca.ts │ │ ├── newegg.ts │ │ ├── nvidia-api.ts │ │ ├── nvidia.ts │ │ ├── officedepot.ts │ │ ├── pny.ts │ │ ├── store.ts │ │ └── zotac.ts │ └── timestamp-url-parameter.ts ├── types │ ├── play-sound.d.ts │ ├── puppeteer-extra-plugin-block-resources.d.ts │ ├── pushbullet.d.ts │ └── pushover-notifications.d.ts └── util.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | # ** All configuration variables are optional ** 2 | # Read https://github.com/jef/nvidia-snatcher#customization for help on customizing this file 3 | ############################################################################################# 4 | 5 | BROWSER_TRUSTED="" 6 | DISCORD_NOTIFY_GROUP="" 7 | DISCORD_WEB_HOOK="" 8 | EMAIL_USERNAME="" 9 | EMAIL_TO="" 10 | EMAIL_PASSWORD="" 11 | HEADLESS="" 12 | IN_STOCK_WAIT_TIME="" 13 | LOG_LEVEL="" 14 | LOW_BANDWIDTH="" 15 | MICROCENTER_LOCATION="" 16 | OPEN_BROWSER="" 17 | PAGE_TIMEOUT="" 18 | PHONE_NUMBER="" 19 | PHONE_CARRIER="" 20 | PLAY_SOUND="" 21 | PUSHBULLET="" 22 | PUSHOVER_TOKEN="" 23 | PUSHOVER_USER="" 24 | PUSHOVER_PRIORITY="" 25 | PAGE_BACKOFF_MIN="" 26 | PAGE_BACKOFF_MAX="" 27 | PAGE_SLEEP_MIN="" 28 | PAGE_SLEEP_MAX="" 29 | SHOW_ONLY_BRANDS="" 30 | SHOW_ONLY_MODELS="" 31 | SHOW_ONLY_SERIES="" 32 | SLACK_CHANNEL="" 33 | SLACK_TOKEN="" 34 | STORES="" 35 | COUNTRY="" 36 | SCREENSHOT="false" 37 | TELEGRAM_ACCESS_TOKEN="" 38 | TELEGRAM_CHAT_ID="" 39 | TWITTER_CONSUMER_KEY="" 40 | TWITTER_CONSUMER_SECRET="" 41 | TWITTER_ACCESS_TOKEN_KEY="" 42 | TWITTER_ACCESS_TOKEN_SECRET="" 43 | TWITTER_TWEET_TAGS="" 44 | USER_AGENT="" 45 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jef 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jef 2 | custom: ["https://www.paypal.me/jxf"] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Report a bug for this project 4 | title: '' 5 | labels: 'bug' 6 | assignees: jef 7 | 8 | --- 9 | 10 | ### Description 11 | 12 | 13 | 14 | 15 | 16 | ### Possible solution 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: Suggest a feature for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: jef 7 | 8 | --- 9 | 10 | ### Description 11 | 12 | 13 | 14 | ### Possible solution 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "docker" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "dependencies" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | labels: 14 | - "dependencies" 15 | - package-ecosystem: "npm" 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | labels: 20 | - "dependencies" 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ### Description 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ### Testing 14 | 15 | 16 | 17 | 18 | 19 | ### New dependencies 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: cd 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | cd: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: GoogleCloudPlatform/release-please-action@v2.4.1 12 | id: release 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | release-type: node 16 | package-name: nvidia-snatcher 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | build-lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - uses: actions/setup-node@v2.1.1 12 | with: 13 | node-version: 14 14 | - uses: actions/cache@v2 15 | with: 16 | path: ~/.npm 17 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 18 | restore-keys: ${{ runner.os }}-node- 19 | - name: Add problem matcher 20 | run: echo "::add-matcher::.github/xo-problem-matcher.json" 21 | - name: Pull dependencies 22 | run: | 23 | npm ci 24 | npm run build 25 | npm run lint 26 | -------------------------------------------------------------------------------- /.github/xo-problem-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "xo", 5 | "pattern": [ 6 | { 7 | "regexp": "^\\s+(.*):(\\d+):(\\d+)$", 8 | "file": 1 9 | }, 10 | { 11 | "regexp": "^\\s+✖\\s+(\\d+):(\\d+)\\s+(.*)\\s+(.*)$", 12 | "line": 1, 13 | "column": 2, 14 | "message": 3, 15 | "code": 4, 16 | "loop": true 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vs/ 3 | .vscode/ 4 | build/ 5 | node_modules/ 6 | 7 | .env 8 | success-*.png 9 | 10 | *.wav 11 | *.mp3 12 | *.flac 13 | *.exe -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 14.11.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.5.0](https://www.github.com/jef/nvidia-snatcher/compare/v1.4.0...v1.5.0) (2020-09-24) 4 | 5 | 6 | ### Features 7 | 8 | * filter models ([#261](https://www.github.com/jef/nvidia-snatcher/issues/261)) ([e1b34a9](https://www.github.com/jef/nvidia-snatcher/commit/e1b34a9ccfa45fa1a11da9af9074059b6084904b)) 9 | * **log:** colors for console logs ([#207](https://www.github.com/jef/nvidia-snatcher/issues/207)) ([0ad67fe](https://www.github.com/jef/nvidia-snatcher/commit/0ad67fe20453898ce0a6b5faff00062735411119)) 10 | * **notification:** add desktop notifications ([#140](https://www.github.com/jef/nvidia-snatcher/issues/140)) ([722eaf3](https://www.github.com/jef/nvidia-snatcher/commit/722eaf3cd680c4600b79f842c6c5acdb9e51ad71)) 11 | * **notification:** add pushbullet, add url with notifications ([#226](https://www.github.com/jef/nvidia-snatcher/issues/226)) ([74490ea](https://www.github.com/jef/nvidia-snatcher/commit/74490eae3ab30de7d7a708d5dd970e070f27f2ea)) 12 | * **notification:** twitter integration ([#224](https://www.github.com/jef/nvidia-snatcher/issues/224)) ([908ed35](https://www.github.com/jef/nvidia-snatcher/commit/908ed358826f9de530f5892ded1a54964a304d15)) 13 | * **store:** add `bannedSeller` label for stores ([#173](https://www.github.com/jef/nvidia-snatcher/issues/173)) ([71c6774](https://www.github.com/jef/nvidia-snatcher/commit/71c6774511f7ba13d34d2e40b69abf52d06e6225)) 14 | * **store:** add amazon-de ([#167](https://www.github.com/jef/nvidia-snatcher/issues/167)) ([8a70f14](https://www.github.com/jef/nvidia-snatcher/commit/8a70f147438584cc334710bc66220d05eb32fcbd)) 15 | * **store:** add bestbuy.ca ([#229](https://www.github.com/jef/nvidia-snatcher/issues/229)) ([22fd22f](https://www.github.com/jef/nvidia-snatcher/commit/22fd22fe743d3e286eae3430aecd6e7a0a5de8c0)) 16 | * **store:** add evga eu ([#172](https://www.github.com/jef/nvidia-snatcher/issues/172)) ([605bdd7](https://www.github.com/jef/nvidia-snatcher/commit/605bdd7ca73c585734f6c5df1a86f4fbfbff9163)) 17 | * **store:** add evga model ([#220](https://www.github.com/jef/nvidia-snatcher/issues/220)) ([190388c](https://www.github.com/jef/nvidia-snatcher/commit/190388cfe4a5e3f19abccd0ff786f654b9a04d2f)) 18 | * **store:** add microcenter store location config ([#215](https://www.github.com/jef/nvidia-snatcher/issues/215)) ([d6a27c9](https://www.github.com/jef/nvidia-snatcher/commit/d6a27c988c7b1011c7a10084d8283a60ed8aea5c)) 19 | * **stores:** add 3090 for bestbuy, newegg ([#249](https://www.github.com/jef/nvidia-snatcher/issues/249)) ([dd45dba](https://www.github.com/jef/nvidia-snatcher/commit/dd45dba82cb86f7e7664298dd202b93bbbd46d9f)) 20 | * **stores:** add 3090s for amazon-ca, bestbuy-ca, newegg-ca ([#258](https://www.github.com/jef/nvidia-snatcher/issues/258)) ([482fb58](https://www.github.com/jef/nvidia-snatcher/commit/482fb58cbfde6f95fb6f77de790d76e6aa2a5926)) 21 | * add chromium sandbox skipping ([#209](https://www.github.com/jef/nvidia-snatcher/issues/209)) ([2065680](https://www.github.com/jef/nvidia-snatcher/commit/20656805c1259637bb3a4db465a8d16d4780296a)) 22 | * deprecate nvidia (api), add 3080 add 3090 ([9f470f0](https://www.github.com/jef/nvidia-snatcher/commit/9f470f06e9e9fb605d340c0b0f9016d7288e8c0b)) 23 | * invert logic ([#141](https://www.github.com/jef/nvidia-snatcher/issues/141)) ([6608a79](https://www.github.com/jef/nvidia-snatcher/commit/6608a79769ff03543ab4ed2f2cead3410d7d7e99)) 24 | * multiple discord roles and webhooks, qol for envs ([#260](https://www.github.com/jef/nvidia-snatcher/issues/260)) ([8913879](https://www.github.com/jef/nvidia-snatcher/commit/8913879593252c9c83020b2e2c46bad7537b2a20)) 25 | * **store:** add newegg.ca ([#160](https://www.github.com/jef/nvidia-snatcher/issues/160)) ([76f5849](https://www.github.com/jef/nvidia-snatcher/commit/76f584988979a40269fd3641e996800a63b4b163)), closes [#159](https://www.github.com/jef/nvidia-snatcher/issues/159) 26 | * **store:** add office depot ([#157](https://www.github.com/jef/nvidia-snatcher/issues/157)) ([0df2dcf](https://www.github.com/jef/nvidia-snatcher/commit/0df2dcfbd48235fba7126d96cd912634c5b4fdd9)) 27 | * **store:** add zotac store ([#214](https://www.github.com/jef/nvidia-snatcher/issues/214)) ([7875855](https://www.github.com/jef/nvidia-snatcher/commit/78758552b22e608dbdf3e76397f5b5efb893fef5)) 28 | * add delay on captcha to try and evade faster ([#119](https://www.github.com/jef/nvidia-snatcher/issues/119)) ([4f83b3b](https://www.github.com/jef/nvidia-snatcher/commit/4f83b3b233657841a4068a8ff9dd6c8dbff631c0)) 29 | * bestbuy bypass international splash, newegg add to cart ([#153](https://www.github.com/jef/nvidia-snatcher/issues/153)) ([133a54f](https://www.github.com/jef/nvidia-snatcher/commit/133a54fa170bb16dd26b0d72b1a02c56b3851b7f)) 30 | * card series filter, fix: newegg `oosLabels` ([#120](https://www.github.com/jef/nvidia-snatcher/issues/120)) ([252459d](https://www.github.com/jef/nvidia-snatcher/commit/252459d5d3de2b8cb25deee9ae318108e3dda2be)) 31 | * custom user agent ([#121](https://www.github.com/jef/nvidia-snatcher/issues/121)) ([d9be3fe](https://www.github.com/jef/nvidia-snatcher/commit/d9be3fe6183eaa9694b186c7a75e1f28bb31dace)) 32 | * include screenshot for emails + sms notifications ([#144](https://www.github.com/jef/nvidia-snatcher/issues/144)) ([7191e03](https://www.github.com/jef/nvidia-snatcher/commit/7191e03a80e577b59b2861289aa658cfa0ffc0fa)) 33 | * load puppeteer faster, run stores in parallel ([#83](https://www.github.com/jef/nvidia-snatcher/issues/83)) ([d1a5aa1](https://www.github.com/jef/nvidia-snatcher/commit/d1a5aa1f02ff0a8f293b93e3c078b5943908a95b)) 34 | * set country in config, login to nvidia when starting ([#162](https://www.github.com/jef/nvidia-snatcher/issues/162)) ([ebd6091](https://www.github.com/jef/nvidia-snatcher/commit/ebd6091a09fb5e52a66742767ae4b58323cd7447)) 35 | * temporarily pause requests if store has stock ([#147](https://www.github.com/jef/nvidia-snatcher/issues/147)) ([6413144](https://www.github.com/jef/nvidia-snatcher/commit/6413144c1cae89f33f852cc93870b407a784f2bb)) 36 | * update for complex add to cart, fix nvidia ([#108](https://www.github.com/jef/nvidia-snatcher/issues/108)) ([3ea146d](https://www.github.com/jef/nvidia-snatcher/commit/3ea146da14ea40d145ccfc05436beeb0a9fed8d9)) 37 | * **notification:** discord integration ([#82](https://www.github.com/jef/nvidia-snatcher/issues/82)) ([a3fc07d](https://www.github.com/jef/nvidia-snatcher/commit/a3fc07daf0a3f33f18e03d4cfc13d3477a9c4fa0)) 38 | * **scraping:** change lookup impl, add randomize sleep ([#110](https://www.github.com/jef/nvidia-snatcher/issues/110)) ([dc0f710](https://www.github.com/jef/nvidia-snatcher/commit/dc0f7106749b0afa0ff1c91cabb90b65be30e909)) 39 | * **store:** add adorama ([#104](https://www.github.com/jef/nvidia-snatcher/issues/104)) ([5b91065](https://www.github.com/jef/nvidia-snatcher/commit/5b910650430ad4806b22722efa9a013e72ea47e7)) 40 | * **store:** add asus ([#102](https://www.github.com/jef/nvidia-snatcher/issues/102)) ([a501cf7](https://www.github.com/jef/nvidia-snatcher/commit/a501cf703bb05f47af6240a4b16a3dc4dcf3baf5)) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * **store:** adorama captcha config ([#234](https://www.github.com/jef/nvidia-snatcher/issues/234)) ([9a53917](https://www.github.com/jef/nvidia-snatcher/commit/9a539175860f98de3b023009f751e59d94f0aaef)) 46 | * color logs and notification ([76b28a6](https://www.github.com/jef/nvidia-snatcher/commit/76b28a6dbdf5480c12a8c82b031c3f2880d17b11)) 47 | * **notification:** change discord ping visibility ([#168](https://www.github.com/jef/nvidia-snatcher/issues/168)) ([9675c5b](https://www.github.com/jef/nvidia-snatcher/commit/9675c5b8d61226db4652964e7f1e7399bb82d04e)) 48 | * **store:** bandh removed cards ([#201](https://www.github.com/jef/nvidia-snatcher/issues/201)) ([6409646](https://www.github.com/jef/nvidia-snatcher/commit/6409646d57bf2b2bb5a4bcf8239740abed8edafb)) 49 | * `rateLimitTimeout` not being defaulted ([#106](https://www.github.com/jef/nvidia-snatcher/issues/106)) ([28947be](https://www.github.com/jef/nvidia-snatcher/commit/28947be9bc8981d7a45a5d0e69c18d039fcd9ed3)) 50 | * check response for rate limiting ([#58](https://www.github.com/jef/nvidia-snatcher/issues/58)) ([#98](https://www.github.com/jef/nvidia-snatcher/issues/98)) ([b7d9462](https://www.github.com/jef/nvidia-snatcher/commit/b7d9462e794ef3961fb57c79ef8f66e77d25d20a)) 51 | * keep single `Store` from draining ([e819e46](https://www.github.com/jef/nvidia-snatcher/commit/e819e46116d4e0b067a59791094b5cfbd2d7cd45)) 52 | * memory leak due to adblocker ([#139](https://www.github.com/jef/nvidia-snatcher/issues/139)) ([0f6e570](https://www.github.com/jef/nvidia-snatcher/commit/0f6e570cc817dfc10bcddc5743a0faf3b1489270)) 53 | * **nvidia:** false positives ([#132](https://www.github.com/jef/nvidia-snatcher/issues/132)) ([a75d214](https://www.github.com/jef/nvidia-snatcher/commit/a75d214dd555d5e0388cb54b15be324cc25b6a15)) 54 | * newegg out-of-stock ([#124](https://www.github.com/jef/nvidia-snatcher/issues/124)) ([770a13a](https://www.github.com/jef/nvidia-snatcher/commit/770a13ac3559401b430547908d1df014582c1e37)) 55 | * newegg out-of-stock labels ([#134](https://www.github.com/jef/nvidia-snatcher/issues/134)) ([19c8f18](https://www.github.com/jef/nvidia-snatcher/commit/19c8f188c796258c469c2b4c6461fc5da3907a47)) 56 | * **notification:** wrong condition for sounds playing ([#91](https://www.github.com/jef/nvidia-snatcher/issues/91)) ([103d96d](https://www.github.com/jef/nvidia-snatcher/commit/103d96dc81d6fd097fcdbed5bdd7487d7d73bf6e)) 57 | * **store:** false positives for nvidia. ([#85](https://www.github.com/jef/nvidia-snatcher/issues/85)) ([c65fa04](https://www.github.com/jef/nvidia-snatcher/commit/c65fa04666775060532e28076a0b4af50f8dd30b)) 58 | 59 | ## [1.4.0](https://www.github.com/jef/nvidia-snatcher/compare/v1.3.0...v1.4.0) (2020-09-19) 60 | 61 | 62 | ### Features 63 | 64 | * **notification:** add mint mobile carrier ([#70](https://www.github.com/jef/nvidia-snatcher/issues/70)) ([8aba7ec](https://www.github.com/jef/nvidia-snatcher/commit/8aba7ecbdb0bfce06257b7b9066e8fccbd82e47e)) 65 | * **notification:** add pushover ([#55](https://www.github.com/jef/nvidia-snatcher/issues/55)) ([c85658b](https://www.github.com/jef/nvidia-snatcher/commit/c85658bf82fdf360e5e9d8345eaa846f0572e67c)) 66 | * **notification:** add telegram ([#71](https://www.github.com/jef/nvidia-snatcher/issues/71)) ([393d5f6](https://www.github.com/jef/nvidia-snatcher/commit/393d5f689887bf1d6f30a37eea163b2e6bbd4efa)) 67 | * **notification:** add telus sms ([6be74a1](https://www.github.com/jef/nvidia-snatcher/commit/6be74a19f3d3f999145d17ac8e91c59db2502071)) 68 | * **store:** add amazon.ca, fix timeout ([#75](https://www.github.com/jef/nvidia-snatcher/issues/75)) ([d4de1a4](https://www.github.com/jef/nvidia-snatcher/commit/d4de1a4638e903eb9518354ab6fb2f8c4befc347)) 69 | * webpage toggle, sound notification, fix evga links ([#52](https://www.github.com/jef/nvidia-snatcher/issues/52)) ([a217409](https://www.github.com/jef/nvidia-snatcher/commit/a21740942bbbbe967948062fa06cfc82c31eb755)) 70 | 71 | 72 | ### Performance Improvements 73 | 74 | * browser abstraction ([#68](https://www.github.com/jef/nvidia-snatcher/issues/68)) ([#81](https://www.github.com/jef/nvidia-snatcher/issues/81)) ([ebbdfe3](https://www.github.com/jef/nvidia-snatcher/commit/ebbdfe3f6378516112f4b6e004bbd6ccf13af685)) 75 | 76 | ## [1.3.0](https://www.github.com/jef/nvidia-snatcher/compare/v1.2.0...v1.3.0) (2020-09-19) 77 | 78 | 79 | ### Features 80 | 81 | * **logging:** add timestamp ([#48](https://www.github.com/jef/nvidia-snatcher/issues/48)) ([6c3cd01](https://www.github.com/jef/nvidia-snatcher/commit/6c3cd016850d03a6c6a894cab24ba2d3781a9af1)) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * **store:** amazon captcha false-positives ([#54](https://www.github.com/jef/nvidia-snatcher/issues/54)) ([5c9e0b6](https://www.github.com/jef/nvidia-snatcher/commit/5c9e0b6d06bd7e1223a7587fec067c8e79c9cfd6)) 87 | * evga xc3 ultra link ([#56](https://www.github.com/jef/nvidia-snatcher/issues/56)) ([d907092](https://www.github.com/jef/nvidia-snatcher/commit/d907092b443b056605e09cb2ca3e94e6ca811d9e)) 88 | * screenshot size, add screenshot config setting ([#53](https://www.github.com/jef/nvidia-snatcher/issues/53)) ([7cfc7c7](https://www.github.com/jef/nvidia-snatcher/commit/7cfc7c74429c808fa14468cdd497eb9f9aeb922c)) 89 | * sms carrier config, add google carrier ([#44](https://www.github.com/jef/nvidia-snatcher/issues/44)) ([971fec2](https://www.github.com/jef/nvidia-snatcher/commit/971fec20e441e2b12a38d5c8d17d2d4cb5e64d6b)) 90 | 91 | ## [1.2.0](https://www.github.com/jef/nvidia-snatcher/compare/v1.1.0...v1.2.0) (2020-09-19) 92 | 93 | 94 | ### Features 95 | 96 | * **ci:** add `npm run build` ([faad3e6](https://www.github.com/jef/nvidia-snatcher/commit/faad3e68efafaab135b77080b02af83429b6eca6)) 97 | * **store:** microcenter ([#39](https://www.github.com/jef/nvidia-snatcher/issues/39)) ([edf17e9](https://www.github.com/jef/nvidia-snatcher/commit/edf17e926f3d186e7630da2834d78de3e540a956)) 98 | * add Amazon links ([#26](https://www.github.com/jef/nvidia-snatcher/issues/26)) ([f0560ce](https://www.github.com/jef/nvidia-snatcher/commit/f0560ce72bfbfdd6360b85e23edaa875d58f228f)) 99 | * add email test, fix memory leak ([#24](https://www.github.com/jef/nvidia-snatcher/issues/24)) ([a2fb973](https://www.github.com/jef/nvidia-snatcher/commit/a2fb97333c6eb81250b24ccb6859e9356acded21)) 100 | * more Best Buy AIBs ([#41](https://www.github.com/jef/nvidia-snatcher/issues/41)) ([7d7bd18](https://www.github.com/jef/nvidia-snatcher/commit/7d7bd18b4dd656ec01ef2fb2d8519e2a7f34ef70)) 101 | * page timeout ([#22](https://www.github.com/jef/nvidia-snatcher/issues/22)) ([643045c](https://www.github.com/jef/nvidia-snatcher/commit/643045c7e0158fb6526bd09427b96cce7958bcea)) 102 | * slack integration ([#34](https://www.github.com/jef/nvidia-snatcher/issues/34)) ([c0a881a](https://www.github.com/jef/nvidia-snatcher/commit/c0a881a16ebb573bf35b7f29cb27e5b3c2e1fe78)) 103 | * sms notification for usa carriers ([#40](https://www.github.com/jef/nvidia-snatcher/issues/40)) ([5a3636b](https://www.github.com/jef/nvidia-snatcher/commit/5a3636bcb639bb33bc586af96264f5df2f3a8307)) 104 | * update to check if FE in-stock via Digital River ([#29](https://www.github.com/jef/nvidia-snatcher/issues/29)) ([00ede13](https://www.github.com/jef/nvidia-snatcher/commit/00ede13501082f530ea672a349816be1d31621a8)) 105 | 106 | 107 | ### Bug Fixes 108 | 109 | * small error in `isOutOfStock` logic ([#33](https://www.github.com/jef/nvidia-snatcher/issues/33)) ([c2a210c](https://www.github.com/jef/nvidia-snatcher/commit/c2a210cc815c3aa06f6f14d33954f65577d95954)) 110 | 111 | ## [1.1.0](https://www.github.com/jef/nvidia-snatcher/compare/v1.0.0...v1.1.0) (2020-09-18) 112 | 113 | 114 | ### Features 115 | 116 | * add conventional commits ([#14](https://www.github.com/jef/nvidia-snatcher/issues/14)) ([eb4f5e0](https://www.github.com/jef/nvidia-snatcher/commit/eb4f5e034176a286eabe381c98ced77cd197d7fb)) 117 | * add evga ([#17](https://www.github.com/jef/nvidia-snatcher/issues/17)) ([#18](https://www.github.com/jef/nvidia-snatcher/issues/18)) ([6c65032](https://www.github.com/jef/nvidia-snatcher/commit/6c6503219f7c188783c24a44f7052b276a4b39a3)) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * exception handling `TimeoutError` ([#20](https://www.github.com/jef/nvidia-snatcher/issues/20)) ([#21](https://www.github.com/jef/nvidia-snatcher/issues/21)) ([00a0687](https://www.github.com/jef/nvidia-snatcher/commit/00a0687d3eba6a8fca871161b447529be00c8896)) 123 | 124 | ## 1.0.0 (2020-09-18) 125 | 126 | 127 | ### Features 128 | 129 | * use ts, update cd, update `README` ([#12](https://www.github.com/jef/nvidia-snatcher/issues/12)) ([e9fc0bf](https://www.github.com/jef/nvidia-snatcher/commit/e9fc0bf5f770481d5e508d8b520e1020624e05d2)) 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jef LeCompte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvidia-snatcher [![ci](https://github.com/jef/nvidia-snatcher/workflows/ci/badge.svg)](https://github.com/jef/nvidia-snatcher/actions?query=workflow%3Aci) [![discord](https://img.shields.io/discord/756303724095471617.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/Cyc7nrz) 2 | 3 | [FAQ](#FAQ) | [Issues](https://github.com/jef/nvidia-snatcher/issues) | [Wiki](https://github.com/jef/nvidia-snatcher/wiki) 4 | 5 | ![nvidia-snatcher](media/screenshot.png) 6 | 7 | The purpose of this bot is to get an Nvidia card. It tries multiple things to do that. 8 | 9 | - Currently, `nvidia-snatcher` is not capable of purchasing a card for you 10 | - Scrapes multiple websites for patterns of being stocked 11 | - API requests are a work in progress (very soon) 12 | - Opens browser when stock is available 13 | - Sends an email to you when stock is avaiable (must have Gmail) 14 | 15 |
16 | What you may see if you're lucky 17 | 18 | ```sh 19 | 2020-09-18T07:06:28.535Z info :: ✖ [nvidia] nvidia founders edition is still out of stock 20 | 2020-09-18T07:06:31.241Z info :: ✖ [nvidia] nvidia founders edition is still out of stock 21 | 2020-09-18T07:06:34.212Z info :: ✖ [bestbuy] nvidia founder edition is still out of stock 22 | 2020-09-18T07:06:39.878Z info :: ✖ [bandh] gigabyte black is still out of stock 23 | 2020-09-18T07:06:43.236Z info :: ✖ [bestbuy] gigabyte black is still out of stock 24 | 2020-09-18T07:06:43.318Z info :: ↗ trying stores again 25 | 2020-09-18T07:06:43.318Z info :: 🚀🚀🚀 [nvidia] nvidia founders edition IN STOCK 🚀🚀🚀 26 | 2020-09-18T07:06:43.318Z info :: https://store.nvidia.com/store/nvidia/en_US/buy/productID.5438481700/clearCart.yes/nextPage.QuickBuyCartPage 27 | ``` 28 | 29 |
30 | 31 | > :point_right: You may get false positives from time to time, so I apologize for that. The library currently waits for all calls to be completed before parsing, but sometimes this can have unknown behavior. Patience is a virtue :) 32 | 33 | | | **Adorama** | **Amazon** | **Amazon (CA)** | **ASUS** | **B&H** | **Best Buy** | **Best Buy (CA)** | **EVGA** | **Micro Center** | **Newegg** | **Newegg (CA)** | **Nvidia** | **Office Depot** | **PNY** | **Zotac** | 34 | |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| 35 | | **3070**| | | | | | | | | | | | | | `✔` | | 36 | | **3080** | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | 37 | | **3090** | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | `✔` | | `✔` | `✔` | `✔` | | `✔` | `✔` | 38 | 39 | ## Installation and prerequisites 40 | 41 | Linux, macOS, and Windows are all capable operating systems. 42 | 43 | You do not need any computer skills, smarts, or anything of that nature. You are very capable as you have made it this far. Some basic understanding how a terminal, git, and or Node.js is a bonus, but that does not limit you to getting `nvidia-snatcher` running! 44 | 45 | ### Quick overview 46 | 47 | - [Node.js 14](https://nodejs.org/en/) 48 | - [git](https://git-scm.com/) 49 | - Clone this project `git clone https://github.com/jef/nvidia-snatcher.git` 50 | - Run `npm install` 51 | - Copy `.env.example` to a new file `.env` and edit the `.env` file to your liking using your [favorite text editor](https://code.visualstudio.com/) 52 | - More on this in [customization](#Customization) 53 | - Run `npm run start` to start 54 | 55 | At any point you want the program to stop, use Ctrl + C. 56 | 57 | > :point_right: Please visit the [wiki](https://github.com/jef/nvidia-snatcher/wiki) if you need more help with installation. 58 | 59 | ### Developer notes 60 | 61 | The command `npm run start:dev` can be used instead of `npm run start` to automatically restart the project when filesystem changes are detected in the `src/` folder or `.env` file. 62 | 63 | ### Customization 64 | 65 | To customize `nvidia-snatcher`, make a copy of `.env-example` as `.env` and make any changes to your liking. _All environment variables are **optional**._ 66 | 67 | Here is a list of variables that you can use to customize your newly copied `.env` file: 68 | 69 | | **Environment variable** | **Description** | **Notes** | 70 | |:---:|---|---| 71 | | `BROWSER_TRUSTED` | Skip Chromium Sandbox | Useful for containerized environments, default: `false` | 72 | | `DESKTOP_NOTIFICATIONS` | Display desktop notifications using [node-notifier](https://www.npmjs.com/package/node-notifier) | Default: `false` | 73 | | `DISCORD_NOTIFY_GROUP` | Discord group you would like to notify | Can be comma separated, use role ID, E.g.: `<@2834729847239842>` | 74 | | `DISCORD_WEB_HOOK` | Discord Web Hook URL | Can be comma separated, use whole webhook URL | 75 | | `EMAIL_USERNAME` | Gmail address | E.g.: `jensen.robbed.us@gmail.com` | 76 | | `EMAIL_TO` | Destination Email | Defaults to username if not set. Can be comma separated | 77 | | `EMAIL_PASSWORD` | Gmail password | See below if you have MFA | 78 | | `HEADLESS` | Puppeteer to run headless or not | Debugging related, default: `true` | 79 | | `IN_STOCK_WAIT_TIME` | Time to wait between requests to the same link if it has that card in stock | In seconds, default: `0` | 80 | | `LOG_LEVEL` | [Logging levels](https://github.com/winstonjs/winston#logging-levels) | Debugging related, default: `info` | 81 | | `LOW_BANDWIDTH` | Blocks images/fonts to reduce traffic | Disables ad blocker, default: `false` | 82 | | `MICROCENTER_LOCATION` | Specific MicroCenter location to search | Default : `web` | 83 | | `OPEN_BROWSER` | Toggle for whether or not the browser should open when item is found | Default: `true` | 84 | | `PAGE_TIMEOUT` | Navigation Timeout in milliseconds | `0` for infinite, default: `30000` | 85 | | `PHONE_NUMBER` | 10 digit phone number | E.g.: `1234567890`, email configuration required | 86 | | `PHONE_CARRIER` | [Supported carriers](#supported-carriers) for SMS | Email configuration required | 87 | | `PLAY_SOUND` | Play this sound notification if a card is found | Relative path accepted, valid formats: wav, mp3, flac, E.g.: `path/to/notification.wav`, [free sounds available](https://notificationsounds.com/) | 88 | | `PUSHBULLET` | PushBullet API key | Generate at https://www.pushbullet.com/#settings/account | | 89 | | `PUSHOVER_TOKEN` | Pushover access token | Generate at https://pushover.net/apps/build | | 90 | | `PUSHOVER_USER` | Pushover username | | 91 | | `PUSHOVER_PRIORITY` | Pushover message priority | 92 | | `PAGE_BACKOFF_MIN` | Minimum backoff time between retrying requests for the same store when a forbidden response is received | Default: `10000` | 93 | | `PAGE_BACKOFF_MAX` | Maximum backoff time between retrying requests for the same store when a forbidden response is received | Default: `3600000` | 94 | | `PAGE_SLEEP_MIN` | Minimum sleep time between queries of the same store | In milliseconds, default: `5000` | 95 | | `PAGE_SLEEP_MAX` | Maximum sleep time between queries of the same store | In milliseconds, default: `10000` | 96 | | `SCREENSHOT` | Capture screenshot of page if a card is found | Default: `true` | 97 | | `SHOW_ONLY_BRANDS` | Filter to show specified brands | Comma separated, e.g.: `evga,zotac` | 98 | | `SHOW_ONLY_MODELS` | Filter to show specified models | Comma separated, e.g.: `founders edition,rog strix` | 99 | | `SHOW_ONLY_SERIES` | Filter to show specified series | Comma separated, e.g.: `3080` | 100 | | `SLACK_CHANNEL` | Slack channel for posting | E.g.: `update`, no need for `#` | 101 | | `SLACK_TOKEN` | Slack API token | | 102 | | `STORES` | [Supported stores](#supported-stores) you want to be scraped | Comma separated, default: `nvidia` | 103 | | `COUNTRY` | [Supported country](#supported-countries) you want to be scraped | Currently only used by Nvidia, default: `usa` | 104 | | `SCREENSHOT` | Capture screenshot of page if a card is found | Default: `true` | 105 | | `TELEGRAM_ACCESS_TOKEN` | Telegram access token | | 106 | | `TELEGRAM_CHAT_ID` | Telegram chat ID | | 107 | | `USER_AGENT` | Custom User-Agent header for HTTP requests | Default: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36` | 108 | | `TWITTER_CONSUMER_KEY` | Twitter Consumer Key | Generate all Twitter keys at: https://developer.twitter.com/ | 109 | | `TWITTER_CONSUMER_SECRET` | Twitter Consumer Secret | | 110 | | `TWITTER_ACCESS_TOKEN_KEY` | Twitter Token Key | | 111 | | `TWITTER_ACCESS_TOKEN_SECRET` | Twitter Token Secret | | 112 | | `TWITTER_TWEET_TAGS` | Optional list of hashtags to append to the tweet message | E.g.: `#nvidia #nvidiastock` | 113 | 114 | > :point_right: If you have multi-factor authentication (MFA), you will need to create an [app password](https://myaccount.google.com/apppasswords) and use this instead of your Gmail password. 115 | 116 | > :point_right: You can find your computer's user agent by [searching google for "my user agent"](http://google.com/search?q=my+user+agent) 117 | 118 | > :point_right: You can test your notification configuration by running `npm run test:notification`. 119 | 120 | #### Supported stores 121 | 122 | | Stores | Environment variable | 123 | |:---:|:---:| 124 | | Adorama | `adorama`| 125 | | Amazon | `amazon`| 126 | | Amazon (CA) | `amazon-ca`| 127 | | Amazon (DE) | `amazon-de`| 128 | | Amazon (NL) | `amazon-nl`| 129 | | ASUS | `asus` | 130 | | B&H | `bandh`| 131 | | Best Buy | `bestbuy`| 132 | | Best Buy (CA) | `bestbuy-ca`| 133 | | EVGA | `evga`| 134 | | EVGA (EU) | `evga-eu`| 135 | | Micro Center | `microcenter`| 136 | | Newegg | `newegg`| 137 | | Newegg (CA) | `newegg-ca`| 138 | | Nvidia | `nvidia`| 139 | | Nvidia (API) | `nvidia-api`| 140 | | Office Depot | `officedepot`| 141 | | PNY | `pny`| 142 | | Zotac | `zotac`| 143 | 144 |
145 | Micro Center stores 146 | 147 | | **Store name** | 148 | |:---:| 149 | | `brooklyn` | 150 | | `brentwood` | 151 | | `cambridge` | 152 | | `chicago` | 153 | | `columbus` | 154 | | `dallas` | 155 | | `devin` | 156 | | `duluth` | 157 | | `fairfax` | 158 | | `flushing` | 159 | | `houston` | 160 | | `madison-heights` | 161 | | `marietta` | 162 | | `mayfield-heights` | 163 | | `north-jersey` | 164 | | `overland-park` | 165 | | `parkville` | 166 | | `rockville` | 167 | | `sharonville` | 168 | | `st-davids` | 169 | | `st-louis-park` | 170 | | `tustin` | 171 | | `westbury` | 172 | | `westmont` | 173 | | `yonkers` | 174 | 175 |
176 | 177 | #### Supported carriers 178 | 179 | | **Carrier** | **Environment variable** | **Notes** | 180 | |:---:|:---:|:---:| 181 | | AT&T | `att`| | 182 | | Bell | `bell` | | 183 | | Fido | `fido` | | 184 | | Google | `google`| | 185 | | Koodo | `koodo` | | 186 | | Mint | `mint`| | 187 | | Rogers | `rogers` | | 188 | | Sprint | `sprint`| | 189 | | Telus | `telus`| | 190 | | T-Mobile | `tmobile`| | 191 | | Verizon | `verizon`| Works with Visible | 192 | | Virgin | `virgin`| | 193 | | Virgin (CA) | `virgin-ca`| | 194 | 195 | #### Supported countries 196 | 197 | | **Country** | **Nvidia.com (3080 FE)** | **Nvidia.com (3090 FE)** | **Notes** | 198 | |:---:|:---:|:---:|:---:| 199 | | austria | `✔` | | | 200 | | belgium | `✔` | | Nvidia supports debug | 201 | | canada | `✔` | | | 202 | | czechia | `✔` | | | 203 | | denmark | `✔` | | | 204 | | finland | `✔` | | | 205 | | france | `✔` | | | 206 | | germany | `✔` | | | 207 | | great_britain | `✔` | | | 208 | | ireland | `✔` | | | 209 | | italy | `✔` | | | 210 | | luxembourg | `✔` | | Nvidia supports debug | 211 | | netherlands | `✔` | | Nvidia supports debug | 212 | | poland | `✔` | | | 213 | | portugal | `✔` | | | 214 | | russia | | | Missing all IDs | 215 | | spain | `✔` | | | 216 | | sweden | `✔` | | | 217 | | usa | `✔` | `✔` | Nvidia supports debug | 218 | 219 | ## FAQ 220 | 221 | **Q: What's Node.js and how do I install it?** Visit [their website](https://nodejs.org/en/) and download and install it. Very straight forward. Otherwise, Google more information related to your system needs. 222 | 223 | **Q: Will this harm my computer?** No. 224 | 225 | **Q: Have you gotten a card yet?** No. :cry: 226 | 227 | **Q: Will I get banned from of the stores?** Perhaps, but getting a card is a nice outcome. 228 | 229 | **Q: I got a problem and need help!** File an [issue](https://github.com/jef/nvidia-snatcher/issues/new/choose), I'll do my best to get to you. I work a full time job and this is only a hobby of mine. 230 | 231 | **Q: How do I get the latest code?** Take look at this [wiki page](https://github.com/jef/nvidia-snatcher/wiki/Troubleshoot:-General:-Getting-the-latest-code) 232 | 233 | **Q: Why don't my notifications work?** There are probably an [issue](https://github.com/jef/nvidia-snatcher/issues?q=is%3Aissue+sort%3Aupdated-desc+sound+is%3Aclosed) [that] has [already](https://github.com/jef/nvidia-snatcher/issues/182) [been](https://github.com/jef/nvidia-snatcher/issues/116) [resolved](https://github.com/jef/nvidia-snatcher/issues/155) 234 | 235 | **Q: I'd love to contribute, how do I do that?** Make a [pull request](https://github.com/jef/nvidia-snatcher/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc)! All contributions are welcome. 236 | 237 | **Q: Why do I have to download all this stuff just to get this bot working?** Well, I would rather you didn't either. See [#11](https://github.com/jef/nvidia-snatcher/issues/11). 238 | 239 | ### Acknowledgements 240 | 241 | Thanks to the great contributors that make this project possible 242 | 243 | Special shout to initial developers: 244 | 245 | - [@andirew](https://github.com/andirew) 246 | - [@fuckingrobot](https://github.com/fuckingrobot) 247 | - [@ioncaza](https://github.com/IonCaza) 248 | - [@malbert69](https://github.com/malbert69) 249 | -------------------------------------------------------------------------------- /media/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterPlatt/nvidia-snatcher/3bde805f2ce9eb5c4a7008f4af1ff0f45b1ba49a/media/screenshot.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "exec": "ts-node --files ./src/index", 3 | "ext": "ts", 4 | "watch": [ 5 | "src/", 6 | ".env" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nvidia-snatcher", 3 | "version": "1.5.0", 4 | "description": "🔮 For all your Nvidia needs", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "build": "rimraf ./build && tsc", 8 | "lint": "xo", 9 | "lint:fix": "xo --fix", 10 | "start": "npm run build && node build/index.js", 11 | "start:dev": "nodemon --config nodemon.json", 12 | "test:notification": "npm run build && node build/__test__/notification-test.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/jef/nvidia-snatcher.git" 17 | }, 18 | "keywords": [], 19 | "author": "jef", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/jef/nvidia-snatcher/issues" 23 | }, 24 | "homepage": "https://github.com/jef/nvidia-snatcher#readme", 25 | "dependencies": { 26 | "@slack/web-api": "^5.12.0", 27 | "chalk": "^4.1.0", 28 | "cheerio": "^1.0.0-rc.3", 29 | "discord-webhook-node": "^1.1.8", 30 | "dotenv": "^8.2.0", 31 | "messaging-api-telegram": "^1.0.1", 32 | "node-notifier": "^8.0.0", 33 | "nodemailer": "^6.4.11", 34 | "open": "^7.2.1", 35 | "play-sound": "^1.1.3", 36 | "puppeteer": "^5.3.1", 37 | "puppeteer-extra": "^3.1.15", 38 | "puppeteer-extra-plugin-adblocker": "^2.11.6", 39 | "puppeteer-extra-plugin-block-resources": "^2.2.7", 40 | "puppeteer-extra-plugin-stealth": "^2.6.1", 41 | "pushbullet": "^2.4.0", 42 | "pushover-notifications": "^1.2.2", 43 | "twitter": "^1.7.1", 44 | "winston": "^3.3.3" 45 | }, 46 | "devDependencies": { 47 | "@types/async": "^3.2.3", 48 | "@types/cheerio": "^0.22.22", 49 | "@types/node": "^14.11.2", 50 | "@types/node-notifier": "^8.0.0", 51 | "@types/nodemailer": "^6.4.0", 52 | "@types/puppeteer": "^3.0.2", 53 | "@types/twitter": "^1.7.0", 54 | "husky": "^4.3.0", 55 | "nodemon": "^2.0.4", 56 | "rimraf": "^3.0.2", 57 | "ts-node": "^9.0.0", 58 | "typescript": "^4.0.2", 59 | "xo": "^0.33.1" 60 | }, 61 | "xo": { 62 | "rules": { 63 | "sort-imports": "error", 64 | "sort-keys": "error", 65 | "sort-vars": "error" 66 | } 67 | }, 68 | "husky": { 69 | "hooks": { 70 | "pre-commit": "npm run lint" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/__test__/notification-test.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {sendNotification} from '../notification'; 3 | 4 | const link: Link = { 5 | brand: 'test:brand', 6 | cartUrl: 'https://www.example.com/cartUrl', 7 | model: 'test:model', 8 | series: 'test:series', 9 | url: 'https://www.example.com/url' 10 | }; 11 | 12 | const store: Store = { 13 | labels: { 14 | inStock: { 15 | container: 'test:container', 16 | text: ['test:text'] 17 | } 18 | }, 19 | links: [link], 20 | name: 'test:name' 21 | }; 22 | 23 | /** 24 | * Send test email. 25 | */ 26 | sendNotification(link, store); 27 | -------------------------------------------------------------------------------- /src/adblocker.ts: -------------------------------------------------------------------------------- 1 | import {Page} from 'puppeteer'; 2 | import {PuppeteerExtraPluginAdblocker} from 'puppeteer-extra-plugin-adblocker'; 3 | 4 | export const adBlocker = new PuppeteerExtraPluginAdblocker({ 5 | blockTrackers: true 6 | }); 7 | 8 | export async function disableBlockerInPage(page: Page) { 9 | const blockerObject = await adBlocker.getBlocker(); 10 | if (blockerObject.isBlockingEnabled(page)) { 11 | await blockerObject.disableBlockingInPage(page); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/banner.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const banner = chalk.green.bold(` 4 | $$\\ $$\\ $$\\ $$\\ $$\\ 5 | \\__| $$ |\\__| $$ | $$ | 6 | $$$$$$$\\ $$\\ $$\\ $$\\ $$$$$$$ |$$\\ $$$$$$\\ $$$$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$\\ $$$$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$\\ 7 | $$ __$$\\\\$$\\ $$ |$$ |$$ __$$ |$$ | \\____$$\\ $$$$$$\\ $$ _____|$$ __$$\\ \\____$$\\\\_$$ _| $$ _____|$$ __$$\\ $$ __$$\\ $$ __$$\\ 8 | $$ | $$ |\\$$\\$$ / $$ |$$ / $$ |$$ | $$$$$$$ |\\______|\\$$$$$$\\ $$ | $$ | $$$$$$$ | $$ | $$ / $$ | $$ |$$$$$$$$ |$$ | \\__| 9 | $$ | $$ | \\$$$ / $$ |$$ | $$ |$$ |$$ __$$ | \\____$$\\ $$ | $$ |$$ __$$ | $$ |$$\\ $$ | $$ | $$ |$$ ____|$$ | 10 | $$ | $$ | \\$ / $$ |\\$$$$$$$ |$$ |\\$$$$$$$ | $$$$$$$ |$$ | $$ |\\$$$$$$$ | \\$$$$ |\\$$$$$$$\\ $$ | $$ |\\$$$$$$$\\ $$ | 11 | \\__| \\__| \\_/ \\__| \\_______|\\__| \\_______| \\_______/ \\__| \\__| \\_______| \\____/ \\_______|\\__| \\__| \\_______|\\__| 12 | `); 13 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import {banner} from './banner'; 2 | console.log(banner); 3 | 4 | import {config} from 'dotenv'; 5 | import path from 'path'; 6 | 7 | config({path: path.resolve(__dirname, '../.env')}); 8 | 9 | /** 10 | * Returns environment variable, given array, or default array. 11 | * 12 | * @param environment Interested environment variable. 13 | * @param array Default array. If not set, is `[]`. 14 | */ 15 | function envOrArray(environment: string | undefined, array?: string[]): string[] { 16 | return environment ? environment.split(',') : (array ?? []); 17 | } 18 | 19 | /** 20 | * Returns environment variable, given boolean, or default boolean. 21 | * 22 | * @param environment Interested environment variable. 23 | * @param boolean Default boolean. If not set, is `true`. 24 | */ 25 | function envOrBoolean(environment: string | undefined, boolean?: boolean): boolean { 26 | return environment ? environment === 'true' : (boolean ?? true); 27 | } 28 | 29 | /** 30 | * Returns environment variable, given string, or default string. 31 | * 32 | * @param environment Interested environment variable. 33 | * @param string Default string. If not set, is `''`. 34 | */ 35 | function envOrString(environment: string | undefined, string?: string): string { 36 | return environment ? environment : (string ?? ''); 37 | } 38 | 39 | /** 40 | * Returns environment variable, given number, or default number. 41 | * 42 | * @param environment Interested environment variable. 43 | * @param number Default number. If not set, is `0`. 44 | */ 45 | function envOrNumber(environment: string | undefined, number?: number): number { 46 | return Number(environment ?? (number ?? 0)); 47 | } 48 | 49 | const browser = { 50 | isHeadless: envOrBoolean(process.env.HEADLESS), 51 | isTrusted: envOrBoolean(process.env.BROWSER_TRUSTED, false), 52 | lowBandwidth: envOrBoolean(process.env.LOW_BANDWIDTH, false), 53 | maxBackoff: envOrNumber(process.env.PAGE_BACKOFF_MAX, 3600000), 54 | maxSleep: envOrNumber(process.env.PAGE_SLEEP_MAX, 10000), 55 | minBackoff: envOrNumber(process.env.PAGE_BACKOFF_MIN, 10000), 56 | minSleep: envOrNumber(process.env.PAGE_SLEEP_MIN, 5000), 57 | open: envOrBoolean(process.env.OPEN_BROWSER) 58 | }; 59 | 60 | const logLevel = envOrString(process.env.LOG_LEVEL, 'info'); 61 | 62 | const notifications = { 63 | desktop: process.env.DESKTOP_NOTIFICATIONS === 'true', 64 | discord: { 65 | notifyGroup: envOrArray(process.env.DISCORD_NOTIFY_GROUP), 66 | webHookUrl: envOrArray(process.env.DISCORD_WEB_HOOK) 67 | }, 68 | email: { 69 | password: envOrString(process.env.EMAIL_PASSWORD), 70 | to: envOrString(process.env.EMAIL_TO, envOrString(process.env.EMAIL_USERNAME)), 71 | username: envOrString(process.env.EMAIL_USERNAME) 72 | }, 73 | phone: { 74 | availableCarriers: new Map([ 75 | ['att', 'txt.att.net'], 76 | ['bell', 'txt.bell.ca'], 77 | ['fido', 'fido.ca'], 78 | ['google', 'msg.fi.google.com'], 79 | ['koodo', 'msg.koodomobile.com'], 80 | ['mint', 'mailmymobile.net'], 81 | ['rogers', 'pcs.rogers.com'], 82 | ['sprint', 'messaging.sprintpcs.com'], 83 | ['telus', 'msg.telus.com'], 84 | ['tmobile', 'tmomail.net'], 85 | ['verizon', 'vtext.com'], 86 | ['virgin', 'vmobl.com'], 87 | ['virgin-ca', 'vmobile.ca'] 88 | ]), 89 | carrier: envOrString(process.env.PHONE_CARRIER), 90 | number: envOrString(process.env.PHONE_NUMBER) 91 | }, 92 | playSound: envOrString(process.env.PLAY_SOUND), 93 | pushBulletApiKey: envOrString(process.env.PUSHBULLET), 94 | pushover: { 95 | priority: envOrString(process.env.PUSHOVER_PRIORITY), 96 | token: envOrString(process.env.PUSHOVER_TOKEN), 97 | username: envOrString(process.env.PUSHOVER_USER) 98 | }, 99 | slack: { 100 | channel: envOrString(process.env.SLACK_CHANNEL), 101 | token: envOrString(process.env.SLACK_TOKEN) 102 | }, 103 | telegram: { 104 | accessToken: envOrString(process.env.TELEGRAM_ACCESS_TOKEN), 105 | chatId: envOrString(process.env.TELEGRAM_CHAT_ID) 106 | }, 107 | twitter: { 108 | accessTokenKey: envOrString(process.env.TWITTER_ACCESS_TOKEN_KEY), 109 | accessTokenSecret: envOrString(process.env.TWITTER_ACCESS_TOKEN_SECRET), 110 | consumerKey: envOrString(process.env.TWITTER_CONSUMER_KEY), 111 | consumerSecret: envOrString(process.env.TWITTER_CONSUMER_SECRET), 112 | tweetTags: envOrString(process.env.TWITTER_TWEET_TAGS) 113 | } 114 | }; 115 | 116 | const page = { 117 | height: 1080, 118 | inStockWaitTime: envOrNumber(process.env.IN_STOCK_WAIT_TIME), 119 | navigationTimeout: envOrNumber(process.env.PAGE_TIMEOUT, 30000), 120 | screenshot: envOrBoolean(process.env.SCREENSHOT), 121 | userAgent: envOrString(process.env.USER_AGENT, 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'), 122 | width: 1920 123 | }; 124 | 125 | const store = { 126 | country: envOrString(process.env.COUNTRY, 'usa'), 127 | microCenterLocation: envOrString(process.env.MICROCENTER_LOCATION, 'web'), 128 | showOnlyBrands: envOrArray(process.env.SHOW_ONLY_BRANDS), 129 | showOnlyModels: envOrArray(process.env.SHOW_ONLY_MODELS), 130 | showOnlySeries: envOrArray(process.env.SHOW_ONLY_SERIES, ['3070', '3080', '3090']), 131 | stores: envOrArray(process.env.STORES, ['nvidia']) 132 | }; 133 | 134 | export const Config = { 135 | browser, 136 | logLevel, 137 | notifications, 138 | page, 139 | store 140 | }; 141 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Config} from './config'; 2 | import {Logger} from './logger'; 3 | import {Stores} from './store/model'; 4 | import {adBlocker} from './adblocker'; 5 | import {fetchLinks} from './store/fetch-links'; 6 | import {getSleepTime} from './util'; 7 | import puppeteer from 'puppeteer-extra'; 8 | import resourceBlock from 'puppeteer-extra-plugin-block-resources'; 9 | import stealthPlugin from 'puppeteer-extra-plugin-stealth'; 10 | import {tryLookupAndLoop} from './store'; 11 | 12 | puppeteer.use(stealthPlugin()); 13 | if (Config.browser.lowBandwidth) { 14 | puppeteer.use(resourceBlock({ 15 | blockedTypes: new Set(['image', 'font']) 16 | })); 17 | } else { 18 | puppeteer.use(adBlocker); 19 | } 20 | 21 | /** 22 | * Starts the bot. 23 | */ 24 | async function main() { 25 | if (Stores.length === 0) { 26 | Logger.error('✖ no stores selected', Stores); 27 | return; 28 | } 29 | 30 | const args: string[] = []; 31 | 32 | // Skip Chromium Linux Sandbox 33 | // https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#setting-up-chrome-linux-sandbox 34 | if (Config.browser.isTrusted) { 35 | args.push('--no-sandbox'); 36 | args.push('--disable-setuid-sandbox'); 37 | } 38 | 39 | const browser = await puppeteer.launch({ 40 | args, 41 | defaultViewport: { 42 | height: Config.page.height, 43 | width: Config.page.width 44 | }, 45 | headless: Config.browser.isHeadless 46 | }); 47 | 48 | const promises = []; 49 | for (const store of Stores) { 50 | Logger.debug(store.links); 51 | if (store.setupAction !== undefined) { 52 | store.setupAction(browser); 53 | } 54 | 55 | if (store.linksBuilder) { 56 | promises.push(fetchLinks(store, browser)); 57 | } 58 | 59 | setTimeout(tryLookupAndLoop, getSleepTime(), browser, store); 60 | } 61 | 62 | await Promise.all(promises); 63 | } 64 | 65 | /** 66 | * Will continually run until user interferes. 67 | */ 68 | try { 69 | void main(); 70 | } catch (error) { 71 | Logger.error('✖ something bad happened, resetting nvidia-snatcher', error); 72 | void main(); 73 | } 74 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from './store/model'; 2 | import winston, {format} from 'winston'; 3 | import {Config} from './config'; 4 | import chalk from 'chalk'; 5 | 6 | const prettyJson = format.printf(info => { 7 | const timestamp = new Date().toLocaleTimeString(); 8 | 9 | if (typeof info.message === 'object') { 10 | info.message = JSON.stringify(info.message, null, 4); 11 | } 12 | 13 | return chalk.grey(`[${timestamp}]`) + ` ${info.level} ` + chalk.grey('::') + ` ${info.message}`; 14 | }); 15 | 16 | export const Logger = winston.createLogger({ 17 | format: format.combine( 18 | format.colorize(), 19 | format.prettyPrint(), 20 | format.splat(), 21 | format.simple(), 22 | prettyJson 23 | ), 24 | level: Config.logLevel, 25 | transports: [new winston.transports.Console({})] 26 | }); 27 | 28 | export const Print = { 29 | backoff(link: Link, store: Store, delay: number, color?: boolean): string { 30 | if (color) { 31 | return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow(`REQUEST FORBIDDEN - BACKOFF DELAY ${delay}`); 32 | } 33 | 34 | return `✖ ${buildProductString(link, store)} :: REQUEST FORBIDDEN - BACKOFF DELAY ${delay}`; 35 | }, 36 | badStatusCode(link: Link, store: Store, statusCode: number, color?: boolean): string { 37 | if (color) { 38 | return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow(`STATUS CODE ERROR ${statusCode}`); 39 | } 40 | 41 | return `✖ ${buildProductString(link, store)} :: STATUS CODE ERROR ${statusCode}`; 42 | }, 43 | bannedSeller(link: Link, store: Store, color?: boolean): string { 44 | if (color) { 45 | return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow('BANNED SELLER'); 46 | } 47 | 48 | return `✖ ${buildProductString(link, store)} :: BANNED SELLER`; 49 | }, 50 | captcha(link: Link, store: Store, color?: boolean): string { 51 | if (color) { 52 | return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow('CAPTCHA'); 53 | } 54 | 55 | return `✖ ${buildProductString(link, store)} :: CAPTCHA`; 56 | }, 57 | inStock(link: Link, store: Store, color?: boolean, sms?: boolean): string { 58 | const productString = `${buildProductString(link, store)} :: IN STOCK`; 59 | 60 | if (color) { 61 | return chalk.bgGreen.white.bold(`🚀🚨 ${productString} 🚨🚀`); 62 | } 63 | 64 | if (sms) { 65 | return productString; 66 | } 67 | 68 | return `🚀🚨 ${productString} 🚨🚀`; 69 | }, 70 | inStockWaiting(link: Link, store: Store, color?: boolean): string { 71 | if (color) { 72 | return 'ℹ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow('IN STOCK, WAITING'); 73 | } 74 | 75 | return `ℹ ${buildProductString(link, store)} :: IN STOCK, WAITING`; 76 | }, 77 | message(message: string, topic: string, store: Store, color?: boolean): string { 78 | if (color) { 79 | return '✖ ' + buildSetupString(topic, store, true) + ' :: ' + chalk.yellow(message); 80 | } 81 | 82 | return `✖ ${buildSetupString(topic, store)} :: ${message}`; 83 | }, 84 | noResponse(link: Link, store: Store, color?: boolean): string { 85 | if (color) { 86 | return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow('NO RESPONSE'); 87 | } 88 | 89 | return `✖ ${buildProductString(link, store)} :: NO RESPONSE`; 90 | }, 91 | outOfStock(link: Link, store: Store, color?: boolean): string { 92 | if (color) { 93 | return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.red('OUT OF STOCK'); 94 | } 95 | 96 | return `✖ ${buildProductString(link, store)} :: OUT OF STOCK`; 97 | }, 98 | rateLimit(link: Link, store: Store, color?: boolean): string { 99 | if (color) { 100 | return '✖ ' + buildProductString(link, store, true) + ' :: ' + chalk.yellow('RATE LIMIT EXCEEDED'); 101 | } 102 | 103 | return `✖ ${buildProductString(link, store)} :: RATE LIMIT EXCEEDED`; 104 | } 105 | }; 106 | 107 | function buildSetupString(topic: string, store: Store, color?: boolean): string { 108 | if (color) { 109 | return chalk.cyan(`[${store.name}]`) + chalk.grey(` [setup (${topic})]`); 110 | } 111 | 112 | return `[${store.name}] [setup (${topic})]`; 113 | } 114 | 115 | function buildProductString(link: Link, store: Store, color?: boolean): string { 116 | if (color) { 117 | return chalk.cyan(`[${store.name}]`) + chalk.grey(` [${link.brand} (${link.series})] ${link.model}`); 118 | } 119 | 120 | return `[${store.name}] [${link.brand} (${link.series})] ${link.model}`; 121 | } 122 | -------------------------------------------------------------------------------- /src/notification/desktop.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import notifier from 'node-notifier'; 4 | 5 | export function sendDesktopNotification(link: Link, store: Store) { 6 | (async () => { 7 | notifier.notify({ 8 | message: link.cartUrl ? link.cartUrl : link.url, 9 | title: Print.inStock(link, store) 10 | }); 11 | 12 | Logger.info('✔ desktop notification sent'); 13 | })(); 14 | } 15 | -------------------------------------------------------------------------------- /src/notification/discord.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {MessageBuilder, Webhook} from 'discord-webhook-node'; 3 | import {Config} from '../config'; 4 | import {Logger} from '../logger'; 5 | 6 | const hooks = Config.notifications.discord.webHookUrl; 7 | const notifyGroup = Config.notifications.discord.notifyGroup; 8 | 9 | export function sendDiscordMessage(link: Link, store: Store) { 10 | (async () => { 11 | try { 12 | const embed = new MessageBuilder(); 13 | embed.setTitle('Stock Notification'); 14 | embed.addField('URL', link.cartUrl ? link.cartUrl : link.url, true); 15 | embed.addField('Store', store.name, true); 16 | embed.addField('Brand', link.brand, true); 17 | embed.addField('Model', link.model, true); 18 | 19 | if (notifyGroup) { 20 | embed.setText(notifyGroup.join(' ')); 21 | } 22 | 23 | embed.setColor(0x76B900); 24 | embed.setTimestamp(); 25 | 26 | const promises = []; 27 | for (const hook of hooks) { 28 | promises.push(new Webhook(hook).send(embed)); 29 | } 30 | 31 | await Promise.all(promises); 32 | 33 | Logger.info('✔ discord message sent'); 34 | } catch (error) { 35 | Logger.error('✖ couldn\'t send discord message', error); 36 | } 37 | })(); 38 | } 39 | -------------------------------------------------------------------------------- /src/notification/email.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Config} from '../config'; 4 | import Mail from 'nodemailer/lib/mailer'; 5 | import nodemailer from 'nodemailer'; 6 | 7 | const email = Config.notifications.email; 8 | 9 | const transporter = nodemailer.createTransport({ 10 | auth: { 11 | pass: email.password, 12 | user: email.username 13 | }, 14 | service: 'gmail' 15 | }); 16 | 17 | export function sendEmail(link: Link, store: Store) { 18 | const mailOptions: Mail.Options = { 19 | attachments: link.screenshot ? [ 20 | { 21 | filename: link.screenshot, 22 | path: `./${link.screenshot}` 23 | } 24 | ] : undefined, 25 | from: email.username, 26 | subject: Print.inStock(link, store), 27 | text: link.cartUrl ? link.cartUrl : link.url, 28 | to: email.to 29 | }; 30 | 31 | transporter.sendMail(mailOptions, error => { 32 | if (error) { 33 | Logger.error('✖ couldn\'t send email', error); 34 | } else { 35 | Logger.info('✔ email sent'); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/notification/index.ts: -------------------------------------------------------------------------------- 1 | export * from './notification'; 2 | -------------------------------------------------------------------------------- /src/notification/notification.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Config} from '../config'; 3 | import {Logger} from '../logger'; 4 | import {playSound} from './sound'; 5 | import {sendDesktopNotification} from './desktop'; 6 | import {sendDiscordMessage} from './discord'; 7 | import {sendEmail} from './email'; 8 | import {sendPushBulletNotification} from './pushbullet'; 9 | import {sendPushoverNotification} from './pushover'; 10 | import {sendSMS} from './sms'; 11 | import {sendSlackMessage} from './slack'; 12 | import {sendTelegramMessage} from './telegram'; 13 | import {sendTweet} from './twitter'; 14 | 15 | const notifications = Config.notifications; 16 | 17 | export function sendNotification(link: Link, store: Store) { 18 | if (notifications.email.username && notifications.email.password) { 19 | Logger.debug('↗ sending email'); 20 | sendEmail(link, store); 21 | } 22 | 23 | if (notifications.phone.number) { 24 | Logger.debug('↗ sending sms'); 25 | const carrier = notifications.phone.carrier; 26 | if (carrier && notifications.phone.availableCarriers.has(carrier)) { 27 | sendSMS(link, store); 28 | } 29 | } 30 | 31 | if (notifications.playSound) { 32 | Logger.debug('↗ playing sound'); 33 | playSound(); 34 | } 35 | 36 | if (notifications.desktop) { 37 | Logger.debug('↗ sending desktop notification'); 38 | sendDesktopNotification(link, store); 39 | } 40 | 41 | if (notifications.discord.webHookUrl) { 42 | Logger.debug('↗ sending discord message'); 43 | sendDiscordMessage(link, store); 44 | } 45 | 46 | if (notifications.slack.channel && notifications.slack.token) { 47 | Logger.debug('↗ sending slack message'); 48 | sendSlackMessage(link, store); 49 | } 50 | 51 | if (notifications.telegram.accessToken && notifications.telegram.chatId) { 52 | Logger.debug('↗ sending telegram message'); 53 | sendTelegramMessage(link, store); 54 | } 55 | 56 | if (notifications.pushBulletApiKey) { 57 | Logger.debug('↗ sending pushbullet message'); 58 | sendPushBulletNotification(link, store); 59 | } 60 | 61 | if (notifications.pushover.token && notifications.pushover.username) { 62 | Logger.debug('↗ sending pushover message'); 63 | sendPushoverNotification(link, store); 64 | } 65 | 66 | if ( 67 | notifications.twitter.accessTokenKey && 68 | notifications.twitter.accessTokenSecret && 69 | notifications.twitter.consumerKey && 70 | notifications.twitter.consumerSecret 71 | ) { 72 | Logger.debug('↗ sending twitter message'); 73 | sendTweet(link, store); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/notification/pushbullet.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Config} from '../config'; 4 | import PushBullet from 'pushbullet'; 5 | 6 | const pushBulletApiKey = Config.notifications.pushBulletApiKey; 7 | 8 | export function sendPushBulletNotification(link: Link, store: Store) { 9 | const pusher = new PushBullet(pushBulletApiKey); 10 | 11 | pusher.note( 12 | {}, 13 | Print.inStock(link, store), 14 | link.cartUrl ? link.cartUrl : link.url, 15 | (error: Error) => { 16 | if (error) { 17 | Logger.error('✖ couldn\'t send pushbullet message', error); 18 | } else { 19 | Logger.info('✔ pushbullet message sent'); 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/notification/pushover.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Config} from '../config'; 4 | import Push from 'pushover-notifications'; 5 | 6 | const pushover = Config.notifications.pushover; 7 | const push = new Push({ 8 | token: pushover.token, 9 | user: pushover.username 10 | }); 11 | 12 | export function sendPushoverNotification(link: Link, store: Store) { 13 | const message = { 14 | message: link.cartUrl ? link.cartUrl : link.url, 15 | priority: pushover.priority, 16 | title: Print.inStock(link, store) 17 | }; 18 | 19 | push.send(message, (error: Error) => { 20 | if (error) { 21 | Logger.error('✖ couldn\'t send pushover message', error); 22 | } else { 23 | Logger.info('✔ pushover message sent'); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/notification/slack.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Config} from '../config'; 4 | import {WebClient} from '@slack/web-api'; 5 | 6 | const channel = Config.notifications.slack.channel; 7 | const token = Config.notifications.slack.token; 8 | const web = new WebClient(token); 9 | 10 | export function sendSlackMessage(link: Link, store: Store) { 11 | (async () => { 12 | const givenUrl = link.cartUrl ? link.cartUrl : link.url; 13 | 14 | try { 15 | const result = await web.chat.postMessage({ 16 | channel, 17 | text: `${Print.inStock(link, store)}\n${givenUrl}` 18 | }); 19 | 20 | if (!result.ok) { 21 | Logger.error('✖ couldn\'t send slack message', result); 22 | return; 23 | } 24 | 25 | Logger.info('✔ slack message sent'); 26 | } catch (error) { 27 | Logger.error('✖ couldn\'t send slack message', error); 28 | } 29 | })(); 30 | } 31 | -------------------------------------------------------------------------------- /src/notification/sms.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Config} from '../config'; 4 | import Mail from 'nodemailer/lib/mailer'; 5 | import nodemailer from 'nodemailer'; 6 | 7 | if (Config.notifications.phone.number && !Config.notifications.email.username) { 8 | Logger.warn('✖ in order to recieve sms alerts, email notifications must also be configured'); 9 | } 10 | 11 | const [email, phone] = [Config.notifications.email, Config.notifications.phone]; 12 | 13 | const transporter = nodemailer.createTransport({ 14 | auth: { 15 | pass: email.password, 16 | user: email.username 17 | }, 18 | service: 'gmail' 19 | }); 20 | 21 | export function sendSMS(link: Link, store: Store) { 22 | const mailOptions: Mail.Options = { 23 | attachments: link.screenshot ? [ 24 | { 25 | filename: link.screenshot, 26 | path: `./${link.screenshot}` 27 | } 28 | ] : undefined, 29 | from: email.username, 30 | subject: Print.inStock(link, store, false, true), 31 | text: link.cartUrl ? link.cartUrl : link.url, 32 | to: generateAddress() 33 | }; 34 | 35 | transporter.sendMail(mailOptions, error => { 36 | if (error) { 37 | Logger.error('✖ couldn\'t send sms', error); 38 | } else { 39 | Logger.info('✔ sms sent'); 40 | } 41 | }); 42 | } 43 | 44 | function generateAddress() { 45 | const carrier = phone.carrier; 46 | 47 | if (carrier && phone.availableCarriers.has(carrier)) { 48 | return [phone.number, phone.availableCarriers.get(carrier)].join('@'); 49 | } 50 | 51 | Logger.error('✖ unknown carrier', carrier); 52 | } 53 | -------------------------------------------------------------------------------- /src/notification/sound.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../config'; 2 | import {Logger} from '../logger'; 3 | import fs from 'fs'; 4 | import playerLib from 'play-sound'; 5 | 6 | let player: any; 7 | 8 | if (Config.notifications.playSound) { 9 | player = playerLib(); 10 | 11 | if (player.player === null) { 12 | Logger.warn('✖ couldn\'t find sound player'); 13 | } else { 14 | const playerName: string = player.player; 15 | Logger.info(`✔ sound player found: ${playerName}`); 16 | } 17 | } 18 | 19 | export function playSound() { 20 | if (player.player !== null) { 21 | fs.access(Config.notifications.playSound, fs.constants.F_OK, error => { 22 | if (error) { 23 | Logger.error(`✖ error opening sound file: ${error.message}`); 24 | return; 25 | } 26 | 27 | player.play(Config.notifications.playSound, (error: Error) => { 28 | if (error) { 29 | Logger.error('✖ couldn\'t play sound', error); 30 | } 31 | 32 | Logger.info('✔ played sound'); 33 | }); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/notification/telegram.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Config} from '../config'; 4 | import {TelegramClient} from 'messaging-api-telegram'; 5 | 6 | const telegram = Config.notifications.telegram; 7 | 8 | const client = new TelegramClient({ 9 | accessToken: telegram.accessToken 10 | }); 11 | 12 | export function sendTelegramMessage(link: Link, store: Store) { 13 | (async () => { 14 | const givenUrl = link.cartUrl ? link.cartUrl : link.url; 15 | 16 | try { 17 | await client.sendMessage(telegram.chatId, `${Print.inStock(link, store)}\n${givenUrl}`); 18 | Logger.info('✔ telegram message sent'); 19 | } catch (error) { 20 | Logger.error('✖ couldn\'t send telegram message', error); 21 | } 22 | })(); 23 | } 24 | -------------------------------------------------------------------------------- /src/notification/twitter.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Config} from '../config'; 4 | import Twitter from 'twitter'; 5 | 6 | const twitter = Config.notifications.twitter; 7 | 8 | const client = new Twitter({ 9 | access_token_key: twitter.accessTokenKey, 10 | access_token_secret: twitter.accessTokenSecret, 11 | consumer_key: twitter.consumerKey, 12 | consumer_secret: twitter.consumerSecret 13 | }); 14 | 15 | export function sendTweet(link: Link, store: Store) { 16 | let status = `${Print.inStock(link, store)}\n${link.cartUrl ? link.cartUrl : link.url}`; 17 | 18 | if (twitter.tweetTags) { 19 | status += `\n\n${twitter.tweetTags}`; 20 | } 21 | 22 | client.post('statuses/update', {status}, error => { 23 | if (error) { 24 | Logger.error('✖ couldn\'t send twitter notification', error); 25 | } else { 26 | Logger.info('✔ twitter notification sent'); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/store/fetch-links.ts: -------------------------------------------------------------------------------- 1 | import {Link, Series, Store} from './model'; 2 | import {Logger, Print} from '../logger'; 3 | import {Browser} from 'puppeteer'; 4 | import cheerio from 'cheerio'; 5 | import {filterSeries} from './filter'; 6 | import {usingResponse} from '../util'; 7 | 8 | function addNewLinks(store: Store, links: Link[], series: Series) { 9 | if (links.length === 0) { 10 | Logger.error(Print.message('NO STORE LINKS FOUND', series, store, true)); 11 | 12 | return; 13 | } 14 | 15 | const existingUrls = new Set(store.links.map(link => link.url)); 16 | const newLinks = links.filter(link => !existingUrls.has(link.url)); 17 | 18 | if (newLinks.length === 0) { 19 | return; 20 | } 21 | 22 | Logger.info(Print.message(`FOUND ${newLinks.length} STORE LINKS`, series, store, true)); 23 | Logger.debug(JSON.stringify(newLinks, null, 2)); 24 | 25 | store.links = store.links.concat(newLinks); 26 | } 27 | 28 | export async function fetchLinks(store: Store, browser: Browser) { 29 | if (!store.linksBuilder) { 30 | return; 31 | } 32 | 33 | const promises = []; 34 | 35 | for (const {series, url} of store.linksBuilder.urls) { 36 | if (!filterSeries(series)) { 37 | continue; 38 | } 39 | 40 | Logger.info(Print.message('DETECTING STORE LINKS', series, store, true)); 41 | 42 | promises.push(usingResponse(browser, url, async response => { 43 | const text = await response?.text(); 44 | 45 | if (!text) { 46 | Logger.error(Print.message('NO RESPONSE', series, store, true)); 47 | return; 48 | } 49 | 50 | const docElement = cheerio.load(text).root(); 51 | const links = store.linksBuilder!.builder(docElement, series); 52 | 53 | addNewLinks(store, links, series); 54 | })); 55 | } 56 | 57 | await Promise.all(promises); 58 | } 59 | -------------------------------------------------------------------------------- /src/store/filter.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../config'; 2 | import {Link} from './model'; 3 | 4 | /** 5 | * Returns true if the brand should be checked for stock 6 | * 7 | * @param brand The brand of the GPU 8 | */ 9 | function filterBrand(brand: Link['brand']): boolean { 10 | if (Config.store.showOnlyBrands.length === 0) { 11 | return true; 12 | } 13 | 14 | return Config.store.showOnlyBrands.includes(brand); 15 | } 16 | 17 | /** 18 | * Returns true if the model should be checked for stock 19 | * 20 | * @param model The model of the GPU 21 | */ 22 | function filterModel(model: Link['model']): boolean { 23 | if (Config.store.showOnlyModels.length === 0) { 24 | return true; 25 | } 26 | 27 | const sanitizedModel = model.replace(/\s/g, ''); 28 | for (const configModel of Config.store.showOnlyModels) { 29 | const sanitizedConfigModel = configModel.replace(/\s/g, ''); 30 | if (sanitizedModel === sanitizedConfigModel) { 31 | return true; 32 | } 33 | } 34 | 35 | return false; 36 | } 37 | 38 | /** 39 | * Returns true if the series should be checked for stock 40 | * 41 | * @param series The series of the GPU 42 | */ 43 | export function filterSeries(series: Link['series']): boolean { 44 | if (Config.store.showOnlySeries.length === 0) { 45 | return true; 46 | } 47 | 48 | return Config.store.showOnlySeries.includes(series); 49 | } 50 | 51 | /** 52 | * Returns true if the link should be checked for stock 53 | * 54 | * @param link The store link of the GPU 55 | */ 56 | export function filterStoreLink(link: Link): boolean { 57 | return ( 58 | filterBrand(link.brand) && 59 | filterModel(link.model) && 60 | filterSeries(link.series) 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/store/includes-labels.ts: -------------------------------------------------------------------------------- 1 | import {Element, LabelQuery} from './model'; 2 | import {Logger} from '../logger'; 3 | import {Page} from 'puppeteer'; 4 | 5 | export type Selector = { 6 | requireVisible: boolean; 7 | selector: string; 8 | type: 'innerHTML' | 'outerHTML' | 'textContent'; 9 | }; 10 | 11 | function isElementArray(query: LabelQuery): query is Element[] { 12 | return Array.isArray(query) && query.length > 0 && typeof query[0] === 'object'; 13 | } 14 | 15 | function getQueryAsElementArray(query: LabelQuery, defaultContainer: string): Array> { 16 | if (isElementArray(query)) { 17 | return query.map(x => ({ 18 | container: x.container ?? defaultContainer, 19 | text: x.text 20 | })); 21 | } 22 | 23 | if (Array.isArray(query)) { 24 | return [{ 25 | container: defaultContainer, 26 | text: query 27 | }]; 28 | } 29 | 30 | return [{ 31 | container: query.container ?? defaultContainer, 32 | text: query.text 33 | }]; 34 | } 35 | 36 | export async function pageIncludesLabels(page: Page, query: LabelQuery, options: Selector) { 37 | const elementQueries = getQueryAsElementArray(query, options.selector); 38 | 39 | const resolved = await Promise.all(elementQueries.map(async query => { 40 | const selector = {...options, selector: query.container}; 41 | const contents = await extractPageContents(page, selector) ?? ''; 42 | 43 | if (!contents) { 44 | return false; 45 | } 46 | 47 | Logger.debug(contents); 48 | 49 | return includesLabels(contents, query.text); 50 | })); 51 | 52 | return resolved.includes(true); 53 | } 54 | 55 | export async function extractPageContents(page: Page, selector: Selector): Promise { 56 | const content = await page.evaluate((options: Selector) => { 57 | // eslint-disable-next-line no-undef 58 | const element: globalThis.HTMLElement | null = document.querySelector(options.selector); 59 | 60 | if (!element) { 61 | return null; 62 | } 63 | 64 | if (options.requireVisible && !(element.offsetWidth > 0 && element.offsetHeight > 0)) { 65 | return null; 66 | } 67 | 68 | switch (options.type) { 69 | case 'innerHTML': 70 | return element.innerHTML; 71 | case 'outerHTML': 72 | return element.outerHTML; 73 | case 'textContent': 74 | return element.textContent; 75 | default: 76 | return 'Error: selector.type is unknown'; 77 | } 78 | }, selector); 79 | 80 | return content; 81 | } 82 | 83 | /** 84 | * Checks if DOM has any related text. 85 | * 86 | * @param domText Complete DOM of website. 87 | * @param searchLabels Search labels for a match. 88 | */ 89 | export function includesLabels(domText: string, searchLabels: string[]): boolean { 90 | const domTextLowerCase = domText.toLowerCase(); 91 | return searchLabels.some(label => domTextLowerCase.includes(label)); 92 | } 93 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lookup'; 2 | -------------------------------------------------------------------------------- /src/store/lookup.ts: -------------------------------------------------------------------------------- 1 | import {Browser, Page, Response} from 'puppeteer'; 2 | import {Link, Store} from './model'; 3 | import {Logger, Print} from '../logger'; 4 | import {Selector, pageIncludesLabels} from './includes-labels'; 5 | import {closePage, delay, getSleepTime} from '../util'; 6 | import {Config} from '../config'; 7 | import {disableBlockerInPage} from '../adblocker'; 8 | import {filterStoreLink} from './filter'; 9 | import open from 'open'; 10 | import {sendNotification} from '../notification'; 11 | 12 | type Backoff = { 13 | count: number; 14 | time: number; 15 | }; 16 | 17 | const inStock: Record = {}; 18 | 19 | const storeBackoff: Record = {}; 20 | 21 | /** 22 | * Responsible for looking up information about a each product within 23 | * a `Store`. It's important that we ignore `no-await-in-loop` here 24 | * because we don't want to get rate limited within the same store. 25 | * 26 | * @param browser Puppeteer browser. 27 | * @param store Vendor of graphics cards. 28 | */ 29 | async function lookup(browser: Browser, store: Store) { 30 | /* eslint-disable no-await-in-loop */ 31 | for (const link of store.links) { 32 | if (!filterStoreLink(link)) { 33 | continue; 34 | } 35 | 36 | if (Config.page.inStockWaitTime && inStock[link.url]) { 37 | Logger.info(Print.inStockWaiting(link, store, true)); 38 | continue; 39 | } 40 | 41 | const page = await browser.newPage(); 42 | page.setDefaultNavigationTimeout(Config.page.navigationTimeout); 43 | await page.setUserAgent(Config.page.userAgent); 44 | 45 | if (store.disableAdBlocker) { 46 | try { 47 | await disableBlockerInPage(page); 48 | } catch (error) { 49 | Logger.error(error); 50 | } 51 | } 52 | 53 | try { 54 | await lookupCard(browser, store, page, link); 55 | } catch (error) { 56 | Logger.error(`✖ [${store.name}] ${link.brand} ${link.model} - ${error.message as string}`); 57 | } 58 | 59 | await closePage(page); 60 | } 61 | /* eslint-enable no-await-in-loop */ 62 | } 63 | 64 | async function lookupCard(browser: Browser, store: Store, page: Page, link: Link) { 65 | const givenWaitFor = store.waitUntil ? store.waitUntil : 'networkidle0'; 66 | const response: Response | null = await page.goto(link.url, {waitUntil: givenWaitFor}); 67 | 68 | if (!response) { 69 | Logger.debug(Print.noResponse(link, store, true)); 70 | } 71 | 72 | let backoff = storeBackoff[store.name]; 73 | 74 | if (!backoff) { 75 | backoff = {count: 0, time: Config.browser.minBackoff}; 76 | storeBackoff[store.name] = backoff; 77 | } 78 | 79 | if (response?.status() === 403) { 80 | Logger.warn(Print.backoff(link, store, backoff.time, true)); 81 | await delay(backoff.time); 82 | backoff.count++; 83 | backoff.time = Math.min(backoff.time * 2, Config.browser.maxBackoff); 84 | return; 85 | } 86 | 87 | if (response?.status() === 429) { 88 | Logger.warn(Print.rateLimit(link, store, true)); 89 | return; 90 | } 91 | 92 | if ((response?.status() || 200) >= 400) { 93 | Logger.warn(Print.badStatusCode(link, store, response!.status(), true)); 94 | return; 95 | } 96 | 97 | if (backoff.count > 0) { 98 | backoff.count--; 99 | backoff.time = Math.max(backoff.time / 2, Config.browser.minBackoff); 100 | } 101 | 102 | if (await lookupCardInStock(store, page, link)) { 103 | const givenUrl = link.cartUrl ? link.cartUrl : link.url; 104 | Logger.info(`${Print.inStock(link, store, true)}\n${givenUrl}`); 105 | 106 | if (Config.browser.open) { 107 | if (link.openCartAction === undefined) { 108 | await open(givenUrl); 109 | } else { 110 | await link.openCartAction(browser); 111 | } 112 | } 113 | 114 | sendNotification(link, store); 115 | 116 | if (Config.page.inStockWaitTime) { 117 | inStock[link.url] = true; 118 | 119 | setTimeout(() => { 120 | inStock[link.url] = false; 121 | }, 1000 * Config.page.inStockWaitTime); 122 | } 123 | 124 | if (Config.page.screenshot) { 125 | Logger.debug('ℹ saving screenshot'); 126 | 127 | link.screenshot = `success-${Date.now()}.png`; 128 | await page.screenshot({path: link.screenshot}); 129 | } 130 | } 131 | } 132 | 133 | async function lookupCardInStock(store: Store, page: Page, link: Link) { 134 | const baseOptions: Selector = { 135 | requireVisible: false, 136 | selector: store.labels.container ?? 'body', 137 | type: 'textContent' 138 | }; 139 | 140 | if (store.labels.inStock) { 141 | const options = {...baseOptions, requireVisible: true, type: 'outerHTML' as const}; 142 | 143 | if (!await pageIncludesLabels(page, store.labels.inStock, options)) { 144 | Logger.info(Print.outOfStock(link, store, true)); 145 | return false; 146 | } 147 | } 148 | 149 | if (store.labels.outOfStock) { 150 | if (await pageIncludesLabels(page, store.labels.outOfStock, baseOptions)) { 151 | Logger.info(Print.outOfStock(link, store, true)); 152 | return false; 153 | } 154 | } 155 | 156 | if (store.labels.bannedSeller) { 157 | if (await pageIncludesLabels(page, store.labels.bannedSeller, baseOptions)) { 158 | Logger.warn(Print.bannedSeller(link, store, true)); 159 | return false; 160 | } 161 | } 162 | 163 | if (store.labels.captcha) { 164 | if (await pageIncludesLabels(page, store.labels.captcha, baseOptions)) { 165 | Logger.warn(Print.captcha(link, store, true)); 166 | await delay(getSleepTime()); 167 | return false; 168 | } 169 | } 170 | 171 | return true; 172 | } 173 | 174 | export async function tryLookupAndLoop(browser: Browser, store: Store) { 175 | Logger.debug(`[${store.name}] Starting lookup...`); 176 | try { 177 | await lookup(browser, store); 178 | } catch (error) { 179 | Logger.error(error); 180 | } 181 | 182 | const sleepTime = getSleepTime(); 183 | Logger.debug(`[${store.name}] Lookup done, next one in ${sleepTime} ms`); 184 | setTimeout(tryLookupAndLoop, sleepTime, browser, store); 185 | } 186 | -------------------------------------------------------------------------------- /src/store/model/adorama.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Adorama: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['please verify you are a human'] 8 | }, 9 | inStock: { 10 | container: '.buy-section.purchase', 11 | text: ['add to cart'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.adorama.com/ev08gp43067k.html' 20 | }, 21 | { 22 | brand: 'pny', 23 | model: 'xlr8', 24 | series: '3080', 25 | url: 'https://www.adorama.com/pnv301tfxmpb.html' 26 | }, 27 | { 28 | brand: 'msi', 29 | model: 'gaming x trio', 30 | series: '3080', 31 | url: 'https://www.adorama.com/msig380gxt1.html' 32 | }, 33 | { 34 | brand: 'evga', 35 | model: 'ftw3 ultra', 36 | series: '3080', 37 | url: 'https://www.adorama.com/ev10g53897kr.html' 38 | }, 39 | { 40 | brand: 'evga', 41 | model: 'xc3 ultra', 42 | series: '3080', 43 | url: 'https://www.adorama.com/ev10g53885kr.html' 44 | }, 45 | { 46 | brand: 'evga', 47 | model: 'ftw3', 48 | series: '3080', 49 | url: 'https://www.adorama.com/ev10g53895kr.html' 50 | }, 51 | { 52 | brand: 'evga', 53 | model: 'xc3', 54 | series: '3080', 55 | url: 'https://www.adorama.com/ev10g53883kr.html' 56 | }, 57 | { 58 | brand: 'evga', 59 | model: 'xc3 black', 60 | series: '3080', 61 | url: 'https://www.adorama.com/ev10g53881kr.html' 62 | }, 63 | { 64 | brand: 'msi', 65 | model: 'ventus 3x oc', 66 | series: '3080', 67 | url: 'https://www.adorama.com/msig38v3x10c.html' 68 | }, 69 | { 70 | brand: 'pny', 71 | model: 'xlr8 rbg', 72 | series: '3080', 73 | url: 'https://www.adorama.com/png30801tfxb.html' 74 | }, 75 | { 76 | brand: 'asus', 77 | model: 'rog strix oc', 78 | series: '3080', 79 | url: 'https://www.adorama.com/asrx3080o10g.html' 80 | }, 81 | { 82 | brand: 'asus', 83 | model: 'tuf oc', 84 | series: '3080', 85 | url: 'https://www.adorama.com/astr3080o10g.html' 86 | }, 87 | { 88 | brand: 'asus', 89 | model: 'tuf', 90 | series: '3080', 91 | url: 'https://www.adorama.com/astrx308010g.html' 92 | }, 93 | { 94 | brand: 'msi', 95 | model: 'gaming x trio', 96 | series: '3090', 97 | url: 'https://www.adorama.com/msig390gxt24.html' 98 | }, 99 | { 100 | brand: 'msi', 101 | model: 'ventus 3x oc', 102 | series: '3090', 103 | url: 'https://www.adorama.com/msig39v3x24c.html' 104 | }, 105 | { 106 | brand: 'asus', 107 | model: 'tuf', 108 | series: '3090', 109 | url: 'https://www.adorama.com/asrtx309024g.html' 110 | }, 111 | { 112 | brand: 'asus', 113 | model: 'tuf oc', 114 | series: '3090', 115 | url: 'https://www.adorama.com/ast3090o24g.html' 116 | }, 117 | { 118 | brand: 'asus', 119 | model: 'rog strix oc', 120 | series: '3090', 121 | url: 'https://www.adorama.com/asrx3090o24g.html' 122 | } 123 | ], 124 | name: 'adorama' 125 | }; 126 | -------------------------------------------------------------------------------- /src/store/model/amazon-ca.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AmazonCa: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['enter the characters you see below'] 8 | }, 9 | inStock: { 10 | container: '#desktop_buybox', 11 | text: ['add to cart'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.amazon.ca/dp/B07PBLD2MX' 20 | }, 21 | { 22 | brand: 'msi', 23 | model: 'gaming x trio', 24 | series: '3080', 25 | url: 'https://www.amazon.ca/dp/B08HR7SV3M' 26 | }, 27 | { 28 | brand: 'msi', 29 | model: 'ventus 3x oc', 30 | series: '3080', 31 | url: 'https://www.amazon.ca/dp/B08HR5SXPS' 32 | }, 33 | { 34 | brand: 'evga', 35 | model: 'ftw3', 36 | series: '3080', 37 | url: 'https://www.amazon.ca/dp/B08HR3DPGW' 38 | }, 39 | { 40 | brand: 'evga', 41 | model: 'ftw3 ultra', 42 | series: '3080', 43 | url: 'https://www.amazon.ca/dp/B08HR3Y5GQ' 44 | }, 45 | { 46 | brand: 'evga', 47 | model: 'xc3 ultra', 48 | series: '3080', 49 | url: 'https://www.amazon.ca/dp/B08HR55YB5' 50 | }, 51 | { 52 | brand: 'evga', 53 | model: 'xc3', 54 | series: '3080', 55 | url: 'https://www.amazon.ca/dp/B08HR4RJ3Q' 56 | }, 57 | { 58 | brand: 'evga', 59 | model: 'xc3 black', 60 | series: '3080', 61 | url: 'https://www.amazon.ca/dp/B08HR6FMF3' 62 | }, 63 | { 64 | brand: 'gigabyte', 65 | model: 'gaming oc', 66 | series: '3080', 67 | url: 'https://www.amazon.ca/dp/B08HJTH61J' 68 | }, 69 | { 70 | brand: 'gigabyte', 71 | model: 'eagle oc', 72 | series: '3080', 73 | url: 'https://www.amazon.ca/dp/B08HJS2JLJ' 74 | }, 75 | { 76 | brand: 'asus', 77 | model: 'tuf', 78 | series: '3080', 79 | url: 'https://www.amazon.ca/dp/B08HHDP9DW' 80 | }, 81 | { 82 | brand: 'asus', 83 | model: 'tuf oc', 84 | series: '3080', 85 | url: 'https://www.amazon.ca/dp/B08HH5WF97' 86 | }, 87 | { 88 | brand: 'zotac', 89 | model: 'trinity', 90 | series: '3080', 91 | url: 'https://www.amazon.ca/dp/B08HJNKT3P' 92 | }, 93 | { 94 | brand: 'zotac', 95 | model: 'trinity', 96 | series: '3090', 97 | url: 'https://www.amazon.ca/dp/B08HJQ182D' 98 | }, 99 | { 100 | brand: 'msi', 101 | model: 'ventus 3x oc', 102 | series: '3090', 103 | url: 'https://www.amazon.ca/dp/B08HR9D2JS' 104 | }, 105 | { 106 | brand: 'gigabyte', 107 | model: 'gaming oc', 108 | series: '3090', 109 | url: 'https://www.amazon.ca/dp/B08HJRF2CN' 110 | }, 111 | { 112 | brand: 'gigabyte', 113 | model: 'eagle oc', 114 | series: '3090', 115 | url: 'https://www.amazon.ca/dp/B08HJPDJTY' 116 | }, 117 | { 118 | brand: 'asus', 119 | model: 'tuf', 120 | series: '3090', 121 | url: 'https://www.amazon.ca/dp/B08HJGNJ81' 122 | }, 123 | { 124 | brand: 'asus', 125 | model: 'tuf oc', 126 | series: '3090', 127 | url: 'https://www.amazon.ca/dp/B08HJLLF7G' 128 | } 129 | ], 130 | name: 'amazon-ca' 131 | }; 132 | -------------------------------------------------------------------------------- /src/store/model/amazon-de.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AmazonDe: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['geben sie die unten angezeigten zeichen ein', 'geben sie die zeichen unten ein'] 8 | }, 9 | inStock: { 10 | container: '#desktop_buybox', 11 | text: ['in den einkaufswagen'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.amazon.com/dp/B07MQ36Z6L' 20 | }, 21 | { 22 | brand: 'pny', 23 | model: 'xlr8', 24 | series: '3080', 25 | url: 'https://www.amazon.de/dp/B08HBTJMLJ' 26 | }, 27 | { 28 | brand: 'pny', 29 | model: 'xlr8-rgb', 30 | series: '3080', 31 | url: 'https://www.amazon.de/dp/B08HBR7QBM' 32 | }, 33 | { 34 | brand: 'msi', 35 | model: 'gaming x trio', 36 | series: '3080', 37 | url: 'https://www.amazon.de/dp/B08HM4V2DH' 38 | }, 39 | { 40 | brand: 'evga', 41 | model: 'ftw3 ultra', 42 | series: '3080', 43 | url: 'https://www.amazon.de/dp/B08HGYXP4C' 44 | }, 45 | { 46 | brand: 'evga', 47 | model: 'xc3 ultra', 48 | series: '3080', 49 | url: 'https://www.amazon.de/dp/B08HJ9XFNM' 50 | }, 51 | { 52 | brand: 'evga', 53 | model: 'ftw3', 54 | series: '3080', 55 | url: 'https://www.amazon.de/dp/B08HGBYWQ6' 56 | }, 57 | { 58 | brand: 'evga', 59 | model: 'xc3', 60 | series: '3080', 61 | url: 'https://www.amazon.de/dp/B08HGLN78Q' 62 | }, 63 | { 64 | brand: 'evga', 65 | model: 'xc3 black', 66 | series: '3080', 67 | url: 'https://www.amazon.de/dp/B08HH1BMQQ' 68 | }, 69 | { 70 | brand: 'gigabyte', 71 | model: 'gaming oc', 72 | series: '3080', 73 | url: 'https://www.amazon.de/dp/B08HLZXHZY' 74 | }, 75 | { 76 | brand: 'gigabyte', 77 | model: 'eagle oc', 78 | series: '3080', 79 | url: 'https://www.amazon.de/dp/B08HHZVZ3N' 80 | }, 81 | { 82 | brand: 'asus', 83 | model: 'tuf', 84 | series: '3080', 85 | url: 'https://www.amazon.de/dp/B08HN4DSTC' 86 | }, 87 | { 88 | brand: 'msi', 89 | model: 'ventus 3x oc', 90 | series: '3080', 91 | url: 'https://www.amazon.de/dp/B08HM4M621' 92 | }, 93 | { 94 | brand: 'zotac', 95 | model: 'trinity', 96 | series: '3080', 97 | url: 'https://www.amazon.de/dp/B08HR1NPPQ' 98 | } 99 | ], 100 | name: 'amazon-de' 101 | }; 102 | -------------------------------------------------------------------------------- /src/store/model/amazon-nl.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AmazonNl: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['voer de karakters in die u hieronder ziet'] 8 | }, 9 | inStock: { 10 | container: '#desktop_buybox', 11 | text: ['in winkelwagen plaatsen'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.amazon.com/dp/B07MQ36Z6L' 20 | }, 21 | { 22 | brand: 'msi', 23 | model: 'ventus', 24 | series: '3080', 25 | url: 'https://www.amazon.nl/3080-VENTUS-3X-10G-OC/dp/B08HM4M621' 26 | }, 27 | { 28 | brand: 'msi', 29 | model: 'gaming x trio', 30 | series: '3080', 31 | url: 'https://www.amazon.nl/3080-GAMING-TRIO-10G-grafische/dp/B08HM4V2DH' 32 | } 33 | ], 34 | name: 'amazon-nl' 35 | }; 36 | -------------------------------------------------------------------------------- /src/store/model/amazon.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Amazon: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['enter the characters you see below'] 8 | }, 9 | inStock: { 10 | container: '#desktop_buybox', 11 | text: ['add to cart'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B07MQ36Z6L&Quantity.1=1', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.amazon.com/dp/B07MQ36Z6L' 21 | }, 22 | { 23 | brand: 'pny', 24 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HBR7QBM&Quantity.1=1', 25 | model: 'xlr8', 26 | series: '3080', 27 | url: 'https://www.amazon.com/dp/B08HBR7QBM' 28 | }, 29 | { 30 | brand: 'pny', 31 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HBTJMLJ&Quantity.1=1', 32 | model: 'xlr8 rgb', 33 | series: '3080', 34 | url: 'https://www.amazon.com/dp/B08HBTJMLJ' 35 | }, 36 | { 37 | brand: 'msi', 38 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR7SV3M&Quantity.1=1', 39 | model: 'gaming x trio', 40 | series: '3080', 41 | url: 'https://www.amazon.com/dp/B08HR7SV3M' 42 | }, 43 | { 44 | brand: 'evga', 45 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR3Y5GQ&Quantity.1=1', 46 | model: 'ftw3 ultra', 47 | series: '3080', 48 | url: 'https://www.amazon.com/dp/B08HR3Y5GQ' 49 | }, 50 | { 51 | brand: 'evga', 52 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR55YB5&Quantity.1=1', 53 | model: 'xc3 ultra', 54 | series: '3080', 55 | url: 'https://www.amazon.com/dp/B08HR55YB5' 56 | }, 57 | { 58 | brand: 'evga', 59 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR3DPGW&Quantity.1=1', 60 | model: 'ftw3', 61 | series: '3080', 62 | url: 'https://www.amazon.com/dp/B08HR3DPGW' 63 | }, 64 | { 65 | brand: 'evga', 66 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR4RJ3Q&Quantity.1=1', 67 | model: 'xc3', 68 | series: '3080', 69 | url: 'https://www.amazon.com/dp/B08HR4RJ3Q' 70 | }, 71 | { 72 | brand: 'evga', 73 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR6FMF3&Quantity.1=1', 74 | model: 'xc3 black', 75 | series: '3080', 76 | url: 'https://www.amazon.com/dp/B08HR6FMF3' 77 | }, 78 | { 79 | brand: 'gigabyte', 80 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJTH61J&Quantity.1=1', 81 | model: 'gaming oc', 82 | series: '3080', 83 | url: 'https://www.amazon.com/dp/B08HJTH61J' 84 | }, 85 | { 86 | brand: 'gigabyte', 87 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJS2JLJ&Quantity.1=1', 88 | model: 'eagle oc', 89 | series: '3080', 90 | url: 'https://www.amazon.com/dp/B08HJS2JLJ' 91 | }, 92 | { 93 | brand: 'asus', 94 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HH5WF97&Quantity.1=1', 95 | model: 'tuf oc', 96 | series: '3080', 97 | url: 'https://www.amazon.com/dp/B08HH5WF97' 98 | }, 99 | { 100 | brand: 'asus', 101 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HHDP9DW&Quantity.1=1', 102 | model: 'tuf', 103 | series: '3080', 104 | url: 'https://www.amazon.com/dp/B08HHDP9DW' 105 | }, 106 | { 107 | brand: 'asus', 108 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08J6F174Z&Quantity.1=1', 109 | model: 'strix', 110 | series: '3080', 111 | url: 'https://www.amazon.com/dp/B08J6F174Z' 112 | }, 113 | { 114 | brand: 'msi', 115 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR5SXPS&Quantity.1=1', 116 | model: 'ventus 3x oc', 117 | series: '3080', 118 | url: 'https://www.amazon.com/dp/B08HR5SXPS' 119 | }, 120 | { 121 | brand: 'zotac', 122 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJNKT3P&Quantity.1=1', 123 | model: 'trinity', 124 | series: '3080', 125 | url: 'https://www.amazon.com/dp/B08HJNKT3P' 126 | }, 127 | { 128 | brand: 'zotac', 129 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJQ182D&Quantity.1=1', 130 | model: 'trinity', 131 | series: '3090', 132 | url: 'https://www.amazon.com/dp/B08HJQ182D' 133 | }, 134 | { 135 | brand: 'pny', 136 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HBQWBHH&Quantity.1=1', 137 | model: 'xlr8', 138 | series: '3090', 139 | url: 'https://www.amazon.com/dp/B08HBQWBHH' 140 | }, 141 | { 142 | brand: 'pny', 143 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HBVX53D&Quantity.1=1', 144 | model: 'xlr8', 145 | series: '3090', 146 | url: 'https://www.amazon.com/dp/B08HBVX53D' 147 | }, 148 | { 149 | brand: 'msi', 150 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HRBW6VB&Quantity.1=1', 151 | model: 'gaming x trio', 152 | series: '3090', 153 | url: 'https://www.amazon.com/dp/B08HRBW6VB' 154 | }, 155 | { 156 | brand: 'msi', 157 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HR9D2JS&Quantity.1=1', 158 | model: 'ventus 3x', 159 | series: '3090', 160 | url: 'https://www.amazon.com/dp/B08HR9D2JS' 161 | }, 162 | { 163 | brand: 'gigabyte', 164 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJRF2CN&Quantity.1=1', 165 | model: 'gaming oc', 166 | series: '3090', 167 | url: 'https://www.amazon.com/dp/B08HJRF2CN' 168 | }, 169 | { 170 | brand: 'gigabyte', 171 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJPDJTY&Quantity.1=1', 172 | model: 'eagle oc', 173 | series: '3090', 174 | url: 'https://www.amazon.com/dp/B08HJPDJTY' 175 | }, 176 | { 177 | brand: 'asus', 178 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJGNJ81&Quantity.1=1', 179 | model: 'tuf oc', 180 | series: '3090', 181 | url: 'https://www.amazon.com/dp/B08HJGNJ81' 182 | }, 183 | { 184 | brand: 'asus', 185 | cartUrl: 'https://www.amazon.com/gp/aws/cart/add.html?ASIN.1=B08HJLLF7G&Quantity.1=1', 186 | model: 'tuf oc', 187 | series: '3090', 188 | url: 'https://www.amazon.com/dp/B08HJLLF7G' 189 | } 190 | ], 191 | name: 'amazon' 192 | }; 193 | -------------------------------------------------------------------------------- /src/store/model/asus.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Asus: Store = { 4 | labels: { 5 | inStock: { 6 | container: '#item_add_cart', 7 | text: ['add to cart'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'test:brand', 13 | model: 'test:model', 14 | series: 'test:series', 15 | url: 'https://store.asus.com/us/item/202003AM280000002/' 16 | }, 17 | { 18 | brand: 'asus', 19 | model: 'tuf oc', 20 | series: '3080', 21 | url: 'https://store.asus.com/us/item/202009AM160000001/' 22 | }, 23 | { 24 | brand: 'asus', 25 | model: 'tuf', 26 | series: '3080', 27 | url: 'https://store.asus.com/us/item/202009AM150000004/' 28 | }, 29 | { 30 | brand: 'asus', 31 | model: 'tuf', 32 | series: '3090', 33 | url: 'https://store.asus.com/us/item/202009AM150000003/' 34 | } 35 | ], 36 | name: 'asus' 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /src/store/model/bandh.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const BAndH: Store = { 4 | labels: { 5 | inStock: { 6 | container: 'div[data-selenium="addToCartSection"]', 7 | text: ['add to cart'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'test:brand', 13 | model: 'test:model', 14 | series: 'test:series', 15 | url: 'https://www.bhphotovideo.com/c/product/1452927-REG/evga_06g_p4_2063_kr_geforce_rtx_2060_xc.html' 16 | }, 17 | // TUF was removed from BH, not sure why so commenting out listing for now 18 | // { 19 | // brand: 'asus', 20 | // model: 'tuf', 21 | // series: '3080', 22 | // url: 'https://www.bhphotovideo.com/c/product/1593649-REG/asus_tuf_rtx3080_10g_gaming_tuf_gaming_geforce_rtx.html' 23 | // }, 24 | { 25 | brand: 'gigabyte', 26 | model: 'gaming oc', 27 | series: '3080', 28 | url: 'https://www.bhphotovideo.com/c/product/1593333-REG/gigabyte_gv_n3080gaming_oc_10gd_geforce_rtx_3080_gaming.html' 29 | }, 30 | { 31 | brand: 'zotac', 32 | model: 'trinity', 33 | series: '3080', 34 | url: 'https://www.bhphotovideo.com/c/product/1592969-REG/zotac_zt_a30800d_10p_gaming_geforce_rtx_3080.html' 35 | }, 36 | // TUF was removed from BH, not sure why so commenting out listing for now 37 | // { 38 | // brand: 'asus', 39 | // model: 'tuf oc', 40 | // series: '3080', 41 | // url: 'https://www.bhphotovideo.com/c/product/1593650-REG/asus_tuf_rtx3080_o10g_gaming_tuf_gaming_geforce_rtx.html' 42 | // }, 43 | { 44 | brand: 'msi', 45 | model: 'gaming x trio', 46 | series: '3080', 47 | url: 'https://www.bhphotovideo.com/c/product/1593996-REG/msi_g3080gxt10_geforce_rtx_3080_gaming.html' 48 | }, 49 | { 50 | brand: 'msi', 51 | model: 'ventus 3x oc', 52 | series: '3080', 53 | url: 'https://www.bhphotovideo.com/c/product/1593997-REG/msi_g3080v3x10c_geforce_rtx_3080_ventus.html' 54 | }, 55 | { 56 | brand: 'msi', 57 | model: 'gaming x trio - duplicate', 58 | series: '3080', 59 | url: 'https://www.bhphotovideo.com/c/product/1593645-REG/msi_geforce_rtx_3080_gaming.html' 60 | }, 61 | { 62 | brand: 'msi', 63 | model: 'ventus 3x oc - duplicate', 64 | series: '3080', 65 | url: 'https://www.bhphotovideo.com/c/product/1593646-REG/msi_geforce_rtx_3080_ventus.html' 66 | }, 67 | { 68 | brand: 'zotac', 69 | model: 'trinity', 70 | series: '3090', 71 | url: 'https://www.bhphotovideo.com/c/product/1592970-REG/zotac_zt_a30900d_10p_gaming_geforce_rtx_3090.html' 72 | }, 73 | { 74 | brand: 'msi', 75 | model: 'gaming x trio', 76 | series: '3090', 77 | url: 'https://www.bhphotovideo.com/c/product/1593647-REG/msi_geforce_rtx_3090_gaming.html' 78 | }, 79 | { 80 | brand: 'msi', 81 | model: 'gaming x trio', 82 | series: '3090', 83 | url: 'https://www.bhphotovideo.com/c/product/1593994-REG/msi_g3090gxt24_geforce_rtx_3090_gaming.html' 84 | }, 85 | { 86 | brand: 'msi', 87 | model: 'ventus 3x oc', 88 | series: '3090', 89 | url: 'https://www.bhphotovideo.com/c/product/1593648-REG/msi_geforce_rtx_3090_ventus.html' 90 | }, 91 | { 92 | brand: 'msi', 93 | model: 'ventus 3x oc', 94 | series: '3090', 95 | url: 'https://www.bhphotovideo.com/c/product/1593995-REG/msi_g3090v3x24c_geforce_rtx_3090_ventus.html' 96 | }, 97 | { 98 | brand: 'gigabyte', 99 | model: 'eagle oc', 100 | series: '3090', 101 | url: 'https://www.bhphotovideo.com/c/product/1593334-REG/gigabyte_gv_n3090eagle_oc_24gd_geforce_rtx_3090_eagle.html' 102 | }, 103 | { 104 | brand: 'gigabyte', 105 | model: 'gaming oc', 106 | series: '3090', 107 | url: 'https://www.bhphotovideo.com/c/product/1593335-REG/gigabyte_gv_n3090gaming_oc_24gd_geforce_rtx3090_gaming_oc.html' 108 | }, 109 | { 110 | brand: 'asus', 111 | model: 'tuf', 112 | series: '3090', 113 | url: 'https://www.bhphotovideo.com/c/product/1594454-REG/asus_90yv0fd0_m0am00_tuf_gaming_geforce_rtx.html' 114 | }, 115 | { 116 | brand: 'asus', 117 | model: 'tuf oc', 118 | series: '3090', 119 | url: 'https://www.bhphotovideo.com/c/product/1594451-REG/asus_90yv0fd1_m0am00_tuf_gaming_geforce_rtx.html' 120 | } 121 | ], 122 | name: 'bandh' 123 | }; 124 | -------------------------------------------------------------------------------- /src/store/model/bestbuy-ca.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const BestBuyCa: Store = { 4 | labels: { 5 | inStock: { 6 | container: '#root', 7 | text: ['available online'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'test:brand', 13 | model: 'test:model', 14 | series: 'test:series', 15 | url: 'https://www.bestbuy.ca/en-ca/product/msi-nvidia-geforce-rtx-2060-super-gaming-x-8gb-gddr6-video-card/14419420?intl=nosplash' 16 | }, 17 | { 18 | brand: 'zotac', 19 | model: 'trinity', 20 | series: '3080', 21 | url: 'https://www.bestbuy.ca/en-ca/product/zotac-geforce-rtx-3080-trinity-10gb-gddr6x-video-card/14953249?intl=nosplash' 22 | }, 23 | { 24 | brand: 'msi', 25 | model: 'ventus 3x', 26 | series: '3080', 27 | url: 'https://www.bestbuy.ca/en-ca/product/msi-nvidia-geforce-rtx-3080-ventus-3x-10gb-gddr6x-video-card/14950588?intl=nosplash' 28 | }, 29 | { 30 | brand: 'evga', 31 | model: 'xc3 ultra', 32 | series: '3080', 33 | url: 'https://www.bestbuy.ca/en-ca/product/evga-geforce-rtx-3080-xc3-ultra-gaming-10gb-gddr6x-video-card/14961449?intl=nosplash' 34 | }, 35 | { 36 | brand: 'asus', 37 | model: 'tuf', 38 | series: '3080', 39 | url: 'https://www.bestbuy.ca/en-ca/product/asus-tuf-gaming-geforce-rtx-3080-10gb-gddr6x-video-card/14953248?intl=nosplash' 40 | }, 41 | { 42 | brand: 'asus', 43 | model: 'rog strix', 44 | series: '3080', 45 | url: 'https://www.bestbuy.ca/en-ca/product/asus-rog-strix-geforce-rtx-3080-10gb-gddr6x-video-card/14954116?intl=nosplash' 46 | }, 47 | { 48 | brand: 'zotac', 49 | model: 'trinity', 50 | series: '3090', 51 | url: 'https://www.bestbuy.ca/en-ca/product/zotac-geforce-rtx-3090-trinity-24gb-gddr6x-video-card/14953250?intl=nosplash' 52 | }, 53 | { 54 | brand: 'asus', 55 | model: 'tuf', 56 | series: '3090', 57 | url: 'https://www.bestbuy.ca/en-ca/product/asus-tuf-gaming-geforce-rtx-3090-24gb-gddr6x-video-card/14953247?intl=nosplash' 58 | }, 59 | { 60 | brand: 'asus', 61 | model: 'rog strix', 62 | series: '3090', 63 | url: 'https://www.bestbuy.ca/en-ca/product/asus-rog-strix-geforce-rtx-3090-24gb-gddr6x-video-card/14954117?intl=nosplash' 64 | }, 65 | { 66 | brand: 'msi', 67 | model: 'ventus 3x', 68 | series: '3090', 69 | url: 'https://www.bestbuy.ca/en-ca/product/msi-nvidia-geforce-rtx-3090-ventus-3x-oc-24gb-gddr6x-video-card/14966477?intl=nosplash' 70 | } 71 | ], 72 | name: 'bestbuy-ca', 73 | waitUntil: 'domcontentloaded' 74 | }; 75 | -------------------------------------------------------------------------------- /src/store/model/bestbuy.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const BestBuy: Store = { 4 | labels: { 5 | inStock: { 6 | container: '.v-m-bottom-g', 7 | text: ['add to cart'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'test:brand', 13 | model: 'test:model', 14 | series: 'test:series', 15 | url: 'https://www.bestbuy.com/site/evga-ko-ultra-gaming-nvidia-geforce-rtx-2060-6gb-gddr6-pci-express-3-0-graphics-card-black-gray/6403801.p?skuId=6403801&intl=nosplash' 16 | }, 17 | { 18 | brand: 'nvidia', 19 | cartUrl: 'https://api.bestbuy.com/click/-/6429440/cart', 20 | model: 'founders edition', 21 | series: '3080', 22 | url: 'https://www.bestbuy.com/site/nvidia-geforce-rtx-3080-10gb-gddr6x-pci-express-4-0-graphics-card-titanium-and-black/6429440.p?skuId=6429440&intl=nosplash' 23 | }, 24 | { 25 | brand: 'asus', 26 | cartUrl: 'https://api.bestbuy.com/click/-/6432445/cart', 27 | model: 'rog strix', 28 | series: '3080', 29 | url: 'https://www.bestbuy.com/site/asus-geforce-rtx-3080-10gb-gddr6x-pci-express-4-0-strix-graphics-card-black/6432445.p?skuId=6432445&intl=nosplash' 30 | }, 31 | { 32 | brand: 'evga', 33 | cartUrl: 'https://api.bestbuy.com/click/-/6432399/cart', 34 | model: 'xc3 black', 35 | series: '3080', 36 | url: 'https://www.bestbuy.com/site/evga-geforce-rtx-3080-10gb-gddr6x-pci-express-4-0-graphics-card/6432399.p?skuId=6432399&intl=nosplash' 37 | }, 38 | { 39 | brand: 'evga', 40 | cartUrl: 'https://api.bestbuy.com/click/-/6432400/cart', 41 | model: 'xc3 ultra', 42 | series: '3080', 43 | url: 'https://www.bestbuy.com/site/evga-geforce-rtx-3080-10gb-gddr6x-pci-express-4-0-graphics-card/6432400.p?skuId=6432400&intl=nosplash' 44 | }, 45 | { 46 | brand: 'gigabyte', 47 | cartUrl: 'https://api.bestbuy.com/click/-/6430620/cart', 48 | model: 'gaming oc', 49 | series: '3080', 50 | url: 'https://www.bestbuy.com/site/gigabyte-geforce-rtx-3080-10g-gddr6x-pci-express-4-0-graphics-card-black/6430620.p?acampID=0&cmp=RMX&loc=Hatch&ref=198&skuId=6430620&intl=nosplash' 51 | }, 52 | { 53 | brand: 'gigabyte', 54 | cartUrl: 'https://api.bestbuy.com/click/-/6430621/cart', 55 | model: 'eagle oc', 56 | series: '3080', 57 | url: 'https://www.bestbuy.com/site/gigabyte-geforce-rtx-3080-10g-gddr6x-pci-express-4-0-graphics-card-black/6430621.p?skuId=6430621&intl=nosplash' 58 | }, 59 | { 60 | brand: 'msi', 61 | cartUrl: 'https://api.bestbuy.com/click/-/6430175/cart', 62 | model: 'ventus 3x oc', 63 | series: '3080', 64 | url: 'https://www.bestbuy.com/site/msi-geforce-rtx-3080-ventus-3x-10g-oc-bv-gddr6x-pci-express-4-0-graphic-card-black-silver/6430175.p?skuId=6430175&intl=nosplash' 65 | }, 66 | { 67 | brand: 'pny', 68 | cartUrl: 'https://api.bestbuy.com/click/-/6432655/cart', 69 | model: 'xlr8 rgb', 70 | series: '3080', 71 | url: 'https://www.bestbuy.com/site/pny-geforce-rtx-3080-10gb-xlr8-gaming-epic-x-rgb-triple-fan-graphics-card/6432655.p?skuId=6432655&intl=nosplash' 72 | }, 73 | { 74 | brand: 'pny', 75 | cartUrl: 'https://api.bestbuy.com/click/-/6432658/cart', 76 | model: 'xlr8 rgb', 77 | series: '3080', 78 | url: 'https://www.bestbuy.com/site/pny-geforce-rtx-3080-10gb-xlr8-gaming-epic-x-rgb-triple-fan-graphics-card/6432658.p?skuId=6432658&intl=nosplash' 79 | }, 80 | { 81 | brand: 'nvidia', 82 | cartUrl: 'https://api.bestbuy.com/click/-/6429434/cart', 83 | model: 'founders edition', 84 | series: '3090', 85 | url: 'https://www.bestbuy.com/site/nvidia-geforce-rtx-3090-24gb-gddr6x-pci-express-4-0-graphics-card-titanium-and-black/6429434.p?skuId=6429434&intl=nosplash' 86 | }, 87 | { 88 | brand: 'asus', 89 | cartUrl: 'https://api.bestbuy.com/click/-/6432447/cart', 90 | model: 'rog strix', 91 | series: '3090', 92 | url: 'https://www.bestbuy.com/site/asus-geforce-rtx-3090-24gb-gddr6x-pci-express-4-0-strix-graphics-card-black/6432447.p?skuId=6432447&intl=nosplash' 93 | }, 94 | { 95 | brand: 'asus', 96 | cartUrl: 'https://api.bestbuy.com/click/-/6432446/cart', 97 | model: 'tuf', 98 | series: '3090', 99 | url: 'https://www.bestbuy.com/site/asus-tuf-rtx-3090-24gb-gddr6x-pci-express-4-0-graphics-card-black/6432446.p?skuId=6432446&intl=nosplash' 100 | }, 101 | { 102 | brand: 'msi', 103 | cartUrl: 'https://api.bestbuy.com/click/-/6430215/cart', 104 | model: 'ventus 3x oc', 105 | series: '3090', 106 | url: 'https://www.bestbuy.com/site/msi-geforce-rtx-3090-ventus-3x-24g-oc-bv-24gb-gddr6x-pci-express-4-0-graphics-card-black-silver/6430215.p?skuId=6430215&intl=nosplash' 107 | }, 108 | { 109 | brand: 'gigabyte', 110 | cartUrl: 'https://api.bestbuy.com/click/-/6430623/cart', 111 | model: 'gaming', 112 | series: '3090', 113 | url: 'https://www.bestbuy.com/site/gigabyte-geforce-rtx-3090-24g-gddr6x-pci-express-4-0-graphics-card-black/6430623.p?skuId=6430623&intl=nosplash' 114 | }, 115 | { 116 | brand: 'gigabyte', 117 | cartUrl: 'https://api.bestbuy.com/click/-/6430624/cart', 118 | model: 'eagle', 119 | series: '3090', 120 | url: 'https://www.bestbuy.com/site/gigabyte-geforce-rtx-3090-24g-gddr6x-pci-express-4-0-graphics-card-black/6430624.p?skuId=6430624&intl=nosplash' 121 | }, 122 | { 123 | brand: 'evga', 124 | cartUrl: 'https://api.bestbuy.com/click/-/6434363/cart', 125 | model: 'xc3', 126 | series: '3090', 127 | url: 'https://www.bestbuy.com/site/evga-geforce-rtx-3090-24gb-gddr6x-pci-express-4-0-graphics-card/6434363.p?skuId=6434363&intl=nosplash' 128 | }, 129 | { 130 | brand: 'pny', 131 | cartUrl: 'https://api.bestbuy.com/click/-/6432657/cart', 132 | model: 'xlr8 rgb', 133 | series: '3090', 134 | url: 'https://www.bestbuy.com/site/pny-geforce-rtx-3090-24gb-xlr8-gaming-epic-x-rgb-triple-fan-graphics-card/6432657.p?skuId=6432657&intl=nosplash' 135 | } 136 | ], 137 | name: 'bestbuy' 138 | }; 139 | -------------------------------------------------------------------------------- /src/store/model/evga-eu.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const EvgaEu: Store = { 4 | labels: { 5 | inStock: { 6 | container: '.product-buy-specs', 7 | text: ['add to cart'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'evga', 13 | model: 'xc3 black', 14 | series: '3080', 15 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3881-KR' 16 | }, 17 | { 18 | brand: 'evga', 19 | model: 'ftw3', 20 | series: '3080', 21 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3895-KR' 22 | }, 23 | { 24 | brand: 'evga', 25 | model: 'xc3', 26 | series: '3080', 27 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3883-KR' 28 | }, 29 | { 30 | brand: 'evga', 31 | model: 'xc3 ultra', 32 | series: '3080', 33 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3885-KR' 34 | } 35 | ], 36 | name: 'evga-eu' 37 | }; 38 | -------------------------------------------------------------------------------- /src/store/model/evga.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Evga: Store = { 4 | labels: { 5 | inStock: { 6 | container: '.product-buy-specs', 7 | text: ['add to cart'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'test:brand', 13 | model: 'test:model', 14 | series: 'test:series', 15 | url: 'https://www.evga.com/products/product.aspx?pn=06G-P4-2065-KR' 16 | }, 17 | { 18 | brand: 'evga', 19 | model: 'xc3 black', 20 | series: '3080', 21 | url: 'https://www.evga.com/products/product.aspx?pn=10G-P5-3881-KR' 22 | }, 23 | { 24 | brand: 'evga', 25 | model: 'ftw3 ultra', 26 | series: '3080', 27 | url: 'https://www.evga.com/products/product.aspx?pn=10G-P5-3897-KR' 28 | }, 29 | { 30 | brand: 'evga', 31 | model: 'ftw3', 32 | series: '3080', 33 | url: 'https://www.evga.com/products/product.aspx?pn=10G-P5-3895-KR' 34 | }, 35 | { 36 | brand: 'evga', 37 | model: 'xc3', 38 | series: '3080', 39 | url: 'https://www.evga.com/products/product.aspx?pn=10G-P5-3883-KR' 40 | }, 41 | { 42 | brand: 'evga', 43 | model: 'xc3 ultra', 44 | series: '3080', 45 | url: 'https://www.evga.com/products/product.aspx?pn=10G-P5-3885-KR' 46 | }, 47 | { 48 | brand: 'evga', 49 | model: 'xc3 black', 50 | series: '3090', 51 | url: 'https://www.evga.com/products/product.aspx?pn=24G-P5-3971-KR' 52 | }, 53 | { 54 | brand: 'evga', 55 | model: 'ftw3 ultra', 56 | series: '3090', 57 | url: 'https://www.evga.com/products/product.aspx?pn=24G-P5-3987-KR' 58 | }, 59 | { 60 | brand: 'evga', 61 | model: 'ftw3', 62 | series: '3090', 63 | url: 'https://www.evga.com/products/product.aspx?pn=24G-P5-3985-KR' 64 | }, 65 | { 66 | brand: 'evga', 67 | model: 'xc3', 68 | series: '3090', 69 | url: 'https://www.evga.com/products/product.aspx?pn=24G-P5-3973-KR' 70 | }, 71 | { 72 | brand: 'evga', 73 | model: 'xc3 ultra', 74 | series: '3090', 75 | url: 'https://www.evga.com/products/product.aspx?pn=24G-P5-3975-KR' 76 | } 77 | ], 78 | name: 'evga' 79 | }; 80 | 81 | -------------------------------------------------------------------------------- /src/store/model/helpers/card.ts: -------------------------------------------------------------------------------- 1 | export interface Card { 2 | brand: string; 3 | model: string; 4 | } 5 | 6 | export function parseCard(name: string): Card | null { 7 | name = name.replace(/[^\w ]+/g, '').trim(); 8 | name = name.replace(/\bgraphics card\b/gi, '').trim(); 9 | name = name.replace(/\b\w+ fan\b/gi, '').trim(); 10 | name = name.replace(/\s{2,}/g, ' '); 11 | 12 | let model = name.split(' '); 13 | const brand = model.shift(); 14 | 15 | if (!brand) { 16 | return null; 17 | } 18 | 19 | // Some vendors have oc at the beginning of the product name, 20 | // store whether the card contains the term "oc" and remove 21 | // it during filtering, then add it to the end of the name. 22 | let isOC = false; 23 | 24 | /* eslint-disable @typescript-eslint/prefer-regexp-exec */ 25 | model = model.filter(word => { 26 | if (word.toLowerCase() === 'oc') { 27 | isOC = true; 28 | return false; 29 | } 30 | 31 | return !word.match(/^(nvidia|geforce|rtx|amp[ae]re|graphics|card|gpu|pci-?e(xpress)?|ray-?tracing|ray|tracing|core|boost)$/i) && 32 | !word.match(/^(\d+(?:gb?|mhz)?|gb|mhz|g?ddr(\d+x?)?)$/i); 33 | }); 34 | /* eslint-enable @typescript-eslint/prefer-regexp-exec */ 35 | 36 | if (isOC) { 37 | model.push('OC'); 38 | } 39 | 40 | if (model.length === 0) { 41 | return null; 42 | } 43 | 44 | return { 45 | brand: brand.toLowerCase(), 46 | model: model.join(' ').toLowerCase().replace(/ gaming\b/g, '').trim() 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/store/model/helpers/nvidia.ts: -------------------------------------------------------------------------------- 1 | import {Browser, Page, Response} from 'puppeteer'; 2 | import {NvidiaRegionInfo, regionInfos} from '../nvidia-api'; 3 | import {Config} from '../../../config'; 4 | import {Link} from '../store'; 5 | import {Logger} from '../../../logger'; 6 | import open from 'open'; 7 | import {timestampUrlParameter} from '../../timestamp-url-parameter'; 8 | 9 | function getRegionInfo(): NvidiaRegionInfo { 10 | let country = Config.store.country; 11 | if (!regionInfos.has(country)) { 12 | country = 'usa'; 13 | } 14 | 15 | const regionInfo = regionInfos.get(country); 16 | if (!regionInfo) { 17 | throw new Error(`LogicException could not retrieve region info for ${country}`); 18 | } 19 | 20 | return regionInfo; 21 | } 22 | 23 | function nvidiaStockUrl(id: number, drLocale: string, currency: string): string { 24 | return `https://api-prod.nvidia.com/direct-sales-shop/DR/products/${drLocale}/${currency}/${id}?` + 25 | timestampUrlParameter().slice(1); 26 | } 27 | 28 | interface NvidiaSessionTokenJSON { 29 | session_token: string; 30 | } 31 | 32 | interface NvidiaAddToCardJSON { 33 | location: string; 34 | } 35 | 36 | function nvidiaSessionUrl(drLocale: string): string { 37 | return `https://store.nvidia.com/store/nvidia/SessionToken?format=json&locale=${drLocale}` + 38 | timestampUrlParameter(); 39 | } 40 | 41 | async function addToCartAndGetLocationRedirect(page: Page, sessionToken: string, productId: number): Promise { 42 | const url = 'https://api-prod.nvidia.com/direct-sales-shop/DR/add-to-cart'; 43 | 44 | page.removeAllListeners('request'); 45 | 46 | await page.setRequestInterception(true); 47 | 48 | page.on('request', interceptedRequest => { 49 | void interceptedRequest.continue({ 50 | headers: { 51 | ...interceptedRequest.headers(), 52 | 'content-type': 'application/json', 53 | nvidia_shop_id: sessionToken 54 | }, 55 | method: 'POST', 56 | postData: JSON.stringify({ 57 | products: [ 58 | {productId, quantity: 1} 59 | ] 60 | }) 61 | }); 62 | }); 63 | 64 | const response = await page.goto(url, {waitUntil: 'networkidle0'}); 65 | if (response === null) { 66 | throw new Error('NvidiaAddToCartUnavailable'); 67 | } 68 | 69 | const locationData = await response.json() as NvidiaAddToCardJSON; 70 | 71 | return locationData.location; 72 | } 73 | 74 | function fallbackCartUrl(nvidiaLocale: string): string { 75 | return `https://www.nvidia.com/${nvidiaLocale}/shop/geforce?${timestampUrlParameter()}`; 76 | } 77 | 78 | export function generateOpenCartAction(id: number, drLocale: string, cardName: string) { 79 | return async (browser: Browser) => { 80 | const page = await browser.newPage(); 81 | 82 | Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, starting auto add to cart 🚀🚀🚀`); 83 | 84 | let response: Response | null; 85 | let cartUrl: string; 86 | try { 87 | Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, getting access token 🚀🚀🚀`); 88 | 89 | response = await page.goto(nvidiaSessionUrl(drLocale), {waitUntil: 'networkidle0'}); 90 | if (response === null) { 91 | throw new Error('NvidiaAccessTokenUnavailable'); 92 | } 93 | 94 | const data = await response.json() as NvidiaSessionTokenJSON; 95 | const sessionToken = data.session_token; 96 | 97 | Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, adding to cart 🚀🚀🚀`); 98 | 99 | cartUrl = await addToCartAndGetLocationRedirect(page, sessionToken, id); 100 | 101 | Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, opening checkout page 🚀🚀🚀`); 102 | Logger.info(cartUrl); 103 | 104 | await open(cartUrl); 105 | } catch (error) { 106 | Logger.debug(error); 107 | Logger.error(`✖ [nvidia] ${cardName} could not automatically add to cart, opening page`, error); 108 | 109 | cartUrl = fallbackCartUrl(drLocale); 110 | await open(cartUrl); 111 | } 112 | 113 | await page.close(); 114 | 115 | return cartUrl; 116 | }; 117 | } 118 | 119 | export function generateLinks(): Link[] { 120 | const {drLocale, fe3080Id, fe3090Id, fe2060SuperId, currency} = getRegionInfo(); 121 | 122 | const links: Link[] = []; 123 | 124 | if (fe2060SuperId) { 125 | links.push({ 126 | brand: 'test:brand', 127 | model: 'test:model', 128 | openCartAction: generateOpenCartAction(fe2060SuperId, drLocale, 'TEST CARD debug'), 129 | series: 'test:series', 130 | url: nvidiaStockUrl(fe2060SuperId, drLocale, currency) 131 | }); 132 | } 133 | 134 | if (fe3080Id) { 135 | links.push({ 136 | brand: 'nvidia', 137 | model: 'founders edition', 138 | openCartAction: generateOpenCartAction(fe3080Id, drLocale, 'nvidia founders edition 3080'), 139 | series: '3080', 140 | url: nvidiaStockUrl(fe3080Id, drLocale, currency) 141 | }); 142 | } 143 | 144 | if (fe3090Id) { 145 | links.push({ 146 | brand: 'nvidia', 147 | model: 'founders edition', 148 | openCartAction: generateOpenCartAction(fe3090Id, drLocale, 'nvidia founders edition 3090'), 149 | series: '3090', 150 | url: nvidiaStockUrl(fe3090Id, drLocale, currency) 151 | }); 152 | } 153 | 154 | return links; 155 | } 156 | -------------------------------------------------------------------------------- /src/store/model/index.ts: -------------------------------------------------------------------------------- 1 | import {Adorama} from './adorama'; 2 | import {Amazon} from './amazon'; 3 | import {AmazonCa} from './amazon-ca'; 4 | import {AmazonDe} from './amazon-de'; 5 | import {AmazonNl} from './amazon-nl'; 6 | import {Asus} from './asus'; 7 | import {BAndH} from './bandh'; 8 | import {BestBuy} from './bestbuy'; 9 | import {BestBuyCa} from './bestbuy-ca'; 10 | import {Config} from '../../config'; 11 | import {Evga} from './evga'; 12 | import {EvgaEu} from './evga-eu'; 13 | import {Logger} from '../../logger'; 14 | import {MicroCenter} from './microcenter'; 15 | import {Newegg} from './newegg'; 16 | import {NeweggCa} from './newegg-ca'; 17 | import {Nvidia} from './nvidia'; 18 | import {NvidiaApi} from './nvidia-api'; 19 | import {OfficeDepot} from './officedepot'; 20 | import {Pny} from './pny'; 21 | import {Store} from './store'; 22 | import {Zotac} from './zotac'; 23 | 24 | const masterList = new Map([ 25 | [Adorama.name, Adorama], 26 | [Amazon.name, Amazon], 27 | [AmazonCa.name, AmazonCa], 28 | [AmazonDe.name, AmazonDe], 29 | [AmazonNl.name, AmazonNl], 30 | [Asus.name, Asus], 31 | [BAndH.name, BAndH], 32 | [BestBuy.name, BestBuy], 33 | [BestBuyCa.name, BestBuyCa], 34 | [Evga.name, Evga], 35 | [EvgaEu.name, EvgaEu], 36 | [MicroCenter.name, MicroCenter], 37 | [Newegg.name, Newegg], 38 | [NeweggCa.name, NeweggCa], 39 | [Nvidia.name, Nvidia], 40 | [NvidiaApi.name, NvidiaApi], 41 | [OfficeDepot.name, OfficeDepot], 42 | [Pny.name, Pny], 43 | [Zotac.name, Zotac] 44 | ]); 45 | 46 | const list = new Map(); 47 | 48 | for (const name of Config.store.stores) { 49 | if (masterList.has(name)) { 50 | list.set(name, masterList.get(name)); 51 | } else { 52 | const logString = `No store named ${name}, skipping.`; 53 | Logger.warn(logString); 54 | } 55 | } 56 | 57 | Logger.info(`ℹ selected stores: ${Array.from(list.keys()).join(', ')}`); 58 | 59 | if (Config.store.showOnlyBrands.length > 0) { 60 | Logger.info(`ℹ selected brands: ${Config.store.showOnlyBrands.join(', ')}`); 61 | } 62 | 63 | if (Config.store.showOnlyModels.length > 0) { 64 | Logger.info(`ℹ selected models: ${Config.store.showOnlyModels.join(', ')}`); 65 | } 66 | 67 | if (Config.store.showOnlySeries.length > 0) { 68 | Logger.info(`ℹ selected series: ${Config.store.showOnlySeries.join(', ')}`); 69 | } 70 | 71 | export const Stores = Array.from(list.values()) as Store[]; 72 | 73 | export * from './store'; 74 | -------------------------------------------------------------------------------- /src/store/model/microcenter.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../../config'; 2 | import {Store} from './store'; 3 | 4 | const MicroCenterLocation = Config.store.microCenterLocation; 5 | const microCenterLocationToId: Map = new Map([ 6 | ['web', '029'], 7 | ['brooklyn', '115'], 8 | ['brentwood', '095'], 9 | ['cambridge', '121'], 10 | ['chicago', '151'], 11 | ['columbus', '141'], 12 | ['dallas', '131'], 13 | ['denver', '181'], 14 | ['duluth', '065'], 15 | ['fairfax', '081'], 16 | ['flushing', '145'], 17 | ['houston', '155'], 18 | ['madison-heights', '055'], 19 | ['marietta', '041'], 20 | ['mayfield-heights', '051'], 21 | ['north-jersey', '075'], 22 | ['overland-park', '191'], 23 | ['parkville', '125'], 24 | ['rockville', '085'], 25 | ['sharonville', '071'], 26 | ['st-davids', '061'], 27 | ['st-louis-park', '045'], 28 | ['tustin', '101'], 29 | ['westbury', '171'], 30 | ['westmont', '025'], 31 | ['yonkers', '105'] 32 | ]); 33 | 34 | let storeId: string; 35 | if (microCenterLocationToId.get(MicroCenterLocation) === undefined) { 36 | storeId = '029'; 37 | } else { 38 | storeId = microCenterLocationToId.get(MicroCenterLocation)!; 39 | } 40 | 41 | export const MicroCenter: Store = { 42 | labels: { 43 | inStock: { 44 | container: '#cart-options', 45 | text: ['in stock'] 46 | } 47 | }, 48 | links: [ 49 | { 50 | brand: 'test:brand', 51 | model: 'test:model', 52 | series: 'test:series', 53 | url: `https://www.microcenter.com/product/618433/evga-geforce-rtx-2060-ko-ultra-overclocked-dual-fan-6gb-gddr6-pcie-30-graphics-card/?storeid=${storeId}` 54 | }, 55 | { 56 | brand: 'evga', 57 | model: 'xc3 ultra', 58 | series: '3080', 59 | url: `https://www.microcenter.com/product/628344/evga-geforce-rtx-3080-xc3-ultra-gaming-triple-fan-10gb-gddr6x-pcie-40-graphics-card/?storeid=${storeId}` 60 | }, 61 | { 62 | brand: 'msi', 63 | model: 'ventus 3x', 64 | series: '3080', 65 | url: `https://www.microcenter.com/product/628331/msi-geforce-rtx-3080-ventus-3x-overclocked-triple-fan-10gb-gddr6x-pcie-40-graphics-card/?storeid=${storeId}` 66 | }, 67 | { 68 | brand: 'asus', 69 | model: 'tuf', 70 | series: '3080', 71 | url: `https://www.microcenter.com/product/628303/asus-geforce-rtx-3080-tuf-gaming-triple-fan-10gb-gddr6x-pcie-40-graphics-card/?storeid=${storeId}` 72 | }, 73 | { 74 | brand: 'msi', 75 | model: 'gaming x trio', 76 | series: '3080', 77 | url: `https://www.microcenter.com/product/628330/msi-geforce-rtx-3080-gaming-x-trio-triple-fan-10gb-gddr6x-pcie-40-graphics-card/?storeid=${storeId}` 78 | }, 79 | { 80 | brand: 'evga', 81 | model: 'xc3 black', 82 | series: '3080', 83 | url: `https://www.microcenter.com/product/628340/evga-geforce-rtx-3080-xc3-black-triple-fan-10gb-gddr6x-pcie-40-graphics-card/?storeid=${storeId}` 84 | }, 85 | { 86 | brand: 'zotac', 87 | model: 'trinity', 88 | series: '3080', 89 | url: `https://www.microcenter.com/product/628607/zotac-geforce-rtx-3080-trinity-overclocked-triple-fan-10gb-gddr6x-pcie-40-graphics-card/?storeid=${storeId}` 90 | } 91 | ], 92 | name: 'microcenter' 93 | }; 94 | -------------------------------------------------------------------------------- /src/store/model/newegg-ca.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const NeweggCa: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['are you a human?'] 8 | }, 9 | inStock: { 10 | container: '#landingpage-cart .btn-primary span', 11 | text: ['add to cart'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.newegg.ca/evga-geforce-rtx-2060-06g-p4-2066-kr/p/N82E16814487488' 20 | }, 21 | { 22 | brand: 'asus', 23 | model: 'tuf', 24 | series: '3080', 25 | url: 'https://www.newegg.ca/asus-geforce-rtx-3080-tuf-rtx3080-10g-gaming/p/N82E16814126453' 26 | }, 27 | { 28 | brand: 'evga', 29 | model: 'xc3 black', 30 | series: '3080', 31 | url: 'https://www.newegg.ca/evga-geforce-rtx-3080-10g-p5-3881-kr/p/N82E16814487522' 32 | }, 33 | { 34 | brand: 'evga', 35 | model: 'xc3', 36 | series: '3080', 37 | url: 'https://www.newegg.ca/evga-geforce-rtx-3080-10g-p5-3883-kr/p/N82E16814487521' 38 | }, 39 | { 40 | brand: 'evga', 41 | model: 'xc3 ultra', 42 | series: '3080', 43 | url: 'https://www.newegg.ca/evga-geforce-rtx-3080-10g-p5-3885-kr/p/N82E16814487520' 44 | }, 45 | { 46 | brand: 'msi', 47 | model: 'ventus 3x oc', 48 | series: '3080', 49 | url: 'https://www.newegg.ca/msi-geforce-rtx-3080-rtx-3080-ventus-3x-10g-oc/p/N82E16814137598' 50 | }, 51 | { 52 | brand: 'msi', 53 | model: 'gaming x trio', 54 | series: '3080', 55 | url: 'https://www.newegg.ca/msi-geforce-rtx-3080-rtx-3080-gaming-x-trio-10g/p/N82E16814137597' 56 | }, 57 | { 58 | brand: 'gigabyte', 59 | model: 'gaming oc', 60 | series: '3080', 61 | url: 'https://www.newegg.ca/gigabyte-geforce-rtx-3080-gv-n3080gaming-oc-10gd/p/N82E16814932329' 62 | }, 63 | { 64 | brand: 'gigabyte', 65 | model: 'eagle oc', 66 | series: '3080', 67 | url: 'https://www.newegg.ca/gigabyte-geforce-rtx-3080-gv-n3080eagle-oc-10gd/p/N82E16814932330' 68 | }, 69 | { 70 | brand: 'zotac', 71 | model: 'trinity', 72 | series: '3080', 73 | url: 'https://www.newegg.ca/zotac-geforce-rtx-3080-zt-a30800d-10p/p/N82E16814500502' 74 | }, 75 | { 76 | brand: 'asus', 77 | model: 'tuf oc', 78 | series: '3080', 79 | url: 'https://www.newegg.ca/asus-geforce-rtx-3080-tuf-rtx3080-o10g-gaming/p/N82E16814126452' 80 | }, 81 | { 82 | brand: 'msi', 83 | model: 'gaming x trio', 84 | series: '3090', 85 | url: 'https://www.newegg.ca/msi-geforce-rtx-3090-rtx-3090-gaming-x-trio-24g/p/N82E16814137595' 86 | }, 87 | { 88 | brand: 'gigabyte', 89 | model: 'gaming oc', 90 | series: '3090', 91 | url: 'https://www.newegg.ca/gigabyte-geforce-rtx-3090-gv-n3090gaming-oc-24gd/p/N82E16814932327' 92 | }, 93 | { 94 | brand: 'msi', 95 | model: 'ventus 3x', 96 | series: '3090', 97 | url: 'https://www.newegg.ca/msi-geforce-rtx-3090-rtx-3090-ventus-3x-24g-oc/p/N82E16814137596' 98 | }, 99 | { 100 | brand: 'zotac', 101 | model: 'trinity', 102 | series: '3090', 103 | url: 'https://www.newegg.ca/zotac-geforce-rtx-3090-zt-a30900d-10p/p/N82E16814500503' 104 | }, 105 | { 106 | brand: 'asus', 107 | model: 'tuf', 108 | series: '3090', 109 | url: 'https://www.newegg.ca/asus-geforce-rtx-3090-tuf-rtx3090-o24g-gaming/p/N82E16814126454' 110 | }, 111 | { 112 | brand: 'asus', 113 | model: 'rog strix', 114 | series: '3090', 115 | url: 'https://www.newegg.ca/asus-geforce-rtx-3090-rog-strix-rtx3090-o24g-gaming/p/N82E16814126456' 116 | } 117 | ], 118 | name: 'newegg-ca' 119 | }; 120 | -------------------------------------------------------------------------------- /src/store/model/newegg.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Newegg: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['are you a human?'] 8 | }, 9 | inStock: { 10 | container: '#landingpage-cart .btn-primary span', 11 | text: ['add to cart'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.newegg.com/evga-geforce-rtx-2060-06g-p4-2066-kr/p/N82E16814487488' 20 | }, 21 | { 22 | brand: 'asus', 23 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814126453', 24 | model: 'tuf', 25 | series: '3080', 26 | url: 'https://www.newegg.com/asus-geforce-rtx-3080-tuf-rtx3080-10g-gaming/p/N82E16814126453' 27 | }, 28 | { 29 | brand: 'evga', 30 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487518', 31 | model: 'ftw3 ultra', 32 | series: '3080', 33 | url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3897-kr/p/N82E16814487518' 34 | }, 35 | { 36 | brand: 'evga', 37 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487519', 38 | model: 'ftw3', 39 | series: '3080', 40 | url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3895-kr/p/N82E16814487519' 41 | }, 42 | { 43 | brand: 'evga', 44 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487522', 45 | model: 'xc3 black', 46 | series: '3080', 47 | url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3881-kr/p/N82E16814487522' 48 | }, 49 | { 50 | brand: 'evga', 51 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487521', 52 | model: 'xc3', 53 | series: '3080', 54 | url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3883-kr/p/N82E16814487521' 55 | }, 56 | // Removed from Newegg currently not available in US 57 | // { 58 | // brand: 'evga', 59 | // cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487520', 60 | // model: 'xc3 ultra', 61 | // series: '3080', 62 | // url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3885-kr/p/N82E16814487520' 63 | // }, 64 | { 65 | brand: 'msi', 66 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814137600', 67 | model: 'ventus 3x', 68 | series: '3080', 69 | url: 'https://www.newegg.com/msi-geforce-rtx-3080-rtx-3080-ventus-3x-10g/p/N82E16814137600' 70 | }, 71 | { 72 | brand: 'msi', 73 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814137598', 74 | model: 'ventus 3x oc', 75 | series: '3080', 76 | url: 'https://www.newegg.com/msi-geforce-rtx-3080-rtx-3080-ventus-3x-10g-oc/p/N82E16814137598' 77 | }, 78 | { 79 | brand: 'msi', 80 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814137597', 81 | model: 'gaming x trio', 82 | series: '3080', 83 | url: 'https://www.newegg.com/msi-geforce-rtx-3080-rtx-3080-gaming-x-trio-10g/p/N82E16814137597' 84 | }, 85 | { 86 | brand: 'gigabyte', 87 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814932329', 88 | model: 'gaming oc', 89 | series: '3080', 90 | url: 'https://www.newegg.com/gigabyte-geforce-rtx-3080-gv-n3080gaming-oc-10gd/p/N82E16814932329' 91 | }, 92 | { 93 | brand: 'gigabyte', 94 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814932330', 95 | model: 'eagle oc', 96 | series: '3080', 97 | url: 'https://www.newegg.com/gigabyte-geforce-rtx-3080-gv-n3080eagle-oc-10gd/p/N82E16814932330' 98 | }, 99 | { 100 | brand: 'zotac', 101 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814500502', 102 | model: 'trinity', 103 | series: '3080', 104 | url: 'https://www.newegg.com/zotac-geforce-rtx-3080-zt-a30800d-10p/p/N82E16814500502' 105 | }, 106 | { 107 | brand: 'asus', 108 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814126457', 109 | model: 'rog strix', 110 | series: '3080', 111 | url: 'https://www.newegg.com/asus-geforce-rtx-3080-rog-strix-rtx3080-o10g-gaming/p/N82E16814126457' 112 | }, 113 | { 114 | brand: 'asus', 115 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814126452', 116 | model: 'tuf oc', 117 | series: '3080', 118 | url: 'https://www.newegg.com/asus-geforce-rtx-3080-tuf-rtx3080-o10g-gaming/p/N82E16814126452' 119 | }, 120 | { 121 | brand: 'zotac', 122 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814500504', 123 | model: 'trinity oc', 124 | series: '3080', 125 | url: 'https://www.newegg.com/zotac-geforce-rtx-3080-zt-t30800j-10p/p/N82E16814500504' 126 | }, 127 | { 128 | brand: 'pny', 129 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814133809', 130 | model: 'xlr8 rgb', 131 | series: '3080', 132 | url: 'https://www.newegg.com/pny-geforce-rtx-3080-vcg308010tfxppb/p/N82E16814133809' 133 | }, 134 | { 135 | brand: 'asus', 136 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814126455', 137 | model: 'tuf', 138 | series: '3090', 139 | url: 'https://www.newegg.com/asus-geforce-rtx-3090-tuf-rtx3090-24g-gaming/p/N82E16814126455' 140 | }, 141 | { 142 | brand: 'asus', 143 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814126456', 144 | model: 'rog strix', 145 | series: '3090', 146 | url: 'https://www.newegg.com/asus-geforce-rtx-3090-rog-strix-rtx3090-o24g-gaming/p/N82E16814126456' 147 | }, 148 | { 149 | brand: 'msi', 150 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814137595', 151 | model: 'gaming x trio', 152 | series: '3090', 153 | url: 'https://www.newegg.com/msi-geforce-rtx-3090-rtx-3090-gaming-x-trio-24g/p/N82E16814137595' 154 | }, 155 | { 156 | brand: 'msi', 157 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814137596', 158 | model: 'ventus 3x oc', 159 | series: '3090', 160 | url: 'https://www.newegg.com/msi-geforce-rtx-3090-rtx-3090-ventus-3x-24g-oc/p/N82E16814137596' 161 | }, 162 | { 163 | brand: 'zotac', 164 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814500503', 165 | model: 'trinity', 166 | series: '3090', 167 | url: 'https://www.newegg.com/zotac-geforce-rtx-3090-zt-a30900d-10p/p/N82E16814500503' 168 | }, 169 | { 170 | brand: 'msi', 171 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814137599', 172 | model: 'ventus 3x', 173 | series: '3090', 174 | url: 'https://www.newegg.com/msi-geforce-rtx-3090-rtx-3090-ventus-3x-24g/p/N82E16814137599' 175 | }, 176 | { 177 | brand: 'evga', 178 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487525', 179 | model: 'ftw3 gaming', 180 | series: '3090', 181 | url: 'https://www.newegg.com/evga-geforce-rtx-3090-24g-p5-3985-kr/p/N82E16814487525' 182 | }, 183 | { 184 | brand: 'evga', 185 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487524', 186 | model: 'xc3 ultra gaming', 187 | series: '3090', 188 | url: 'https://www.newegg.com/evga-geforce-rtx-3090-24g-p5-3975-kr/p/N82E16814487524' 189 | }, 190 | { 191 | brand: 'evga', 192 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487526', 193 | model: 'ftw3 ultra gaming', 194 | series: '3090', 195 | url: 'https://www.newegg.com/evga-geforce-rtx-3090-24g-p5-3987-kr/p/N82E16814487526' 196 | }, 197 | { 198 | brand: 'evga', 199 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487527', 200 | model: 'xc3 black', 201 | series: '3090', 202 | url: 'https://www.newegg.com/evga-geforce-rtx-3090-24g-p5-3971-kr/p/N82E16814487527' 203 | }, 204 | { 205 | brand: 'evga', 206 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814487523', 207 | model: 'xc3', 208 | series: '3090', 209 | url: 'https://www.newegg.com/evga-geforce-rtx-3090-24g-p5-3973-kr/p/N82E16814487523' 210 | }, 211 | { 212 | brand: 'gigabyte', 213 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814932327', 214 | model: 'gaming', 215 | series: '3090', 216 | url: 'https://www.newegg.com/gigabyte-geforce-rtx-3090-gv-n3090gaming-oc-24gd/p/N82E16814932327' 217 | }, 218 | { 219 | brand: 'gigabyte', 220 | cartUrl: 'https://secure.newegg.com/Shopping/AddtoCart.aspx?Submit=ADD&ItemList=N82E16814932328', 221 | model: 'eagle', 222 | series: '3090', 223 | url: 'https://www.newegg.com/gigabyte-geforce-rtx-3090-gv-n3090eagle-oc-24gd/p/N82E16814932328' 224 | } 225 | ], 226 | name: 'newegg' 227 | }; 228 | -------------------------------------------------------------------------------- /src/store/model/nvidia-api.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | import {generateLinks} from './helpers/nvidia'; 3 | 4 | // Region/country set by config file, silently ignores null / missing values and defaults to usa 5 | 6 | export interface NvidiaRegionInfo { 7 | currency: string; 8 | drLocale: string; 9 | fe3080Id: number | null; 10 | fe3090Id: number | null; 11 | fe2060SuperId: number | null; 12 | } 13 | 14 | export const regionInfos = new Map([ 15 | ['austria', {currency: 'EUR', drLocale: 'de_de', fe2060SuperId: 5394902900, fe3080Id: 5440853700, fe3090Id: 5444941400}], 16 | ['belgium', {currency: 'EUR', drLocale: 'fr_fr', fe2060SuperId: 5394902700, fe3080Id: 5438795700, fe3090Id: 5438795600}], 17 | ['canada', {currency: 'CAD', drLocale: 'en_us', fe2060SuperId: 5379432500, fe3080Id: 5438481700, fe3090Id: 5438481600}], 18 | ['czechia', {currency: 'CZK', drLocale: 'en_gb', fe2060SuperId: 5394902800, fe3080Id: 5438793800, fe3090Id: 5438793600}], 19 | ['denmark', {currency: 'EUR', drLocale: 'en_gb', fe2060SuperId: 5394903100, fe3080Id: 5438793300, fe3090Id: 5438793500}], 20 | ['finland', {currency: 'EUR', drLocale: 'en_gb', fe2060SuperId: 5394903100, fe3080Id: 5438793300, fe3090Id: 5438793500}], 21 | ['france', {currency: 'EUR', drLocale: 'fr_fr', fe2060SuperId: 5394903200, fe3080Id: 5438795200, fe3090Id: 5438761500}], 22 | ['germany', {currency: 'EUR', drLocale: 'de_de', fe2060SuperId: 5394902900, fe3080Id: 5438792300, fe3090Id: 5438761400}], 23 | ['great_britain', {currency: 'GBP', drLocale: 'en_gb', fe2060SuperId: 5394903300, fe3080Id: 5438792800, fe3090Id: 5438792700}], 24 | ['ireland', {currency: 'GBP', drLocale: 'en_gb', fe2060SuperId: 5394903300, fe3080Id: 5438792800, fe3090Id: 5438792700}], 25 | ['italy', {currency: 'EUR', drLocale: 'it_it', fe2060SuperId: 5394903400, fe3080Id: 5438796200, fe3090Id: 5438796100}], 26 | ['luxembourg', {currency: 'EUR', drLocale: 'fr_fr', fe2060SuperId: 5394902700, fe3080Id: 5438795700, fe3090Id: 5438795600}], 27 | ['netherlands', {currency: 'EUR', drLocale: 'nl_nl', fe2060SuperId: 5394903500, fe3080Id: 5438796700, fe3090Id: 5438796600}], 28 | ['norway', {currency: 'EUR', drLocale: 'nb_no', fe2060SuperId: 5394903600, fe3080Id: 5438797200, fe3090Id: 5438797100}], 29 | ['poland', {currency: 'PLN', drLocale: 'pl_pl', fe2060SuperId: 5394903700, fe3080Id: 5438797700, fe3090Id: 5438797600}], 30 | ['portugal', {currency: 'EUR', drLocale: 'en_gb', fe2060SuperId: null, fe3080Id: 5438794300, fe3090Id: null}], 31 | ['russia', {currency: 'RUB', drLocale: 'ru_ru', fe2060SuperId: null, fe3080Id: null, fe3090Id: null}], 32 | ['spain', {currency: 'EUR', drLocale: 'es_es', fe2060SuperId: 5394903000, fe3080Id: 5438794800, fe3090Id: 5438794700}], 33 | ['sweden', {currency: 'SEK', drLocale: 'sv_se', fe2060SuperId: 5394903900, fe3080Id: 5438798100, fe3090Id: 5438761600}], 34 | ['usa', {currency: 'USD', drLocale: 'en_us', fe2060SuperId: 5379432500, fe3080Id: 5438481700, fe3090Id: 5438481600}] 35 | ]); 36 | 37 | export const NvidiaApi: Store = { 38 | labels: { 39 | inStock: { 40 | container: 'body', 41 | text: ['product_inventory_in_stock'] 42 | } 43 | }, 44 | links: generateLinks(), 45 | name: 'nvidia-api' 46 | }; 47 | -------------------------------------------------------------------------------- /src/store/model/nvidia.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Nvidia: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['are you a human?'] 8 | }, 9 | inStock: { 10 | container: '.main-container', 11 | text: ['add to cart'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.nvidia.com/en-us/shop/geforce/gpu/' 20 | }, 21 | { 22 | brand: 'nvidia', 23 | model: 'founders edition', 24 | series: '3080', 25 | url: 'https://www.nvidia.com/en-us/shop/geforce/gpu/?page=1&limit=9&locale=en-us&category=GPU&gpu=RTX%203080' 26 | }, 27 | { 28 | brand: 'nvidia', 29 | model: 'founders edition', 30 | series: '3080', 31 | url: 'https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3080' 32 | }, 33 | { 34 | brand: 'nvidia', 35 | model: 'founders edition', 36 | series: '3090', 37 | url: 'https://www.nvidia.com/en-us/shop/geforce/gpu/?page=1&limit=9&locale=en-us&category=GPU&gpu=RTX%203090' 38 | }, 39 | { 40 | brand: 'nvidia', 41 | model: 'founders edition', 42 | series: '3090', 43 | url: 'https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3090' 44 | } 45 | ], 46 | name: 'nvidia' 47 | }; 48 | -------------------------------------------------------------------------------- /src/store/model/officedepot.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const OfficeDepot: Store = { 4 | labels: { 5 | captcha: { 6 | container: 'body', 7 | text: ['please verify you are a human'] 8 | }, 9 | inStock: { 10 | container: '#productPurchase', 11 | text: ['add to cart'] 12 | } 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.officedepot.com/a/products/4652239/EVGA-GeForce-RTX-2060-Graphic-Card/' 20 | }, 21 | { 22 | brand: 'pny', 23 | model: 'xlr8', 24 | series: '3080', 25 | url: 'https://www.officedepot.com/a/products/7189374/PNY-GeForce-RTX-3080-10GB-GDDR6X/' 26 | }, 27 | { 28 | brand: 'pny', 29 | model: 'xlr8 rgb', 30 | series: '3080', 31 | url: 'https://www.officedepot.com/a/products/7791294/PNY-GeForce-RTX-3080-10GB-GDDR6X/' 32 | } 33 | ], 34 | name: 'officedepot' 35 | }; 36 | -------------------------------------------------------------------------------- /src/store/model/pny.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Pny: Store = { 4 | labels: { 5 | inStock: { 6 | container: '#ctl01_lbtnAddToCart', 7 | text: ['add to cart'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'test:brand', 13 | model: 'test:model', 14 | series: 'test:series', 15 | url: 'https://www.pny.com/pny-geforce-gtx-1660-super-gaming-oc-sf' 16 | }, 17 | { 18 | brand: 'pny', 19 | model: 'dual fan', 20 | series: '3070', 21 | url: 'https://www.pny.com/pny-geforce-rtx-3070-8gb-df' 22 | }, 23 | { 24 | brand: 'pny', 25 | model: 'xlr8 rgb', 26 | series: '3070', 27 | url: 'https://www.pny.com/geforce-rtx-3070-xlr8-gaming-epic-x-rgb-triple-fan' 28 | }, 29 | { 30 | brand: 'pny', 31 | model: 'xlr8 rgb', 32 | series: '3080', 33 | url: 'https://www.pny.com/geforce-rtx-3080-xlr8-gaming-epic-x-rgb-triple-fan-m' 34 | }, 35 | { 36 | brand: 'pny', 37 | model: 'xlr8 rgb', 38 | series: '3080', 39 | url: 'https://www.pny.com/geforce-rtx-3080-xlr8-gaming-epic-x-rgb-triple-fan-p' 40 | }, 41 | { 42 | brand: 'pny', 43 | model: 'xlr8 rgb', 44 | series: '3090', 45 | url: 'https://www.pny.com/geforce-rtx-3090-xlr8-gaming-epic-x-rgb-triple-fan-m' 46 | }, 47 | { 48 | brand: 'pny', 49 | model: 'xlr8 rgb', 50 | series: '3090', 51 | url: 'https://www.pny.com/geforce-rtx-3090-xlr8-gaming-epic-x-rgb-triple-fan-p' 52 | } 53 | ], 54 | name: 'pny' 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /src/store/model/store.ts: -------------------------------------------------------------------------------- 1 | import {Browser, LoadEvent} from 'puppeteer'; 2 | 3 | export type Element = { 4 | container?: string; 5 | text: string[]; 6 | }; 7 | 8 | export type Series = 'test:series' | '3070' | '3080' | '3090'; 9 | 10 | export type Link = { 11 | brand: 'test:brand' | 'asus' | 'evga' | 'gigabyte' | 'inno3d' | 'kfa2' | 'palit' | 'pny' | 'msi' | 'nvidia' | 'zotac'; 12 | series: Series; 13 | model: string; 14 | url: string; 15 | cartUrl?: string; 16 | openCartAction?: (browser: Browser) => Promise; 17 | screenshot?: string; 18 | }; 19 | 20 | export type LabelQuery = Element[] | Element | string[]; 21 | 22 | export type Labels = { 23 | bannedSeller?: LabelQuery; 24 | captcha?: LabelQuery; 25 | container?: string; 26 | inStock?: LabelQuery; 27 | outOfStock?: LabelQuery; 28 | }; 29 | 30 | export type Store = { 31 | disableAdBlocker?: boolean; 32 | links: Link[]; 33 | linksBuilder?: { 34 | builder: (docElement: cheerio.Cheerio, series: Series) => Link[]; 35 | urls: Array<{series: Series; url: string}>; 36 | }; 37 | labels: Labels; 38 | name: string; 39 | setupAction?: (browser: Browser) => void; 40 | waitUntil?: LoadEvent; 41 | }; 42 | -------------------------------------------------------------------------------- /src/store/model/zotac.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Zotac: Store = { 4 | labels: { 5 | inStock: { 6 | container: '.add-to-cart-wrapper', 7 | text: ['add to cart'] 8 | } 9 | }, 10 | links: [ 11 | { 12 | brand: 'test:brand', 13 | model: 'test:model', 14 | series: 'test:series', 15 | url: 'https://store.zotac.com/zotac-gaming-geforce-rtx-2060-twin-fan-zt-t20600f-10m' 16 | }, 17 | { 18 | brand: 'zotac', 19 | model: 'trinity', 20 | series: '3080', 21 | url: 'https://store.zotac.com/zotac-gaming-geforce-rtx-3080-trinity-zt-a30800d-10p' 22 | }, 23 | { 24 | brand: 'zotac', 25 | model: 'trinity OC', 26 | series: '3080', 27 | url: 'https://store.zotac.com/zotac-gaming-geforce-rtx-3080-trinity-oc-zt-a30800j-10p' 28 | }, 29 | { 30 | brand: 'zotac', 31 | model: 'trinity', 32 | series: '3090', 33 | url: 'https://store.zotac.com/zotac-gaming-geforce-rtx-3090-trinity-zt-a30900d-10p' 34 | } 35 | ], 36 | name: 'zotac' 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /src/store/timestamp-url-parameter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates unique URL param to prevent cached responses (similar to jQuery that Nvidia uses) 3 | * 4 | * @return string in format &=1111111111111 (time since epoch in ms) 5 | */ 6 | export function timestampUrlParameter(): string { 7 | return `&_=${Date.now()}`; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/play-sound.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'play-sound'; 2 | -------------------------------------------------------------------------------- /src/types/puppeteer-extra-plugin-block-resources.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'puppeteer-extra-plugin-block-resources'; 2 | -------------------------------------------------------------------------------- /src/types/pushbullet.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pushbullet'; 2 | -------------------------------------------------------------------------------- /src/types/pushover-notifications.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pushover-notifications'; 2 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import {Browser, Page, Response} from 'puppeteer'; 2 | import {Config} from './config'; 3 | import {Logger} from './logger'; 4 | import {disableBlockerInPage} from './adblocker'; 5 | 6 | export function getSleepTime() { 7 | return Config.browser.minSleep + (Math.random() * (Config.browser.maxSleep - Config.browser.minSleep)); 8 | } 9 | 10 | export async function delay(ms: number) { 11 | return new Promise(resolve => { 12 | setTimeout(resolve, ms); 13 | }); 14 | } 15 | 16 | export async function usingResponse( 17 | browser: Browser, 18 | url: string, 19 | cb: (response: (Response | null), page: Page, browser: Browser) => Promise 20 | ): Promise { 21 | return usingPage(browser, async (page, browser) => { 22 | const response = await page.goto(url, {waitUntil: 'domcontentloaded'}); 23 | 24 | return cb(response, page, browser); 25 | }); 26 | } 27 | 28 | export async function usingPage(browser: Browser, cb: (page: Page, browser: Browser) => Promise): Promise { 29 | const page = await browser.newPage(); 30 | page.setDefaultNavigationTimeout(Config.page.navigationTimeout); 31 | await page.setUserAgent(Config.page.userAgent); 32 | 33 | try { 34 | return await cb(page, browser); 35 | } finally { 36 | try { 37 | await closePage(page); 38 | } catch (error) { 39 | Logger.error(error); 40 | } 41 | } 42 | } 43 | 44 | export async function closePage(page: Page) { 45 | if (!Config.browser.lowBandwidth) { 46 | await disableBlockerInPage(page); 47 | } 48 | 49 | await page.close(); 50 | } 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6", "dom"], 6 | "allowJs": true, 7 | "outDir": "build", 8 | "rootDir": "src", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "sourceMap": true 16 | } 17 | } 18 | --------------------------------------------------------------------------------