├── .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 [](https://github.com/jef/nvidia-snatcher/actions?query=workflow%3Aci) [](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 | 
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 |
--------------------------------------------------------------------------------