├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── Readme.md
├── bin
└── cli.js
├── examples
├── CLI
│ ├── BatchDownload.md
│ ├── DownloadHistory.md
│ ├── Examples.md
│ └── batchDownloadExample
└── Module
│ └── Examples.md
├── package-lock.json
├── package.json
├── src
├── constant
│ └── index.ts
├── core
│ ├── Downloader.ts
│ ├── InstaTouch.ts
│ └── index.ts
├── entry.ts
├── helpers
│ ├── Bar.ts
│ ├── Random.ts
│ └── index.ts
├── index.ts
└── types
│ ├── Cli.ts
│ ├── Downloader.ts
│ ├── Ig.ts
│ ├── InstaTouch.ts
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | },
6 | extends: ['airbnb-base', 'prettier'],
7 | globals: {
8 | Atomics: 'readonly',
9 | SharedArrayBuffer: 'readonly',
10 | },
11 | parser: '@typescript-eslint/parser',
12 | parserOptions: {
13 | ecmaVersion: 2018,
14 | sourceType: 'module',
15 | },
16 | plugins: ['@typescript-eslint', 'prettier'],
17 | rules: {
18 | 'prettier/prettier': ['error'],
19 | 'import/no-unresolved': 'off',
20 | 'import/extensions': 'off',
21 | 'no-bitwise': 'off',
22 | camelcase: 'off',
23 | 'import/prefer-default-export': 'off',
24 | },
25 | overrides: [
26 | {
27 | files: ['*.ts'],
28 | rules: {
29 | '@typescript-eslint/no-unused-vars': [2, { args: 'none' }],
30 | },
31 | },
32 | ],
33 | };
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | coverage/
4 | garbage/
5 |
6 | .DS_Store
7 | *.csv
8 | *.zip
9 | test.js
10 | test.ts
11 | proxy
12 | bulk
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | src/
4 | examples/
5 | .github/
6 | garbage/
7 |
8 | .dockerignore
9 | Dockerfile
10 | .DS_Store
11 | .eslintrc.js
12 | tsconfig.json
13 | .gitignore
14 | .prettierrc.js
15 | *.csv
16 | *.zip
17 | .eslintrc.js
18 | test.js
19 | test.ts
20 | proxy
21 | bulk
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | trailingComma: 'all',
4 | singleQuote: true,
5 | printWidth: 150,
6 | tabWidth: 4,
7 | };
8 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # InstaTouch
2 |
3 |   
4 |
5 | Scrape useful information from instagram.
6 |
7 | **No login or password are required.**
8 |
9 | This is not an official API support and etc. This is just a scraper that is using instagram graph api to scrape media.
10 |
11 | ---
12 |
13 |
14 |
15 | ---
16 |
17 | ## Content
18 | - [Demo](#demo)
19 | - [To Do](#to-do)
20 | - [Features](#features)
21 | - [Installation](#installation)
22 | - [Usage](#usage)
23 | - [In Terminal](#in-terminal)
24 | - [Terminal Examples](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/CLI/Examples.md)
25 | - [Manage Download History](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/CLI/DownloadHistory.md)
26 | - [Scrape and Download in Batch](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/CLI/BatchDownload.md)
27 | - [Output File Example](#output-file-example)
28 | - [Session ID ???](#get-session-id)
29 | - [Module](#docker)
30 | - [Methods](#methods)
31 | - [Options](#options)
32 | - [Use with Promises](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/Module/Examples.md)
33 | - [Output Example](#json-output-example)
34 | - [User, Hashtag, Location Feeds](#feed)
35 | - [comments](#comments)
36 | - [likers](#likers)
37 |
38 | ## Demo
39 |
40 | 
41 |
42 | ## Features
43 |
44 | - Scrape media posts from username, hashtag or location **`REQUIRES AN ACTIVE SESSION`**
45 | - Scrape comments from a specific instagram post
46 | - Scrape users who liked specific post **`REQUIRES AN ACTIVE SESSION`**
47 | - Scrape followers **`REQUIRES AN ACTIVE SESSION`**
48 | - Scrape following **`REQUIRES AN ACTIVE SESSION`**
49 | - Download and save media to a ZIP archive
50 | - Create JSON/CSV files with a post information
51 |
52 | ## To Do
53 | - [ ] Improve documentation
54 | - [ ] More examples
55 | - [ ] Web interface
56 |
57 | **Possible errors from instagram API**
58 |
59 | - Rate Limit - Instagram API temporarily blocked your IP, you can wait a little, try to use a proxy or set the higher {timeout}
60 |
61 | ## Installation
62 |
63 | instatouch requires [Node.js](https://nodejs.org/) v8.6.0+ to run.
64 |
65 | **Install from NPM**
66 |
67 | ```sh
68 | $ npm i -g instatouch
69 | ```
70 |
71 | **Install from YARN**
72 |
73 | ```sh
74 | $ yarn global add instatouch
75 | ```
76 |
77 | ## USAGE
78 |
79 | ### In Terminal
80 |
81 | ```sh
82 | $ instatouch --help
83 |
84 | Usage: cli [options]
85 |
86 | Commands:
87 | instatouch user [id] Scrape posts from username. Enter only username
88 | instatouch hashtag [id] Scrape posts from hashtag. Enter hashtag without
89 | #
90 | instatouch location [id] Scrape posts from a specific location. Enter
91 | location ID
92 | instatouch comments [id] Scrape comments from a post. Enter post url or
93 | post id
94 | instatouch likers [id] Scrape users who liked a post. Enter post url or
95 | post id
96 | instatouch history View previous download history
97 | instatouch from-file [file] [async] Scrape users, hashtags, music, videos mentioned
98 | in a file. 1 value per 1 line
99 |
100 | Options:
101 | --version Show version number [boolean]
102 | --count, -c Number of post to scrape [default: 40]
103 | --mediaType, -m Media type to scrape
104 | [choices: "image", "video", "all"] [default: "all"]
105 | --proxy, -p Set single proxy [default: ""]
106 | --proxy-file Use proxies from a file. Scraper will use random proxies
107 | from the file per each request. 1 line 1 proxy.
108 | [default: ""]
109 | --session Set session. For example: sessionid=BLBLBLBLLBL
110 | [default: ""]
111 | --timeout If you will receive error saying 'rate limit', you can
112 | try to set timeout. Timeout is in mls: 1000 mls = 1
113 | second [default: 0]
114 | --download, -d Download all scraped posts [boolean] [default: false]
115 | --zip, -z ZIP all downloaded posts [boolean] [default: false]
116 | --asyncDownload, -a How many posts should be downloaded at the same time.
117 | Try not to set more then 5 [default: 5]
118 | --filename, -f Set custom filename for the output files [default: ""]
119 | --filepath Directory to save all output files.
120 | [default: "/Users/karl.wint"]
121 | --filetype, -t Type of output file where post information will be
122 | saved. 'all' - save information about all posts to a
123 | 'json' and 'csv'. '' - do not save data in to files
124 | [choices: "csv", "json", "all", ""] [default: "csv"]
125 | --store, -s Scraper will save the progress in the OS TMP or Custom
126 | folder and in the future usage will only download new
127 | posts avoiding duplicates [boolean] [default: false]
128 | --historypath Set custom path where history file/files will be stored
129 | [default: "/var/folders/d5/fyh1_f2926q7c65g7skc0qh80000gn/T"]
130 | --remove, -r Delete the history record by entering "TYPE:INPUT" or
131 | "all" to clean all the history. For example: user:bob
132 | [default: ""]
133 | --help Show help [boolean]
134 | ```
135 | - [Terminal Examples](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/CLI/Examples.md)
136 | - [Manage Download History](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/CLI/DownloadHistory.md)
137 | - [Scrape and Download in Batch](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/CLI/BatchDownload.md)
138 |
139 | ### Output File Example
140 |
141 | 
142 | ## Get Session Id
143 | In order to access user,hashtag,location,likers,comments data you need an active session cookie value! This value can be taken from the instagram web(you need to be authorized in the web version)
144 |
145 | - Open inspector for example in Google Chrome browser then **right click on the web page -> inspector -> Network**
146 | - Refresh the page
147 | - In the "Network" section you will see the request, select it, scroll down to the "Request Headers" section and look for the "cookie:" section, there you will find this value "sessionid=BLAHLBAH"
148 | - Use it
149 |
150 | ## Module
151 |
152 | ### Methods
153 |
154 | ```javascript
155 | .user('tiktok', options) // User feed
156 | .hashtag('summer', options) // Hashtag feed
157 | .location('', options) // Location feed
158 | .comments('https://www.instagram.com/p/CATMghXnGrg/', options) // Post comments
159 | .likers('https://www.instagram.com/p/CATMghXnGrg/', options) // People who liked post
160 | .followers('instagram', options) // Get followers
161 | .following('instagram', options) // Get followings
162 | .getUserMeta('USERNAME', options) // Get user metadata
163 | .getPostMeta('https://www.instagram.com/p/CATMghXnGrg/', options) // Get post metadata
164 | ```
165 |
166 | ### Options
167 |
168 | ```javascript
169 | const options = {
170 | // Number of posts to scrape: {int default: 0}
171 | count: 0,
172 |
173 | // Download posts or not. {boolean default: false}
174 | download: false,
175 |
176 | // Archive downloaded posts. {boolean default: false}
177 | // If set to {false} then posts will be saved in the newly created folder
178 | zip: false
179 |
180 | // How many post should be downloaded asynchronously. Only if {download:true}: {int default: 5}
181 | asyncDownload: 5,
182 |
183 | // Media type to scrape: ["image", "video", "all"]: {string default: 'all'}
184 | mediaType: 'all',
185 |
186 | // Set proxy {string[] | string default: ''}
187 | // http proxy: 127.0.0.1:8080
188 | // socks proxy: socks5://127.0.0.1:8080
189 | // You can pass proxies as an array and scraper will randomly select a proxy from the array to execute the requests
190 | proxy: '',
191 |
192 | // File name that will be used to save data to: {string default: '[id]'}
193 | filename: '[id]',
194 |
195 | // File path where all files will be saved: {string default: 'USER_HOME_DIR'}
196 | filepath: `USER_HOME_DIR`,
197 |
198 | // Output with information can be saved to a CSV or JSON files: {string default: 'na'}
199 | // 'csv' to save in csv
200 | // 'json' to save in json
201 | // 'all' to save in json and csv
202 | // 'na' to skip this step
203 | filetype: `na`,
204 |
205 | // Set initial cursor value to start pagination over the feed from the specific point: {string default: ''}
206 | endCursor: ''
207 |
208 | // Timeout between requests. If error 'rate limit' received then this option can be useful: {int default: 0}
209 | timeout: 0,
210 |
211 | // Some endpoints do require a valid session cookie value
212 | // This value can be taken from the instagram web(you need to be authorized in the web version)
213 | // Open inspector(google chrome -> right click on the web page -> inspector->Network)
214 | // refresh page and in the "Network" section you will see the request, select it
215 | // scroll down to the "Request Headers" section and look for "cookie:" section
216 | // and there you will find this value "sessionid=BLAHLBAH"
217 | session: "sessionid=BLAHLBAH"
218 | };
219 | ```
220 |
221 | - [Promise Examples](https://github.com/drawrowfly/instagram-scraper/tree/master/examples/Module/Examples.md)
222 |
223 | **Result will contain a bunch of data**
224 |
225 | ```javascript
226 | instaTouch {
227 | collector:[ARRAY_OF_DATA]
228 | //Files are below
229 | zip: '/{CURRENT_PATH}/natgeo_1552963581094.zip',
230 | json: '/{CURRENT_PATH}/natgeo_1552963581094.json',
231 | csv: '/{CURRENT_PATH}/natgeo_1552963581094.csv'
232 | }
233 | ```
234 |
235 | ### Json Output Example
236 |
237 | ##### Feed
238 | Example output for the methods: **user, hashtag, location**
239 |
240 | ```javascript
241 | {
242 | id: '2311886241697642614',
243 | shortcode: 'CAVeEm1gDh2',
244 | type: 'GraphSidecar',
245 | is_video: false,
246 | dimension: { height: 1080, width: 1080 },
247 | display_url:
248 | 'https://scontent-hel2-1.cdninstagram.com/v/t51.2885-15/e35/97212979_112497166934732_8766432510789477700_n.jpg?_nc_ht=scontent-hel2-1.cdninstagram.com&_nc_cat=1&_nc_ohc=4jd1cuOMYrkAX_y6CK2&oh=2aa0b339cdf653dac916a64a70c81e31&oe=5EEB5E07',
249 | thumbnail_src:
250 | 'https://scontent-hel2-1.cdninstagram.com/v/t51.2885-15/sh0.08/e35/s640x640/97212979_112497166934732_8766432510789477700_n.jpg?_nc_ht=scontent-hel2-1.cdninstagram.com&_nc_cat=1&_nc_ohc=4jd1cuOMYrkAX_y6CK2&oh=af5440bdf071108b7e74b1524c358e66&oe=5EED8CE4',
251 | owner: { id: '25025320', username: 'instagram' },
252 | description:
253 | 'FernandoMagalhães’(@mglhs_com)ever-evolvingfuturisticbeingsliveintheGenesisHumanProject.Alpha,acomputer-generatedworldhedreamedupattheendof2018.TheLondon-basedBrazilianartistusesproceduralmodeling,aprogrammingtechniquethatcreates3Dmodelsandtextures.\n\n“I’monlyabletoseethem[hischaracters]oncetherenderisdone—soit’skindoflikemeetingsomebodyforthefirsttime,”explainsFernando.“Ilovetoseethemandtrytounderstand,tofeelfromwheretheycamefrom,whotheyare,whattheydoandsoon.”\n\n“TodayI’mworkinginthisuniversethatIcreated,butmymindgoesmuchfurtherthanthat.Throughmywork,Ihopepeopleunderstandthatartgoesbeyondwhatweknowasart,there’sdifferentpaths,approachesandpossibilities.”#ThisWeekOnInstagram\n\nDigitalimagesby@mglhs_com',
254 | comments: 5050,
255 | likes: 412657,
256 | comments_disabled: false,
257 | taken_at_timestamp: 1589818338,
258 | location: { id: '213385402', has_public_page: true, name: 'London,UnitedKingdom', slug: 'london-united-kingdom' },
259 | hashtags: ['#ThisWeekOnInstagram'],
260 | mentions: ['@mglhs_com', '@mglhs_com'],
261 | };
262 | ```
263 |
264 | ##### Comments
265 | Example output for the methods: **comments**
266 |
267 | ```javascript
268 | {
269 | id: '17854856327003928',
270 | text: 'Böyle şeytani figürleri yayınlamak mi zorundasınız. Euzu billahi mineşşeytanirracim Bismillahirrahmanirrahim.',
271 | created_at: 1589837238,
272 | did_report_as_spam: false,
273 | owner: {
274 | id: '13492154487',
275 | is_verified: false,
276 | profile_pic_url:
277 | 'https://scontent-hel2-1.cdninstagram.com/v/t51.2885-19/s150x150/89832595_142698410416916_7218363900150939648_n.jpg?_nc_ht=scontent-hel2-1.cdninstagram.com&_nc_ohc=dIhkVzLiHVUAX-o8Vx6&oh=d516c43b444dc3409ac3f0cca145f9ca&oe=5EEBA851',
278 | username: 'hasan_dede4809',
279 | },
280 | likes: 0,
281 | comments: 0,
282 | };
283 | ```
284 |
285 | ##### Likers
286 | Example output for the methods: **likers**
287 |
288 | ```javascript
289 | {
290 | id: '27165506664',
291 | username: 'josedhl_priv',
292 | full_name: 'José David',
293 | profile_pic_url:
294 | 'https://scontent-hel2-1.cdninstagram.com/v/t51.2885-19/s150x150/80568189_848308822340996_1519415041114243072_n.jpg?_nc_ht=scontent-hel2-1.cdninstagram.com&_nc_ohc=Kgmrwidffj0AX99RC-n&oh=a4e999c7ec74630c9a4a468272fc22c8&oe=5EEE2A91',
295 | is_private: true,
296 | is_verified: false,
297 | };
298 | ```
299 |
300 | ## License
301 |
302 | **MIT**
303 |
304 | **Free Software**
305 |
--------------------------------------------------------------------------------
/bin/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /* eslint-disable no-console */
3 | /* eslint-disable no-unused-expressions */
4 | /* eslint-disable prefer-destructuring */
5 | /* eslint-disable no-param-reassign */
6 |
7 | const yargs = require('yargs');
8 | const { tmpdir } = require('os');
9 | const IGScraper = require('../build');
10 | const CONST = require('../build/constant');
11 |
12 | const startScraper = async (argv) => {
13 | try {
14 | argv.scrapeType = argv._[0];
15 | argv.input = argv.id;
16 | argv.cli = true;
17 | argv.store_history = argv.store;
18 | if (argv.filename) {
19 | argv.fileName = argv.filename;
20 | }
21 |
22 | if (argv.historypath) {
23 | argv.historyPath = argv.historypath;
24 | }
25 | if (argv.file) {
26 | argv.input = argv.file;
27 | }
28 | if (argv.scrapeType.indexOf('-') > -1) {
29 | argv.scrapeType = argv.scrapeType.replace('-', '');
30 | }
31 |
32 | if (argv.async) {
33 | argv.asyncBulk = argv.async;
34 | }
35 | argv.bulk = false;
36 | const scraper = await IGScraper[argv.scrapeType](argv.input, argv);
37 |
38 | if (scraper.zip) {
39 | console.log(`ZIP path: ${scraper.zip}`);
40 | }
41 | if (scraper.json) {
42 | console.log(`JSON path: ${scraper.json}`);
43 | }
44 | if (scraper.csv) {
45 | console.log(`CSV path: ${scraper.csv}`);
46 | }
47 | if (scraper.message) {
48 | console.log(scraper.message);
49 | }
50 | if (scraper.table) {
51 | console.table(scraper.table);
52 | }
53 | } catch (error) {
54 | console.log(error);
55 | }
56 | };
57 |
58 | yargs
59 | .usage('Usage: $0 [options]')
60 | .command('user [id]', 'Scrape posts from username. Enter only username', {}, (argv) => {
61 | startScraper(argv);
62 | })
63 | .command('hashtag [id]', 'Scrape posts from hashtag. Enter hashtag without #', {}, (argv) => {
64 | startScraper(argv);
65 | })
66 | .command('location [id]', 'Scrape posts from a specific location. Enter location ID', {}, (argv) => {
67 | startScraper(argv);
68 | })
69 | .command('comments [id]', 'Scrape comments from a post. Enter post url or post id', {}, (argv) => {
70 | startScraper(argv);
71 | })
72 | .command('likers [id]', 'Scrape users who liked a post. Enter post url or post id', {}, (argv) => {
73 | startScraper(argv);
74 | })
75 | .command('history', 'View previous download history', {}, (argv) => {
76 | startScraper(argv);
77 | })
78 | .command('from-file [file] [async]', 'Scrape users, hashtags, music, videos mentioned in a file. 1 value per 1 line', {}, (argv) => {
79 | startScraper(argv);
80 | })
81 | .options({
82 | help: {
83 | alias: 'h',
84 | describe: 'help',
85 | },
86 | count: {
87 | alias: 'c',
88 | default: 40,
89 | describe: 'Number of post to scrape',
90 | },
91 | mediaType: {
92 | alias: 'm',
93 | default: 'all',
94 | choices: ['image', 'video', 'all'],
95 | describe: 'Media type to scrape',
96 | },
97 | proxy: {
98 | alias: 'p',
99 | default: '',
100 | describe: 'Set single proxy',
101 | },
102 | 'proxy-file': {
103 | default: '',
104 | describe: 'Use proxies from a file. Scraper will use random proxies from the file per each request. 1 line 1 proxy.',
105 | },
106 | session: {
107 | default: '',
108 | describe: 'Set session. For example: sessionid=BLBLBLBLLBL',
109 | },
110 | timeout: {
111 | default: 0,
112 | describe: "If you will receive error saying 'rate limit', you can try to set timeout. Timeout is in mls: 1000 mls = 1 second",
113 | },
114 | download: {
115 | alias: 'd',
116 | boolean: true,
117 | default: false,
118 | describe: 'Download all scraped posts',
119 | },
120 | zip: {
121 | alias: 'z',
122 | boolean: true,
123 | default: false,
124 | describe: 'ZIP all downloaded posts',
125 | },
126 | asyncDownload: {
127 | alias: 'a',
128 | default: 5,
129 | describe: 'How many posts should be downloaded at the same time. Try not to set more then 5 ',
130 | },
131 | filename: {
132 | alias: ['f'],
133 | default: '',
134 | describe: 'Set custom filename for the output files',
135 | },
136 | filepath: {
137 | default: process.env.SCRAPING_FROM_DOCKER ? '' : process.cwd(),
138 | describe: 'Directory to save all output files.',
139 | },
140 | filetype: {
141 | alias: ['t'],
142 | default: 'csv',
143 | choices: ['csv', 'json', 'all', ''],
144 | describe:
145 | "Type of output file where post information will be saved. 'all' - save information about all posts to a 'json' and 'csv'. '' - do not save data in to files ",
146 | },
147 | store: {
148 | alias: ['s'],
149 | boolean: true,
150 | default: false,
151 | describe:
152 | 'Scraper will save the progress in the OS TMP or Custom folder and in the future usage will only download new posts avoiding duplicates',
153 | },
154 | historypath: {
155 | default: process.env.SCRAPING_FROM_DOCKER ? '' : tmpdir(),
156 | describe: 'Set custom path where history file/files will be stored',
157 | },
158 | remove: {
159 | alias: ['r'],
160 | default: '',
161 | describe: 'Delete the history record by entering "TYPE:INPUT" or "all" to clean all the history. For example: user:bob',
162 | },
163 | })
164 | .check((argv) => {
165 | if (CONST.scrapeType.indexOf(argv._[0]) === -1) {
166 | throw new Error('Wrong command');
167 | }
168 |
169 | if (!argv.download) {
170 | if (argv.cli && !argv.zip && !argv.type) {
171 | throw new Error(`Pointless commands. Try again but with the correct set of commands`);
172 | }
173 | }
174 |
175 | if (argv._[0] === 'from-file') {
176 | const async = parseInt(argv.async, 10);
177 | if (!async) {
178 | throw new Error('You need to set number of task that should be executed at the same time');
179 | }
180 | if (!argv.t && !argv.d) {
181 | throw new Error('You need to specify file type(-t) where data will be saved AND/OR if posts should be downloaded (-d)');
182 | }
183 | }
184 |
185 | if (process.env.SCRAPING_FROM_DOCKER && (argv.historypath || argv.filepath)) {
186 | throw new Error(`Can't set custom path when running from Docker`);
187 | }
188 | if (argv.remove) {
189 | if (argv.remove.indexOf(':') === -1) {
190 | argv.remove = `${argv.remove}:`;
191 | }
192 | const split = argv.remove.split(':');
193 | const type = split[0];
194 | const input = split[1];
195 |
196 | if (type !== 'all' && CONST.history.indexOf(type) === -1) {
197 | throw new Error(`--remove, -r list of allowed types: ${CONST.history}`);
198 | }
199 | if (!input && type !== 'trend' && type !== 'all') {
200 | throw new Error('--remove, -r to remove the specific history record you need to enter "TYPE:INPUT". For example: user:bob');
201 | }
202 | }
203 |
204 | return true;
205 | })
206 | .demandCommand()
207 | .help().argv;
208 |
--------------------------------------------------------------------------------
/examples/CLI/BatchDownload.md:
--------------------------------------------------------------------------------
1 | [Go back to the Main Documenation](https://github.com/drawrowfly/instagram-scraper)
2 |
3 | ## Batch Scrape And Download Example
4 |
5 | **This function works really good with fast internet and good proxies**
6 |
7 | In order to download in batch you need to create a file(any name) with one value per line. Lines that are starting with ## are considered as comments and will be ignored.
8 |
9 | To scrape data from the user feed, enter **username**.
10 |
11 | To scrape from the user feed by user id, enter **id:USER_ID**.
12 |
13 | To scrape from the hashtag feed, enter hashtag starting with **#**.
14 |
15 | To scrape from the music feed, enter **music:MUSIC_ID**.
16 |
17 | To download video, enter plain video url.
18 |
19 | **File Example:**
20 |
21 | ```
22 | ## User <---- this is just a comment that will be ignored by the scraper
23 | instagram
24 | tiktok
25 | https://www.instagram.com/facebook/
26 |
27 | ## Hashtag
28 | #love
29 | #summer
30 | #story
31 |
32 | ## Location
33 | location|213385402
34 | location|259121424
35 |
36 | ## People who liked post
37 | likers|https://www.instagram.com/p/CAVeEm1gDh2/
38 | likers|https://www.instagram.com/p/CAS6In0AU_K/
39 |
40 | ## Comments
41 | comments|https://www.instagram.com/p/CAOL3ySAOyy/
42 | comments|https://www.instagram.com/p/CAS6In0AU_K/
43 | ```
44 |
45 | **Example 1 without proxy**
46 |
47 | **FILE** - name of the file
48 |
49 | **ASYNC_TASKS** - how many tasks(each line in the file is a task) should be started at the time. I have tested with 40 and it worked really well, if you have slow connection then do not set more then 5
50 |
51 | Command below will download single videos without the watermark and will make attempt to download all the videos from the users, hashtags and music feeds specified in the example file above and will save user, hashtag, music metadata to the JSON and CSV files.
52 |
53 | ```sh
54 | instatouch from-file FILE ASYNC_TASKS -d -f all
55 | ```
56 |
57 | **Example 2 with the proxy**
58 |
59 | It is always better to use proxies for any task especially when downloading in batches. You can set 1 proxy or you can point scraper to the file with the proxy list and scraper will use random proxy from that list per request.
60 |
61 | **FILE** - name of the file
62 |
63 | **ASYNC_TASKS** - how many tasks(each line in the file is a task) should be started at the time.
64 |
65 | **PROXY** - proxy file
66 |
67 | **Proxy File Example**
68 |
69 | ```
70 | user:password@127.0.0.1:8080
71 | user:password@127.0.0.1:8081
72 | 127.0.0.1:8082
73 | socks5://127.0.0.1:8083
74 | socks4://127.0.0.1:8084
75 | ```
76 |
77 | Command below will download single videos without the watermark and will make attempt to download 50 videos from each user, hashtag and music feed specified in the example file above.
78 |
79 | ```sh
80 | instatouch from-file FILE ASYNC_TASKS --proxy-file PROXY -d -n 50
81 | ```
82 |
83 | ### Batch Scraping Output
84 |
85 | 
86 |
--------------------------------------------------------------------------------
/examples/CLI/DownloadHistory.md:
--------------------------------------------------------------------------------
1 | [Go back to the Main Documenation](https://github.com/drawrowfly/instagram-scraper)
2 |
3 | ## Manage Download History
4 |
5 | 
6 |
7 | You can only view the history from the CLI and only if you have used **-s** flag in your previous scraper executions.
8 |
9 | **-s** save download history to avoid downloading duplicate posts in the future
10 |
11 | To view history record:
12 |
13 | ```sh
14 | instatouch history
15 | ```
16 |
17 | To delete single history record:
18 |
19 | ```sh
20 | instatouch history -r TYPE:INPUT
21 | instatouch history -r user:tiktok
22 | instatouch history -r hashtag:summer
23 | instatouch history -r location:434343
24 | instatouch history -r likers:https://www.instagram.com/p/CAS6In0AU_K/
25 | instatouch history -r comments:https://www.instagram.com/p/CAS6In0AU_K/
26 | ```
27 |
28 | Set custom path where history files will be stored.
29 |
30 | **NOTE: After setting the custom path you will have to specify it all the time so that the scraper knows the file location**
31 |
32 | ```
33 | instatouch hashtag summer -s -d -n 10 --historypath /Blah/Blah/Blah
34 | ```
35 |
36 | To delete all records:
37 |
38 | ```sh
39 | instatouch history -r all
40 | ```
41 |
--------------------------------------------------------------------------------
/examples/CLI/Examples.md:
--------------------------------------------------------------------------------
1 | [Go back to the Main Documenation](https://github.com/drawrowfly/instagram-scraper)
2 |
3 | ## Terminal Examples
4 |
5 | **Example 1:**
6 | Scrape 50 **(-c 50)** video **(-m video)** posts from hashtag **summer**. Save post info in to a CSV file **(-t csv)**
7 |
8 | ```sh
9 | $ instatouch hashtag summer -c 50 -m video -t csv
10 |
11 | Output:
12 | JSON path: /{CURRENT_PATH}/summer_1552945544582.csv
13 | ```
14 |
15 | **Example 2:**
16 | Scrape 100 **(-c 100)** posts from user natgeo, download **(-d)** and save them to a ZIP **(--zip)** archive. Save post info in to a JSON and CSV files **(-t all)**
17 |
18 | ```
19 | $ instatouch user natgeo -c 100 -d --zip -t all
20 |
21 | Output:
22 | ZIP path: /{CURRENT_PATH}/natgeo_1552945659138.zip
23 | JSON path: /{CURRENT_PATH}/natgeo_1552945659138.json
24 | CSV path: /{CURRENT_PATH}/natgeo_1552945659138.csv
25 | ```
26 |
27 | **Example 3:**
28 | Scrape 50 **(-c 50)** posts from user natgeo, download **(-d)** and save them to a ZIP **(--zip)** archive. Save post info in to a JSON and CSV files **(-t all)**. Save all files to a custom path **(--filepath /custom/path/to/save/files)**
29 |
30 | ```
31 | $ instatouch user natgeo -c 50 -d --zip -t all --filepath /custom/path/to/save/files
32 |
33 | Output:
34 | ZIP path: /custom/path/to/save/files/natgeo_1552945659138.zip
35 | JSON path: /custom/path/to/save/files/natgeo_1552945659138.json
36 | CSV path: /custom/path/to/save/files/natgeo_1552945659138.csv
37 | ```
38 |
39 | **Example 4:**
40 | Scrape 200 **(-c 200)** comments from this post https://www.instagram.com/p/B3XPst_A98M/. Save comment data in to a CSV file **(-t csv)**
41 |
42 | ```
43 | $ instatouch comments https://www.instagram.com/p/B3XPst_A98M/ -c 200 -t csv
44 |
45 | Output:
46 | CSV path: /{CURRENT_PATH}/B3XPst_A98M_1552945659138.csv
47 | ```
48 |
49 | **Example 5:**
50 | Scrape 200 **(-c 200)** users who liked this post https://www.instagram.com/p/B3XPst_A98M/. Save comment data in to a CSV file **(-t csv)**
51 |
52 | ```
53 | $ instatouch likers https://www.instagram.com/p/B3XPst_A98M/ -c 200 -t csv
54 |
55 | Output:
56 | CSV path: /{CURRENT_PATH}/B3XPst_A98M_1552945659138.csv
57 | ```
58 |
59 | **Example 6:**
60 | Download **(-d)** 20 **(-c 20)** newest post from the user {USERNAME} and save the progress to avoid downloading the same posts in the future **(-s)**
61 |
62 | - When executing same command next time scraper will only download new posts that weren't downloaded before
63 |
64 | ```sh
65 | instatouch user USERNAME -c 20 -d -s
66 |
67 |
68 | Output:
69 | Folder Path: /User/Bob/Downloads/USERNAME
70 | ```
71 |
72 | **To make it look better, when downloading posts the progress will be shown in terminal**
73 |
74 | ```
75 | Downloading VIDEO B3PmkisgjSx [==============================] 100%
76 | Downloading PHOTO B3Pmme3ASuY [==============================] 100%
77 | Downloading PHOTO B3PmmLHjE4s [==============================] 100%
78 | Downloading VIDEO B3PmiL0HxG3 [==============================] 100%
79 | Downloading PHOTO B3PmmJFAWVI [==============================] 100%
80 | Downloading PHOTO B3Pml8PFg3i [==============================] 100%
81 | Downloading PHOTO B3Pml-hJyvc [==============================] 100%
82 | Downloading PHOTO B3Pml2lnS0B [==============================] 100%
83 | Downloading PHOTO B3PmltPiTDi [==============================] 100%
84 | Downloading PHOTO B3Pml05osiU [==============================] 100%
85 | Downloading PHOTO B3Pmlmficxo [==============================] 100%
86 | ```
87 |
--------------------------------------------------------------------------------
/examples/CLI/batchDownloadExample:
--------------------------------------------------------------------------------
1 | ## User
2 | instagram
3 | tiktok
4 | https://www.instagram.com/facebook/
5 |
6 | ## Hashtag
7 | #love
8 | #summer
9 | #story
10 |
11 | ## Location
12 | location|213385402
13 | location|259121424
14 |
15 | ## People who liked post
16 | likers|https://www.instagram.com/p/CAVeEm1gDh2/
17 | likers|https://www.instagram.com/p/CAS6In0AU_K/
18 |
19 | ## Comments
20 | comments|https://www.instagram.com/p/CAOL3ySAOyy/
21 | comments|https://www.instagram.com/p/CAS6In0AU_K/
--------------------------------------------------------------------------------
/examples/Module/Examples.md:
--------------------------------------------------------------------------------
1 | [Go back to the Main Documenation](https://github.com/drawrowfly/instagram-scraper)
2 |
3 | ## Promise
4 |
5 | ```javascript
6 | const instaTouch = require('instatouch');
7 |
8 | // Scrape 100 image posts from the user feed
9 | (async () => {
10 | try {
11 | const options = { count: 100, mediaType: 'image' };
12 | const user = await instaTouch.user('natgeo', options);
13 | console.log(user);
14 | } catch (error) {
15 | console.log(error);
16 | }
17 | })();
18 |
19 | // Scrape 100 video posts from the hashtag feed
20 | (async () => {
21 | try {
22 | const options = { count: 100, mediaType: 'video' };
23 | const hashtag = await instaTouch.hashtag('summer', options);
24 | console.log(const);
25 | } catch (error) {
26 | console.log(error);
27 | }
28 | })();
29 |
30 | // Scrape 100 video and iage posts from the location feed
31 | // For example from this location https://www.instagram.com/explore/locations/213359469/munich-germany/
32 | // In this example location id will be 213359469
33 | (async () => {
34 | try {
35 | const options = { count: 100, mediaType: 'all' };
36 | const location = await instaTouch.location('213359469', options);
37 | console.log(location);
38 | } catch (error) {
39 | console.log(error);
40 | }
41 | })();
42 |
43 | // Scrape comments from a post
44 | // For example from this post https://www.instagram.com/p/B7wOyffArc5/
45 | // In this example post id will be B7wOyffArc5 or you can set full URL
46 | (async () => {
47 | try {
48 | const options = { count: 100};
49 | const comments = await instaTouch.comments('B7wOyffArc5', options);
50 | console.log(comments);
51 | } catch (error) {
52 | console.log(error);
53 | }
54 | })();
55 |
56 | // Scrape users who liked a post
57 | // For example from this post https://www.instagram.com/p/B7wOyffArc5/
58 | // In this example post id will be B7wOyffArc5 or you can set full URL
59 | (async () => {
60 | try {
61 | const options = { count: 200 };
62 | const likers = await instaTouch.likers('B7wOyffArc5', options);
63 | console.log(likers);
64 | } catch (error) {
65 | console.log(error);
66 | }
67 | })();
68 |
69 | // Scrape 2000 users who liked a post and set custom proxy list to avoid rate limit error
70 | // For example from this post https://www.instagram.com/p/B7wOyffArc5/
71 | // In this example post id will be B7wOyffArc5 or you can set full URL
72 | (async () => {
73 | try {
74 | const proxy = [
75 | 'username:password@127.0.0.1:1000',
76 | 'username:password@127.0.0.1:1002',
77 | 'username:password@127.0.0.1:1003',
78 | 'username:password@127.0.0.1:1004',
79 | 'username:password@127.0.0.1:1005',
80 | ]
81 | const options = { count: 200, proxy };
82 | const likers = await instaTouch.likers('B7wOyffArc5', options);
83 | console.log(likers);
84 | } catch (error) {
85 | console.log(error);
86 | }
87 | })();
88 | ```
89 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "instatouch",
3 | "version": "2.3.20",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "ajv": {
8 | "version": "6.10.2",
9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
10 | "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
11 | "requires": {
12 | "fast-deep-equal": "^2.0.1",
13 | "fast-json-stable-stringify": "^2.0.0",
14 | "json-schema-traverse": "^0.4.1",
15 | "uri-js": "^4.2.2"
16 | }
17 | },
18 | "ansi-regex": {
19 | "version": "4.1.0",
20 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
21 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
22 | },
23 | "ansi-styles": {
24 | "version": "3.2.1",
25 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
26 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
27 | "requires": {
28 | "color-convert": "^1.9.0"
29 | }
30 | },
31 | "archiver": {
32 | "version": "3.1.1",
33 | "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz",
34 | "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==",
35 | "requires": {
36 | "archiver-utils": "^2.1.0",
37 | "async": "^2.6.3",
38 | "buffer-crc32": "^0.2.1",
39 | "glob": "^7.1.4",
40 | "readable-stream": "^3.4.0",
41 | "tar-stream": "^2.1.0",
42 | "zip-stream": "^2.1.2"
43 | },
44 | "dependencies": {
45 | "async": {
46 | "version": "2.6.3",
47 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
48 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
49 | "requires": {
50 | "lodash": "^4.17.14"
51 | }
52 | }
53 | }
54 | },
55 | "archiver-utils": {
56 | "version": "2.1.0",
57 | "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
58 | "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
59 | "requires": {
60 | "glob": "^7.1.4",
61 | "graceful-fs": "^4.2.0",
62 | "lazystream": "^1.0.0",
63 | "lodash.defaults": "^4.2.0",
64 | "lodash.difference": "^4.5.0",
65 | "lodash.flatten": "^4.4.0",
66 | "lodash.isplainobject": "^4.0.6",
67 | "lodash.union": "^4.6.0",
68 | "normalize-path": "^3.0.0",
69 | "readable-stream": "^2.0.0"
70 | },
71 | "dependencies": {
72 | "readable-stream": {
73 | "version": "2.3.6",
74 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
75 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
76 | "requires": {
77 | "core-util-is": "~1.0.0",
78 | "inherits": "~2.0.3",
79 | "isarray": "~1.0.0",
80 | "process-nextick-args": "~2.0.0",
81 | "safe-buffer": "~5.1.1",
82 | "string_decoder": "~1.1.1",
83 | "util-deprecate": "~1.0.1"
84 | }
85 | }
86 | }
87 | },
88 | "asn1": {
89 | "version": "0.2.4",
90 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
91 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
92 | "requires": {
93 | "safer-buffer": "~2.1.0"
94 | }
95 | },
96 | "assert-plus": {
97 | "version": "1.0.0",
98 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
99 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
100 | },
101 | "async": {
102 | "version": "3.1.0",
103 | "resolved": "https://registry.npmjs.org/async/-/async-3.1.0.tgz",
104 | "integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ=="
105 | },
106 | "asynckit": {
107 | "version": "0.4.0",
108 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
109 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
110 | },
111 | "aws-sign2": {
112 | "version": "0.7.0",
113 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
114 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
115 | },
116 | "aws4": {
117 | "version": "1.8.0",
118 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
119 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
120 | },
121 | "balanced-match": {
122 | "version": "1.0.0",
123 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
124 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
125 | },
126 | "base64-js": {
127 | "version": "1.3.1",
128 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
129 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
130 | },
131 | "bcrypt-pbkdf": {
132 | "version": "1.0.2",
133 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
134 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
135 | "requires": {
136 | "tweetnacl": "^0.14.3"
137 | }
138 | },
139 | "bl": {
140 | "version": "3.0.0",
141 | "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.0.tgz",
142 | "integrity": "sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==",
143 | "requires": {
144 | "readable-stream": "^3.0.1"
145 | }
146 | },
147 | "bluebird": {
148 | "version": "3.7.1",
149 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz",
150 | "integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg=="
151 | },
152 | "brace-expansion": {
153 | "version": "1.1.11",
154 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
155 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
156 | "requires": {
157 | "balanced-match": "^1.0.0",
158 | "concat-map": "0.0.1"
159 | }
160 | },
161 | "buffer": {
162 | "version": "5.4.3",
163 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",
164 | "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",
165 | "requires": {
166 | "base64-js": "^1.0.2",
167 | "ieee754": "^1.1.4"
168 | }
169 | },
170 | "buffer-crc32": {
171 | "version": "0.2.13",
172 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
173 | "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
174 | },
175 | "camelcase": {
176 | "version": "5.3.1",
177 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
178 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
179 | },
180 | "caseless": {
181 | "version": "0.12.0",
182 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
183 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
184 | },
185 | "chalk": {
186 | "version": "2.4.2",
187 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
188 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
189 | "requires": {
190 | "ansi-styles": "^3.2.1",
191 | "escape-string-regexp": "^1.0.5",
192 | "supports-color": "^5.3.0"
193 | }
194 | },
195 | "cli-cursor": {
196 | "version": "3.1.0",
197 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
198 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
199 | "requires": {
200 | "restore-cursor": "^3.1.0"
201 | }
202 | },
203 | "cli-spinners": {
204 | "version": "2.2.0",
205 | "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz",
206 | "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ=="
207 | },
208 | "cliui": {
209 | "version": "5.0.0",
210 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
211 | "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
212 | "requires": {
213 | "string-width": "^3.1.0",
214 | "strip-ansi": "^5.2.0",
215 | "wrap-ansi": "^5.1.0"
216 | }
217 | },
218 | "clone": {
219 | "version": "1.0.4",
220 | "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
221 | "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
222 | },
223 | "color-convert": {
224 | "version": "1.9.3",
225 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
226 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
227 | "requires": {
228 | "color-name": "1.1.3"
229 | }
230 | },
231 | "color-name": {
232 | "version": "1.1.3",
233 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
234 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
235 | },
236 | "combined-stream": {
237 | "version": "1.0.8",
238 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
239 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
240 | "requires": {
241 | "delayed-stream": "~1.0.0"
242 | }
243 | },
244 | "commander": {
245 | "version": "2.20.1",
246 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz",
247 | "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg=="
248 | },
249 | "compress-commons": {
250 | "version": "2.1.1",
251 | "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
252 | "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==",
253 | "requires": {
254 | "buffer-crc32": "^0.2.13",
255 | "crc32-stream": "^3.0.1",
256 | "normalize-path": "^3.0.0",
257 | "readable-stream": "^2.3.6"
258 | },
259 | "dependencies": {
260 | "readable-stream": {
261 | "version": "2.3.6",
262 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
263 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
264 | "requires": {
265 | "core-util-is": "~1.0.0",
266 | "inherits": "~2.0.3",
267 | "isarray": "~1.0.0",
268 | "process-nextick-args": "~2.0.0",
269 | "safe-buffer": "~5.1.1",
270 | "string_decoder": "~1.1.1",
271 | "util-deprecate": "~1.0.1"
272 | }
273 | }
274 | }
275 | },
276 | "concat-map": {
277 | "version": "0.0.1",
278 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
279 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
280 | },
281 | "core-util-is": {
282 | "version": "1.0.2",
283 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
284 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
285 | },
286 | "crc": {
287 | "version": "3.8.0",
288 | "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
289 | "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
290 | "requires": {
291 | "buffer": "^5.1.0"
292 | }
293 | },
294 | "crc32-stream": {
295 | "version": "3.0.1",
296 | "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz",
297 | "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==",
298 | "requires": {
299 | "crc": "^3.4.4",
300 | "readable-stream": "^3.4.0"
301 | }
302 | },
303 | "dashdash": {
304 | "version": "1.14.1",
305 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
306 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
307 | "requires": {
308 | "assert-plus": "^1.0.0"
309 | }
310 | },
311 | "decamelize": {
312 | "version": "1.2.0",
313 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
314 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
315 | },
316 | "defaults": {
317 | "version": "1.0.3",
318 | "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
319 | "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
320 | "requires": {
321 | "clone": "^1.0.2"
322 | }
323 | },
324 | "delayed-stream": {
325 | "version": "1.0.0",
326 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
327 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
328 | },
329 | "ecc-jsbn": {
330 | "version": "0.1.2",
331 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
332 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
333 | "requires": {
334 | "jsbn": "~0.1.0",
335 | "safer-buffer": "^2.1.0"
336 | }
337 | },
338 | "emoji-regex": {
339 | "version": "7.0.3",
340 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
341 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
342 | },
343 | "end-of-stream": {
344 | "version": "1.4.4",
345 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
346 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
347 | "requires": {
348 | "once": "^1.4.0"
349 | }
350 | },
351 | "escape-string-regexp": {
352 | "version": "1.0.5",
353 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
354 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
355 | },
356 | "extend": {
357 | "version": "3.0.2",
358 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
359 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
360 | },
361 | "extsprintf": {
362 | "version": "1.3.0",
363 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
364 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
365 | },
366 | "fast-deep-equal": {
367 | "version": "2.0.1",
368 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
369 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
370 | },
371 | "fast-json-stable-stringify": {
372 | "version": "2.0.0",
373 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
374 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
375 | },
376 | "find-up": {
377 | "version": "3.0.0",
378 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
379 | "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
380 | "requires": {
381 | "locate-path": "^3.0.0"
382 | }
383 | },
384 | "forever-agent": {
385 | "version": "0.6.1",
386 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
387 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
388 | },
389 | "form-data": {
390 | "version": "2.3.3",
391 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
392 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
393 | "requires": {
394 | "asynckit": "^0.4.0",
395 | "combined-stream": "^1.0.6",
396 | "mime-types": "^2.1.12"
397 | }
398 | },
399 | "fs-constants": {
400 | "version": "1.0.0",
401 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
402 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
403 | },
404 | "fs.realpath": {
405 | "version": "1.0.0",
406 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
407 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
408 | },
409 | "get-caller-file": {
410 | "version": "2.0.5",
411 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
412 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
413 | },
414 | "getpass": {
415 | "version": "0.1.7",
416 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
417 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
418 | "requires": {
419 | "assert-plus": "^1.0.0"
420 | }
421 | },
422 | "glob": {
423 | "version": "7.1.4",
424 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
425 | "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
426 | "requires": {
427 | "fs.realpath": "^1.0.0",
428 | "inflight": "^1.0.4",
429 | "inherits": "2",
430 | "minimatch": "^3.0.4",
431 | "once": "^1.3.0",
432 | "path-is-absolute": "^1.0.0"
433 | }
434 | },
435 | "graceful-fs": {
436 | "version": "4.2.2",
437 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz",
438 | "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q=="
439 | },
440 | "har-schema": {
441 | "version": "2.0.0",
442 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
443 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
444 | },
445 | "har-validator": {
446 | "version": "5.1.3",
447 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
448 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
449 | "requires": {
450 | "ajv": "^6.5.5",
451 | "har-schema": "^2.0.0"
452 | }
453 | },
454 | "has-flag": {
455 | "version": "3.0.0",
456 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
457 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
458 | },
459 | "http-signature": {
460 | "version": "1.2.0",
461 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
462 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
463 | "requires": {
464 | "assert-plus": "^1.0.0",
465 | "jsprim": "^1.2.2",
466 | "sshpk": "^1.7.0"
467 | }
468 | },
469 | "ieee754": {
470 | "version": "1.1.13",
471 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
472 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
473 | },
474 | "inflight": {
475 | "version": "1.0.6",
476 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
477 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
478 | "requires": {
479 | "once": "^1.3.0",
480 | "wrappy": "1"
481 | }
482 | },
483 | "inherits": {
484 | "version": "2.0.4",
485 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
486 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
487 | },
488 | "is-fullwidth-code-point": {
489 | "version": "2.0.0",
490 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
491 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
492 | },
493 | "is-interactive": {
494 | "version": "1.0.0",
495 | "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
496 | "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="
497 | },
498 | "is-typedarray": {
499 | "version": "1.0.0",
500 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
501 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
502 | },
503 | "isarray": {
504 | "version": "1.0.0",
505 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
506 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
507 | },
508 | "isstream": {
509 | "version": "0.1.2",
510 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
511 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
512 | },
513 | "jsbn": {
514 | "version": "0.1.1",
515 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
516 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
517 | },
518 | "json-schema": {
519 | "version": "0.2.3",
520 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
521 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
522 | },
523 | "json-schema-traverse": {
524 | "version": "0.4.1",
525 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
526 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
527 | },
528 | "json-stringify-safe": {
529 | "version": "5.0.1",
530 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
531 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
532 | },
533 | "json2csv": {
534 | "version": "4.5.3",
535 | "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-4.5.3.tgz",
536 | "integrity": "sha512-tg5sm25TOwgMsPUixPFmmuOUFtVCj4p57XipoE8gi/ejNftce/0d8LBgWnCkjF4HsLDsFzszdbIEV6mnK0WfNg==",
537 | "requires": {
538 | "commander": "^2.15.1",
539 | "jsonparse": "^1.3.1",
540 | "lodash.get": "^4.4.2"
541 | }
542 | },
543 | "jsonparse": {
544 | "version": "1.3.1",
545 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
546 | "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
547 | },
548 | "jsprim": {
549 | "version": "1.4.1",
550 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
551 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
552 | "requires": {
553 | "assert-plus": "1.0.0",
554 | "extsprintf": "1.3.0",
555 | "json-schema": "0.2.3",
556 | "verror": "1.10.0"
557 | }
558 | },
559 | "lazystream": {
560 | "version": "1.0.0",
561 | "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
562 | "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
563 | "requires": {
564 | "readable-stream": "^2.0.5"
565 | },
566 | "dependencies": {
567 | "readable-stream": {
568 | "version": "2.3.6",
569 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
570 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
571 | "requires": {
572 | "core-util-is": "~1.0.0",
573 | "inherits": "~2.0.3",
574 | "isarray": "~1.0.0",
575 | "process-nextick-args": "~2.0.0",
576 | "safe-buffer": "~5.1.1",
577 | "string_decoder": "~1.1.1",
578 | "util-deprecate": "~1.0.1"
579 | }
580 | }
581 | }
582 | },
583 | "locate-path": {
584 | "version": "3.0.0",
585 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
586 | "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
587 | "requires": {
588 | "p-locate": "^3.0.0",
589 | "path-exists": "^3.0.0"
590 | }
591 | },
592 | "lodash": {
593 | "version": "4.17.15",
594 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
595 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
596 | },
597 | "lodash.defaults": {
598 | "version": "4.2.0",
599 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
600 | "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
601 | },
602 | "lodash.difference": {
603 | "version": "4.5.0",
604 | "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
605 | "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
606 | },
607 | "lodash.flatten": {
608 | "version": "4.4.0",
609 | "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
610 | "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
611 | },
612 | "lodash.get": {
613 | "version": "4.4.2",
614 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
615 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
616 | },
617 | "lodash.isplainobject": {
618 | "version": "4.0.6",
619 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
620 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
621 | },
622 | "lodash.union": {
623 | "version": "4.6.0",
624 | "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
625 | "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
626 | },
627 | "log-symbols": {
628 | "version": "3.0.0",
629 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
630 | "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
631 | "requires": {
632 | "chalk": "^2.4.2"
633 | }
634 | },
635 | "mime-db": {
636 | "version": "1.40.0",
637 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
638 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
639 | },
640 | "mime-types": {
641 | "version": "2.1.24",
642 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
643 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
644 | "requires": {
645 | "mime-db": "1.40.0"
646 | }
647 | },
648 | "mimic-fn": {
649 | "version": "2.1.0",
650 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
651 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
652 | },
653 | "minimatch": {
654 | "version": "3.0.4",
655 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
656 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
657 | "requires": {
658 | "brace-expansion": "^1.1.7"
659 | }
660 | },
661 | "normalize-path": {
662 | "version": "3.0.0",
663 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
664 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
665 | },
666 | "oauth-sign": {
667 | "version": "0.9.0",
668 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
669 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
670 | },
671 | "once": {
672 | "version": "1.4.0",
673 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
674 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
675 | "requires": {
676 | "wrappy": "1"
677 | }
678 | },
679 | "onetime": {
680 | "version": "5.1.0",
681 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
682 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
683 | "requires": {
684 | "mimic-fn": "^2.1.0"
685 | }
686 | },
687 | "ora": {
688 | "version": "4.0.2",
689 | "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.2.tgz",
690 | "integrity": "sha512-YUOZbamht5mfLxPmk4M35CD/5DuOkAacxlEUbStVXpBAt4fyhBf+vZHI/HRkI++QUp3sNoeA2Gw4C+hi4eGSig==",
691 | "requires": {
692 | "chalk": "^2.4.2",
693 | "cli-cursor": "^3.1.0",
694 | "cli-spinners": "^2.2.0",
695 | "is-interactive": "^1.0.0",
696 | "log-symbols": "^3.0.0",
697 | "strip-ansi": "^5.2.0",
698 | "wcwidth": "^1.0.1"
699 | }
700 | },
701 | "p-limit": {
702 | "version": "2.2.1",
703 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
704 | "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
705 | "requires": {
706 | "p-try": "^2.0.0"
707 | }
708 | },
709 | "p-locate": {
710 | "version": "3.0.0",
711 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
712 | "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
713 | "requires": {
714 | "p-limit": "^2.0.0"
715 | }
716 | },
717 | "p-try": {
718 | "version": "2.2.0",
719 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
720 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
721 | },
722 | "path-exists": {
723 | "version": "3.0.0",
724 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
725 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
726 | },
727 | "path-is-absolute": {
728 | "version": "1.0.1",
729 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
730 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
731 | },
732 | "performance-now": {
733 | "version": "2.1.0",
734 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
735 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
736 | },
737 | "process-nextick-args": {
738 | "version": "2.0.1",
739 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
740 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
741 | },
742 | "progress": {
743 | "version": "2.0.3",
744 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
745 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
746 | },
747 | "psl": {
748 | "version": "1.4.0",
749 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz",
750 | "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw=="
751 | },
752 | "punycode": {
753 | "version": "2.1.1",
754 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
755 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
756 | },
757 | "qs": {
758 | "version": "6.5.2",
759 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
760 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
761 | },
762 | "readable-stream": {
763 | "version": "3.4.0",
764 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
765 | "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
766 | "requires": {
767 | "inherits": "^2.0.3",
768 | "string_decoder": "^1.1.1",
769 | "util-deprecate": "^1.0.1"
770 | }
771 | },
772 | "request": {
773 | "version": "2.88.0",
774 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
775 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
776 | "requires": {
777 | "aws-sign2": "~0.7.0",
778 | "aws4": "^1.8.0",
779 | "caseless": "~0.12.0",
780 | "combined-stream": "~1.0.6",
781 | "extend": "~3.0.2",
782 | "forever-agent": "~0.6.1",
783 | "form-data": "~2.3.2",
784 | "har-validator": "~5.1.0",
785 | "http-signature": "~1.2.0",
786 | "is-typedarray": "~1.0.0",
787 | "isstream": "~0.1.2",
788 | "json-stringify-safe": "~5.0.1",
789 | "mime-types": "~2.1.19",
790 | "oauth-sign": "~0.9.0",
791 | "performance-now": "^2.1.0",
792 | "qs": "~6.5.2",
793 | "safe-buffer": "^5.1.2",
794 | "tough-cookie": "~2.4.3",
795 | "tunnel-agent": "^0.6.0",
796 | "uuid": "^3.3.2"
797 | }
798 | },
799 | "request-promise": {
800 | "version": "4.2.4",
801 | "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz",
802 | "integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==",
803 | "requires": {
804 | "bluebird": "^3.5.0",
805 | "request-promise-core": "1.1.2",
806 | "stealthy-require": "^1.1.1",
807 | "tough-cookie": "^2.3.3"
808 | }
809 | },
810 | "request-promise-core": {
811 | "version": "1.1.2",
812 | "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
813 | "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==",
814 | "requires": {
815 | "lodash": "^4.17.11"
816 | }
817 | },
818 | "require-directory": {
819 | "version": "2.1.1",
820 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
821 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
822 | },
823 | "require-main-filename": {
824 | "version": "2.0.0",
825 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
826 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
827 | },
828 | "restore-cursor": {
829 | "version": "3.1.0",
830 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
831 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
832 | "requires": {
833 | "onetime": "^5.1.0",
834 | "signal-exit": "^3.0.2"
835 | }
836 | },
837 | "safe-buffer": {
838 | "version": "5.1.2",
839 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
840 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
841 | },
842 | "safer-buffer": {
843 | "version": "2.1.2",
844 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
845 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
846 | },
847 | "set-blocking": {
848 | "version": "2.0.0",
849 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
850 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
851 | },
852 | "signal-exit": {
853 | "version": "3.0.2",
854 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
855 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
856 | },
857 | "sshpk": {
858 | "version": "1.16.1",
859 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
860 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
861 | "requires": {
862 | "asn1": "~0.2.3",
863 | "assert-plus": "^1.0.0",
864 | "bcrypt-pbkdf": "^1.0.0",
865 | "dashdash": "^1.12.0",
866 | "ecc-jsbn": "~0.1.1",
867 | "getpass": "^0.1.1",
868 | "jsbn": "~0.1.0",
869 | "safer-buffer": "^2.0.2",
870 | "tweetnacl": "~0.14.0"
871 | }
872 | },
873 | "stealthy-require": {
874 | "version": "1.1.1",
875 | "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
876 | "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
877 | },
878 | "string-width": {
879 | "version": "3.1.0",
880 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
881 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
882 | "requires": {
883 | "emoji-regex": "^7.0.1",
884 | "is-fullwidth-code-point": "^2.0.0",
885 | "strip-ansi": "^5.1.0"
886 | }
887 | },
888 | "string_decoder": {
889 | "version": "1.1.1",
890 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
891 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
892 | "requires": {
893 | "safe-buffer": "~5.1.0"
894 | }
895 | },
896 | "strip-ansi": {
897 | "version": "5.2.0",
898 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
899 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
900 | "requires": {
901 | "ansi-regex": "^4.1.0"
902 | }
903 | },
904 | "supports-color": {
905 | "version": "5.5.0",
906 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
907 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
908 | "requires": {
909 | "has-flag": "^3.0.0"
910 | }
911 | },
912 | "tar-stream": {
913 | "version": "2.1.0",
914 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.0.tgz",
915 | "integrity": "sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==",
916 | "requires": {
917 | "bl": "^3.0.0",
918 | "end-of-stream": "^1.4.1",
919 | "fs-constants": "^1.0.0",
920 | "inherits": "^2.0.3",
921 | "readable-stream": "^3.1.1"
922 | }
923 | },
924 | "tough-cookie": {
925 | "version": "2.4.3",
926 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
927 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
928 | "requires": {
929 | "psl": "^1.1.24",
930 | "punycode": "^1.4.1"
931 | },
932 | "dependencies": {
933 | "punycode": {
934 | "version": "1.4.1",
935 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
936 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
937 | }
938 | }
939 | },
940 | "tunnel-agent": {
941 | "version": "0.6.0",
942 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
943 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
944 | "requires": {
945 | "safe-buffer": "^5.0.1"
946 | }
947 | },
948 | "tweetnacl": {
949 | "version": "0.14.5",
950 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
951 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
952 | },
953 | "uri-js": {
954 | "version": "4.2.2",
955 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
956 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
957 | "requires": {
958 | "punycode": "^2.1.0"
959 | }
960 | },
961 | "util-deprecate": {
962 | "version": "1.0.2",
963 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
964 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
965 | },
966 | "uuid": {
967 | "version": "3.3.3",
968 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
969 | "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
970 | },
971 | "verror": {
972 | "version": "1.10.0",
973 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
974 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
975 | "requires": {
976 | "assert-plus": "^1.0.0",
977 | "core-util-is": "1.0.2",
978 | "extsprintf": "^1.2.0"
979 | }
980 | },
981 | "wcwidth": {
982 | "version": "1.0.1",
983 | "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
984 | "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
985 | "requires": {
986 | "defaults": "^1.0.3"
987 | }
988 | },
989 | "which-module": {
990 | "version": "2.0.0",
991 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
992 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
993 | },
994 | "wrap-ansi": {
995 | "version": "5.1.0",
996 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
997 | "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
998 | "requires": {
999 | "ansi-styles": "^3.2.0",
1000 | "string-width": "^3.0.0",
1001 | "strip-ansi": "^5.0.0"
1002 | }
1003 | },
1004 | "wrappy": {
1005 | "version": "1.0.2",
1006 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1007 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
1008 | },
1009 | "y18n": {
1010 | "version": "4.0.0",
1011 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
1012 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
1013 | },
1014 | "yargs": {
1015 | "version": "14.0.0",
1016 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.0.0.tgz",
1017 | "integrity": "sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow==",
1018 | "requires": {
1019 | "cliui": "^5.0.0",
1020 | "decamelize": "^1.2.0",
1021 | "find-up": "^3.0.0",
1022 | "get-caller-file": "^2.0.1",
1023 | "require-directory": "^2.1.1",
1024 | "require-main-filename": "^2.0.0",
1025 | "set-blocking": "^2.0.0",
1026 | "string-width": "^3.0.0",
1027 | "which-module": "^2.0.0",
1028 | "y18n": "^4.0.0",
1029 | "yargs-parser": "^13.1.1"
1030 | }
1031 | },
1032 | "yargs-parser": {
1033 | "version": "13.1.1",
1034 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
1035 | "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
1036 | "requires": {
1037 | "camelcase": "^5.0.0",
1038 | "decamelize": "^1.2.0"
1039 | }
1040 | },
1041 | "zip-stream": {
1042 | "version": "2.1.2",
1043 | "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.2.tgz",
1044 | "integrity": "sha512-ykebHGa2+uzth/R4HZLkZh3XFJzivhVsjJt8bN3GvBzLaqqrUdRacu+c4QtnUgjkkQfsOuNE1JgLKMCPNmkKgg==",
1045 | "requires": {
1046 | "archiver-utils": "^2.1.0",
1047 | "compress-commons": "^2.1.1",
1048 | "readable-stream": "^3.4.0"
1049 | }
1050 | }
1051 | }
1052 | }
1053 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "instatouch",
3 | "version": "2.3.20",
4 | "description": "Scrape instagram posts from Username, Hashtag or Location pages. Download media and save them to a ZIP archive. Create JSON/CSV files with a post information. No login required",
5 | "main": "./build/index.js",
6 | "types": "./build/index.d.ts",
7 | "bin": {
8 | "instatouch": "bin/cli.js"
9 | },
10 | "scripts": {
11 | "build": "rimraf build && tsc",
12 | "docker:build": "tsc",
13 | "format": "prettier --config ./.prettierrc.js --write './src/**/*.ts'",
14 | "lint": "eslint ./src/**/*.ts"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+ssh://git@github.com/drawrowfly/instagram-scraper.git"
19 | },
20 | "keywords": [
21 | "instagram",
22 | "post",
23 | "scraper",
24 | "media",
25 | "api",
26 | "data mining",
27 | "scraping",
28 | "collecting"
29 | ],
30 | "author": "drawRowFly",
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/drawrowfly/instagram-scraper/issues"
34 | },
35 | "homepage": "https://github.com/drawrowfly/instagram-scraper#readme",
36 | "dependencies": {
37 | "archiver": "^3.1.1",
38 | "async": "^3.1.0",
39 | "bluebird": "^3.7.1",
40 | "json2csv": "4.5.1",
41 | "ora": "^4.0.2",
42 | "progress": "^2.0.3",
43 | "request": "^2.88.0",
44 | "request-promise": "^4.2.4",
45 | "socks-proxy-agent": "^5.0.0",
46 | "yargs": "^14.0.0"
47 | },
48 | "devDependencies": {
49 | "@types/archiver": "^3.1.0",
50 | "@types/async": "^3.2.0",
51 | "@types/bluebird": "^3.5.30",
52 | "@types/json2csv": "4.5.1",
53 | "@types/ora": "^3.2.0",
54 | "@types/progress": "^2.0.3",
55 | "@types/request": "^2.48.4",
56 | "@types/request-promise": "^4.1.46",
57 | "@typescript-eslint/eslint-plugin": "^2.27.0",
58 | "@typescript-eslint/parser": "^2.27.0",
59 | "eslint": "^6.8.0",
60 | "eslint-config-airbnb-base": "^14.1.0",
61 | "eslint-config-prettier": "^6.10.1",
62 | "eslint-plugin-import": "^2.20.2",
63 | "eslint-plugin-prettier": "^3.1.2",
64 | "jest": "^25.3.0",
65 | "prettier": "^2.0.4",
66 | "ts-jest": "^25.3.1",
67 | "ts-node": "^8.8.2",
68 | "typescript": "^3.8.3"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/constant/index.ts:
--------------------------------------------------------------------------------
1 | export = {
2 | scrapeType: ['user', 'hashtag', 'location', 'comments', 'likers', 'followers', 'following', 'user_meta', 'post_meta', 'history', 'from-file'],
3 | authScrapeType: ['following', 'followers'],
4 | startFromWebPage: ['location', 'comment', 'user', 'stories'],
5 | startFromWebApi: ['following', 'followers'],
6 | downloadable: ['user', 'hashtag', 'location'],
7 | notDownloadable: ['comments', 'likers', 'followers', 'following', 'user_meta', 'post_meta'],
8 | mediaType: ['video', 'image', 'all'],
9 | fileType: ['json', 'csv', 'all', 'na'],
10 | history: ['user', 'hashtag', 'location', 'comments', 'likers'],
11 | csvFields: [
12 | 'id',
13 | 'ownerId',
14 | 'ownerUsername',
15 | 'shortcode',
16 | 'isVideo',
17 | 'takenAtTimestamp',
18 | 'takenAtGMT',
19 | 'commentsDisabled',
20 | 'thumbnailSrc',
21 | 'url',
22 | 'likes',
23 | 'comments',
24 | 'views',
25 | ],
26 | csvCommentFields: [
27 | 'id',
28 | 'text',
29 | 'created_at',
30 | 'did_report_as_spam',
31 | 'owner_id',
32 | 'owner_username',
33 | 'owner_is_verified',
34 | 'viewer_has_liked',
35 | 'likes',
36 | ],
37 | csvLikersFields: [
38 | 'user_id',
39 | 'username',
40 | 'full_name',
41 | 'profile_pic_url',
42 | 'is_private',
43 | 'is_verified',
44 | 'followed_by_viewer',
45 | 'requested_by_viewer',
46 | ],
47 | hash: {
48 | user: '003056d32c2554def87228bc3fd9668a',
49 | hashtag: '174a5243287c5f3a7de741089750ab3b',
50 | location: '1b84447a4d8b6d6d0426fefb34514485',
51 | post: '870ea3e846839a3b6a8cd9cd7e42290c',
52 | comments: 'bc3296d1ce80a24b1b6e40b1e72903f5',
53 | likers: 'd5d763b1e2acf209d62d22d184488e57',
54 | followers: 'c76146de99bb02f6415203be841dd25a',
55 | following: 'd04b0a864b4b54837c0d870b0e77e076',
56 | },
57 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
58 | };
59 |
--------------------------------------------------------------------------------
/src/core/Downloader.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /* eslint-disable consistent-return */
3 | import request from 'request';
4 | import { Agent } from 'http';
5 | import { createWriteStream, writeFile } from 'fs';
6 | import archiver from 'archiver';
7 | import { SocksProxyAgent } from 'socks-proxy-agent';
8 | import { forEachLimit } from 'async';
9 | import { fromCallback } from 'bluebird';
10 |
11 | import { MultipleBar } from '../helpers';
12 | import { DownloaderConstructor, PostCollector, ZipValues, Proxy } from '../types';
13 |
14 | export class Downloader {
15 | public progress: boolean;
16 |
17 | public mbars: MultipleBar;
18 |
19 | public progressBar: any[];
20 |
21 | private proxy: string[] | string;
22 |
23 | public userAgent: string;
24 |
25 | public filepath: string;
26 |
27 | public bulk: boolean;
28 |
29 | constructor({ progress, proxy, userAgent, filepath, bulk }: DownloaderConstructor) {
30 | this.progress = true || progress;
31 | this.progressBar = [];
32 | this.userAgent = userAgent;
33 | this.filepath = filepath;
34 | this.mbars = new MultipleBar();
35 | this.proxy = proxy;
36 | this.bulk = bulk;
37 | }
38 |
39 | /**
40 | * Get proxy
41 | */
42 | private get getProxy(): Proxy {
43 | if (Array.isArray(this.proxy)) {
44 | const selectProxy = this.proxy.length ? this.proxy[Math.floor(Math.random() * this.proxy.length)] : '';
45 | return {
46 | socks: false,
47 | proxy: selectProxy,
48 | };
49 | }
50 | if (this.proxy.indexOf('socks4://') > -1 || this.proxy.indexOf('socks5://') > -1) {
51 | return {
52 | socks: true,
53 | proxy: new SocksProxyAgent(this.proxy as string),
54 | };
55 | }
56 | return {
57 | socks: false,
58 | proxy: this.proxy as string,
59 | };
60 | }
61 |
62 | /**
63 | * Add new bard to indicate download progress
64 | * @param {number} len
65 | */
66 | public addBar(len: number): any[] {
67 | this.progressBar.push(
68 | this.mbars.newBar('Downloading :id [:bar] :percent', {
69 | complete: '=',
70 | incomplete: ' ',
71 | width: 30,
72 | total: len,
73 | }),
74 | );
75 |
76 | return this.progressBar[this.progressBar.length - 1];
77 | }
78 |
79 | /**
80 | * Convert video file to a buffer
81 | * @param {*} item
82 | */
83 | public toBuffer(item: PostCollector): Promise {
84 | return new Promise((resolve, reject) => {
85 | const proxy = this.getProxy;
86 | let r = request;
87 | let barIndex;
88 | let buffer = Buffer.from('');
89 | if (proxy.proxy && !proxy.socks) {
90 | r = request.defaults({ proxy: `http://${proxy.proxy}/` });
91 | }
92 | if (proxy.proxy && proxy.socks) {
93 | r = request.defaults({ agent: proxy.proxy as Agent });
94 | }
95 | r.get({
96 | url: item.is_video ? item.video_url! : item.display_url!,
97 | headers: {
98 | 'user-agent': this.userAgent,
99 | },
100 | })
101 | .on('response', (response) => {
102 | if (this.progress && !this.bulk) {
103 | barIndex = this.addBar(parseInt(response.headers['content-length'] as string, 10));
104 | }
105 | })
106 | .on('data', (chunk) => {
107 | buffer = Buffer.concat([buffer, chunk as Buffer]);
108 | if (this.progress && !this.bulk) {
109 | barIndex.tick(chunk.length, { id: item.id });
110 | }
111 | })
112 | .on('end', () => {
113 | resolve(buffer);
114 | })
115 | .on('error', () => {
116 | reject(new Error(`Cant download video: ${item.id}. If you were using proxy, please try without it.`));
117 | });
118 | });
119 | }
120 |
121 | /**
122 | * Download and ZIP video files
123 | */
124 | public downloadPosts({ zip, folder, collector, fileName, asyncDownload }: ZipValues) {
125 | return new Promise((resolve, reject) => {
126 | const saveDestination = zip ? `${fileName}.zip` : folder;
127 | const archive = archiver('zip', {
128 | gzip: true,
129 | zlib: { level: 9 },
130 | });
131 | if (zip) {
132 | const output = createWriteStream(saveDestination);
133 | archive.pipe(output);
134 | }
135 |
136 | forEachLimit(
137 | collector,
138 | asyncDownload,
139 | (item: PostCollector, cb) => {
140 | this.toBuffer(item)
141 | .then(async (buffer) => {
142 | item.downloaded = true;
143 | if (zip) {
144 | archive.append(buffer, { name: `${item.is_video ? `${item.shortcode}.mp4` : `${item.shortcode}.jpeg`}` });
145 | } else {
146 | await fromCallback((cback) =>
147 | writeFile(
148 | `${saveDestination}/${item.is_video ? `${item.shortcode}.mp4` : `${item.shortcode}.jpeg`}`,
149 | buffer,
150 | cback,
151 | ),
152 | );
153 | }
154 | cb(null);
155 | })
156 | .catch(() => {
157 | item.downloaded = false;
158 | cb(null);
159 | });
160 | },
161 | (error) => {
162 | if (error) {
163 | return reject(error);
164 | }
165 |
166 | if (zip) {
167 | archive.finalize();
168 | archive.on('end', () => resolve());
169 | } else {
170 | resolve();
171 | }
172 | },
173 | );
174 | });
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/core/InstaTouch.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-promise-reject-errors */
2 | /* eslint-disable no-await-in-loop */
3 | /* eslint-disable no-throw-literal */
4 | /* eslint-disable no-underscore-dangle */
5 | /* eslint-disable no-async-promise-executor */
6 | import rp, { OptionsWithUri } from 'request-promise';
7 | import { Parser } from 'json2csv';
8 | import { forEachLimit } from 'async';
9 | import ora, { Ora } from 'ora';
10 | import { tmpdir } from 'os';
11 | import { SocksProxyAgent } from 'socks-proxy-agent';
12 | import { fromCallback } from 'bluebird';
13 | import { writeFile, readFile, mkdir } from 'fs';
14 |
15 | /**
16 | * Helper
17 | */
18 | import { Downloader } from '.';
19 |
20 | import { randomString } from '../helpers';
21 |
22 | /**
23 | * Constant
24 | */
25 | import CONST from '../constant';
26 |
27 | /**
28 | * Types
29 | */
30 | import {
31 | Constructor,
32 | User,
33 | Post,
34 | Hashtag,
35 | Location,
36 | Comments,
37 | PostCollector,
38 | PostMetaFromWebApi,
39 | GraphQlResponse,
40 | Result,
41 | Likers,
42 | UserMetaFromWebApi,
43 | Proxy,
44 | History,
45 | ScrapeType,
46 | Edges,
47 | GraphQl,
48 | UserStories,
49 | UserReelsFeed,
50 | } from '../types';
51 |
52 | export class InstaTouch {
53 | private url: string;
54 |
55 | private download: boolean;
56 |
57 | private filepath: string;
58 |
59 | private filetype: string;
60 |
61 | private fileName: string;
62 |
63 | private storeHistory: boolean;
64 |
65 | private input: string;
66 |
67 | private toCollect: number;
68 |
69 | private storeValue: string;
70 |
71 | private mediaType: string;
72 |
73 | private scrapeType: ScrapeType;
74 |
75 | private asyncDownload: number;
76 |
77 | private cli: boolean;
78 |
79 | private proxy: string[] | string;
80 |
81 | private session: string[] | string;
82 |
83 | private json2csvParser: Parser;
84 |
85 | private collector: PostCollector[];
86 |
87 | private spinner: Ora;
88 |
89 | private userAgent: string;
90 |
91 | public Downloader: Downloader;
92 |
93 | private hasNextPage: boolean;
94 |
95 | private endCursor: string;
96 |
97 | private id: string;
98 |
99 | private timeout: number | undefined;
100 |
101 | private historyPath: string;
102 |
103 | private bulk: boolean;
104 |
105 | private zip: boolean;
106 |
107 | private itemCount: number;
108 |
109 | private csrfToken: string;
110 |
111 | private auth_error: boolean;
112 |
113 | private headers: {};
114 |
115 | constructor({
116 | url,
117 | download = false,
118 | filepath = '',
119 | filename = '',
120 | filetype,
121 | input,
122 | count,
123 | proxy,
124 | session,
125 | mediaType = 'all',
126 | scrapeType,
127 | asyncDownload,
128 | userAgent,
129 | progress = false,
130 | store_history = false,
131 | timeout,
132 | cli,
133 | endCursor,
134 | bulk = false,
135 | historyPath,
136 | zip = false,
137 | headers = {},
138 | user_id = false,
139 | }: Constructor) {
140 | this.zip = zip;
141 | this.url = url;
142 | this.id = user_id ? input : '';
143 | this.download = download;
144 | this.filepath = process.env.SCRAPING_FROM_DOCKER ? '/usr/app/files' : filepath || '';
145 | this.fileName = filename;
146 | this.filetype = filetype;
147 | this.storeValue = `${scrapeType}_${input}`;
148 | this.input = input;
149 | this.storeHistory = cli && store_history;
150 | this.toCollect = count;
151 | this.proxy = proxy;
152 | this.headers = headers;
153 | this.session = session;
154 | this.json2csvParser = new Parser({ flatten: true });
155 | this.mediaType = mediaType;
156 | this.scrapeType = scrapeType;
157 | this.asyncDownload = asyncDownload;
158 | this.collector = [];
159 | this.itemCount = 0;
160 | this.spinner = ora('InstaTouch Scraper Started');
161 | this.historyPath = process.env.SCRAPING_FROM_DOCKER ? '/usr/app/files' : historyPath || tmpdir();
162 | this.bulk = bulk;
163 | this.csrfToken = randomString(29);
164 | this.Downloader = new Downloader({
165 | progress,
166 | proxy,
167 | userAgent,
168 | filepath: process.env.SCRAPING_FROM_DOCKER ? '/usr/app/files' : filepath || '',
169 | bulk,
170 | });
171 | this.timeout = timeout;
172 | this.cli = cli;
173 | // Important!!! If you change user agents, hash keys will be invalid
174 | this.userAgent = userAgent || CONST.userAgent;
175 | this.hasNextPage = false;
176 | this.endCursor = endCursor as string;
177 | this.auth_error = false;
178 | }
179 |
180 | /**
181 | * Get proxy
182 | */
183 | private get getProxy(): Proxy {
184 | if (Array.isArray(this.proxy)) {
185 | const selectProxy = this.proxy.length ? this.proxy[Math.floor(Math.random() * this.proxy.length)] : '';
186 | return {
187 | socks: false,
188 | proxy: selectProxy,
189 | };
190 | }
191 | if (this.proxy.indexOf('socks4://') > -1 || this.proxy.indexOf('socks5://') > -1) {
192 | return {
193 | socks: true,
194 | proxy: new SocksProxyAgent(this.proxy as string),
195 | };
196 | }
197 | return {
198 | socks: false,
199 | proxy: this.proxy as string,
200 | };
201 | }
202 |
203 | /**
204 | * Get session id
205 | */
206 | private get getSession(): string {
207 | if (Array.isArray(this.session) && this.session.length) {
208 | return this.session[Math.floor(Math.random() * this.session.length)];
209 | }
210 | if (!Array.isArray(this.session) && this.session) {
211 | return this.session as string;
212 | }
213 | return '';
214 | }
215 |
216 | /**
217 | * Main request method
218 | * @param param0
219 | */
220 | private request({ uri, method, qs, body, form, headers, json, gzip }: OptionsWithUri): Promise {
221 | return new Promise(async (resolve, reject) => {
222 | const proxy = this.getProxy;
223 | const session = this.getSession;
224 |
225 | const options = {
226 | uri,
227 | method,
228 | ...(qs ? { qs } : {}),
229 | ...(body ? { body } : {}),
230 | ...(form ? { form } : {}),
231 | headers: {
232 | 'User-Agent': this.userAgent,
233 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
234 | 'Accept-Encoding': 'gzip, deflate, br',
235 | 'Accept-Language': 'en-US,en;q=0.5',
236 | 'Upgrade-Insecure-Requests': 1,
237 | ...(session ? { cookie: session } : {}),
238 | ...headers,
239 | ...this.headers,
240 | },
241 | ...(json ? { json: true } : {}),
242 | ...(gzip ? { gzip: true } : {}),
243 | resolveWithFullResponse: true,
244 | ...(proxy.proxy && proxy.socks ? { agent: proxy.proxy } : {}),
245 | ...(proxy.proxy && !proxy.socks ? { proxy: `http://${proxy.proxy}/` } : {}),
246 | } as OptionsWithUri;
247 |
248 | try {
249 | const response = await rp(options);
250 | if (response.headers['content-type'].indexOf('text/html') > -1) {
251 | if (response.body.indexOf('AuthLogin') > -1) {
252 | this.auth_error = true;
253 | }
254 | throw new Error(`Wrong response content type! Received: ${response.headers['content-type']} Expected: application/json`);
255 | }
256 |
257 | if (this.timeout) {
258 | setTimeout(() => {
259 | resolve(response.body);
260 | }, this.timeout);
261 | } else {
262 | resolve(response.body);
263 | }
264 | } catch (error) {
265 | if (error.name === 'StatusCodeError') {
266 | if (error.response.headers['content-type'].indexOf('application/json') > -1) {
267 | if (error.response.body.status === 'fail') {
268 | this.auth_error = true;
269 | }
270 | }
271 | reject(`Can't find requested data`);
272 | } else if (error.name === 'RequestError') {
273 | reject(`Request error`);
274 | } else {
275 | reject(error);
276 | }
277 | }
278 | });
279 | }
280 |
281 | private returnInitError(error) {
282 | if (this.cli && !this.bulk) {
283 | this.spinner.stop();
284 | }
285 | throw error;
286 | }
287 |
288 | /**
289 | * Get folder destination, where all downloaded posts will be saved
290 | */
291 | private get folderDestination(): string {
292 | switch (this.scrapeType) {
293 | case 'user':
294 | return this.filepath ? `${this.filepath}/${this.input}` : this.input;
295 | case 'hashtag':
296 | return this.filepath ? `${this.filepath}/#${this.input}` : `#${this.input}`;
297 | case 'location':
298 | return this.filepath ? `${this.filepath}/location:${this.input}` : `location:${this.input}`;
299 | default:
300 | throw new TypeError(`${this.scrapeType} is not supported`);
301 | }
302 | }
303 |
304 | /**
305 | * Starting point
306 | */
307 | public async startScraper(): Promise {
308 | if (this.cli && !this.bulk) {
309 | this.spinner.start();
310 | }
311 |
312 | if (this.download && !this.zip) {
313 | try {
314 | await fromCallback((cb) => mkdir(this.folderDestination, { recursive: true }, cb));
315 | } catch (error) {
316 | return this.returnInitError(error.message);
317 | }
318 | }
319 | if (!this.scrapeType || CONST.scrapeType.indexOf(this.scrapeType) === -1) {
320 | return this.returnInitError(`Missing scraping type. Scrape types: ${CONST.scrapeType} `);
321 | }
322 | if (!this.input) {
323 | return this.returnInitError('Missing input');
324 | }
325 |
326 | if (CONST.startFromWebPage.indexOf(this.scrapeType) > -1) {
327 | try {
328 | await this.extractData();
329 | } catch {
330 | //
331 | }
332 | }
333 |
334 | if (CONST.startFromWebApi.indexOf(this.scrapeType) > -1) {
335 | try {
336 | if (!this.id) {
337 | const user = await this.getUserMeta(this.url);
338 | this.id = user.graphql.user.id;
339 | }
340 | } catch {
341 | //
342 | }
343 | }
344 |
345 | if (!this.auth_error) {
346 | if (this.collector.length < this.toCollect) {
347 | await this.mainLoop();
348 | }
349 |
350 | if (this.storeHistory) {
351 | await this.storeDownlodProgress();
352 | }
353 | }
354 | const [json, csv] = await this.saveCollectorData();
355 |
356 | return {
357 | count: this.itemCount,
358 | has_more: this.hasNextPage,
359 | end_cursor: this.endCursor,
360 | id: this.id,
361 | collector: this.collector,
362 | ...(this.filetype === 'all' ? { json, csv } : {}),
363 | ...(this.filetype === 'json' ? { json } : {}),
364 | ...(this.filetype === 'csv' ? { csv } : {}),
365 | auth_error: this.auth_error,
366 | };
367 | }
368 |
369 | /**
370 | * Get file destination(csv, zip, json)
371 | */
372 | private get fileDestination(): string {
373 | if (this.fileName) {
374 | return this.filepath ? `${this.filepath}/${this.fileName}` : this.fileName;
375 | }
376 | switch (this.scrapeType) {
377 | case 'user':
378 | case 'hashtag':
379 | case 'location':
380 | return this.filepath ? `${this.filepath}/${this.input}_${Date.now()}` : `${this.input}_${Date.now()}`;
381 | default:
382 | return this.filepath ? `${this.filepath}/${this.scrapeType}_${Date.now()}` : `${this.scrapeType}_${Date.now()}`;
383 | }
384 | }
385 |
386 | /**
387 | * Store collector data in the CSV and/or JSON files
388 | */
389 | private async saveCollectorData(): Promise {
390 | if (this.download) {
391 | if (this.cli) {
392 | this.spinner.stop();
393 | }
394 | if (this.collector.length) {
395 | await this.Downloader.downloadPosts({
396 | zip: this.zip,
397 | folder: this.folderDestination,
398 | collector: this.collector,
399 | fileName: this.fileDestination,
400 | asyncDownload: this.asyncDownload,
401 | });
402 | }
403 | }
404 | let json = '';
405 | let csv = '';
406 |
407 | if (this.collector.length) {
408 | json = `${this.fileDestination}.json`;
409 | csv = `${this.fileDestination}.csv`;
410 |
411 | if (this.collector.length) {
412 | switch (this.filetype) {
413 | case 'json':
414 | await fromCallback((cb) => writeFile(json, JSON.stringify(this.collector), cb));
415 | break;
416 | case 'csv':
417 | await fromCallback((cb) => writeFile(csv, this.json2csvParser.parse(this.collector), cb));
418 | break;
419 | case 'all':
420 | await Promise.all([
421 | await fromCallback((cb) => writeFile(json, JSON.stringify(this.collector), cb)),
422 | await fromCallback((cb) => writeFile(csv, this.json2csvParser.parse(this.collector), cb)),
423 | ]);
424 | break;
425 | default:
426 | break;
427 | }
428 | }
429 | }
430 | if (this.cli) {
431 | this.spinner.stop();
432 | }
433 | return [json, csv];
434 | }
435 |
436 | /**
437 | * Main loop that collects all required metadata from the ig web api
438 | */
439 | private async mainLoop(): Promise {
440 | while (true) {
441 | try {
442 | /**
443 | * If {bulk} === false and {collector} already has items in it
444 | * then we need to end the process
445 | */
446 | if (!this.bulk && this.collector.length) {
447 | break;
448 | }
449 | await this.graphQlRequest();
450 | if (!this.bulk) {
451 | break;
452 | }
453 | } catch (error) {
454 | break;
455 | }
456 | }
457 | }
458 |
459 | private async graphQlRequest() {
460 | const options = {
461 | method: 'GET',
462 | uri: 'https://www.instagram.com/graphql/query/',
463 | json: true,
464 | gzip: true,
465 | qs: {
466 | query_hash: CONST.hash[this.scrapeType],
467 | variables: this.grapQlQuery,
468 | },
469 | headers: {
470 | Accept: '*/*',
471 | 'X-IG-App-ID': '936619743392459',
472 | 'X-Requested-With': 'XMLHttpRequest',
473 | },
474 | timeout: 5000,
475 | };
476 |
477 | let graphData = {} as Edges;
478 | try {
479 | switch (this.scrapeType) {
480 | case 'user': {
481 | const result = await this.request>(options);
482 | this.hasNextPage = result.data.user.edge_owner_to_timeline_media.page_info.has_next_page;
483 | this.endCursor = result.data.user.edge_owner_to_timeline_media.page_info.end_cursor;
484 | graphData = result.data.user.edge_owner_to_timeline_media;
485 | break;
486 | }
487 | case 'followers': {
488 | const result = await this.request>(options);
489 | this.hasNextPage = result.data.user.edge_followed_by.page_info.has_next_page;
490 | this.endCursor = result.data.user.edge_followed_by.page_info.end_cursor;
491 | graphData = result.data.user.edge_followed_by;
492 | break;
493 | }
494 | case 'following': {
495 | const result = await this.request>(options);
496 | this.hasNextPage = result.data.user.edge_follow.page_info.has_next_page;
497 | this.endCursor = result.data.user.edge_follow.page_info.end_cursor;
498 | graphData = result.data.user.edge_follow;
499 | break;
500 | }
501 | case 'hashtag': {
502 | const result = await this.request>(options);
503 | this.hasNextPage = result.data.hashtag.edge_hashtag_to_media.page_info.has_next_page;
504 | this.endCursor = result.data.hashtag.edge_hashtag_to_media.page_info.end_cursor;
505 | graphData = result.data.hashtag.edge_hashtag_to_media;
506 | break;
507 | }
508 | case 'location': {
509 | const result = await this.request>(options);
510 | this.hasNextPage = result.data.location.edge_location_to_media.page_info.has_next_page;
511 | this.endCursor = result.data.location.edge_location_to_media.page_info.end_cursor;
512 | graphData = result.data.location.edge_location_to_media;
513 | break;
514 | }
515 | case 'comments': {
516 | const result = await this.request>(options);
517 | this.hasNextPage = result.data.shortcode_media.edge_media_to_parent_comment.page_info.has_next_page;
518 | this.endCursor = result.data.shortcode_media.edge_media_to_parent_comment.page_info.end_cursor;
519 | graphData = result.data.shortcode_media.edge_media_to_parent_comment;
520 | break;
521 | }
522 | case 'likers': {
523 | const result = await this.request>(options);
524 | this.hasNextPage = result.data.shortcode_media.edge_liked_by.page_info.has_next_page;
525 | this.endCursor = result.data.shortcode_media.edge_liked_by.page_info.end_cursor;
526 | graphData = result.data.shortcode_media.edge_liked_by;
527 | break;
528 | }
529 | default:
530 | break;
531 | }
532 | this.itemCount = graphData.count;
533 |
534 | await this.collectPosts(graphData.edges);
535 |
536 | if (this.collector.length >= this.toCollect) {
537 | throw new Error('Done');
538 | }
539 | if (!this.hasNextPage) {
540 | throw new Error('No more posts');
541 | }
542 | } catch (error) {
543 | throw error.message;
544 | }
545 | }
546 |
547 | private get grapQlQuery() {
548 | switch (this.scrapeType) {
549 | case 'user':
550 | return JSON.stringify({ id: this.id, first: this.bulk ? 50 : this.toCollect, after: this.endCursor });
551 | case 'hashtag':
552 | return JSON.stringify({ tag_name: this.input, show_ranked: false, first: this.bulk ? 50 : this.toCollect, after: this.endCursor });
553 | case 'location':
554 | return JSON.stringify({ id: this.input, first: this.bulk ? 50 : this.toCollect, after: this.endCursor });
555 | case 'comments':
556 | return JSON.stringify({ shortcode: this.input, first: this.bulk ? 50 : this.toCollect, after: this.endCursor });
557 | case 'likers':
558 | return JSON.stringify({
559 | shortcode: this.input,
560 | include_reel: true,
561 | first: this.bulk ? 50 : this.toCollect,
562 | after: this.endCursor,
563 | });
564 | case 'followers':
565 | case 'following':
566 | return JSON.stringify({
567 | id: this.id,
568 | include_reel: true,
569 | fetch_mutual: false,
570 | first: this.bulk ? 50 : this.toCollect,
571 | after: this.endCursor,
572 | });
573 | default:
574 | return '';
575 | }
576 | }
577 |
578 | private async extractDataHelper(edges: Post[], count: number) {
579 | if (edges.length > this.toCollect) {
580 | edges.splice(this.toCollect);
581 | }
582 |
583 | if (this.toCollect > count) {
584 | this.toCollect = count;
585 | }
586 | this.itemCount = count;
587 | if (!this.endCursor) {
588 | await this.collectPosts(edges);
589 | }
590 | }
591 |
592 | /**
593 | * In order to start scraping user, hashtag, location and comments
594 | * We need to extract ID's that are required to send graphQL request
595 | */
596 | private async extractData(): Promise {
597 | switch (this.scrapeType) {
598 | case 'user': {
599 | if (!this.id) {
600 | const result = await this.extractJson();
601 | try {
602 | this.id = result.graphql.user.id;
603 | } catch (error) {
604 | throw new Error(`Can't scrape date. Please try again or submit issue to the github`);
605 | }
606 | }
607 | break;
608 | }
609 | case 'hashtag': {
610 | if (!this.endCursor) {
611 | const result = await this.extractJson();
612 | try {
613 | const { edges, count } = result.graphql.hashtag.edge_hashtag_to_media;
614 | this.id = result.graphql.hashtag.name;
615 | this.hasNextPage = result.graphql.hashtag.edge_hashtag_to_media.page_info.has_next_page;
616 | await this.extractDataHelper(edges, count);
617 | this.endCursor = this.endCursor || result.graphql.hashtag.edge_hashtag_to_media.page_info.end_cursor;
618 | } catch (error) {
619 | throw new Error(`Can't scrape date. Please try again or submit issue to the github`);
620 | }
621 | }
622 | break;
623 | }
624 | case 'location': {
625 | break;
626 | }
627 | case 'comments': {
628 | const result = await this.extractJson();
629 | try {
630 | const { edges, count } = result.graphql.shortcode_media.edge_media_to_parent_comment;
631 | this.id = result.graphql.shortcode_media.shortcode;
632 | this.hasNextPage = result.graphql.shortcode_media.edge_media_to_parent_comment.page_info.has_next_page;
633 | await this.extractDataHelper(edges, count);
634 | this.endCursor = this.endCursor || result.graphql.shortcode_media.edge_media_to_parent_comment.page_info.end_cursor;
635 | } catch (error) {
636 | throw new Error(`Can't scrape date. Please try again or submit issue to the github`);
637 | }
638 | break;
639 | }
640 | default:
641 | throw new Error(`Not supported type here: ${this.scrapeType}`);
642 | }
643 | }
644 |
645 | /**
646 | * Extract csrf token
647 | */
648 | private async extractJson(): Promise> {
649 | const options = {
650 | method: 'GET',
651 | gzip: true,
652 | jar: true,
653 | uri: this.url,
654 | headers: {
655 | Accept: 'application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
656 | 'Accept-Encoding': 'gzip, deflate, br',
657 | 'Accept-Language': 'en-US,en;q=0.5',
658 | 'Upgrade-Insecure-Requests': 1,
659 | },
660 | json: true,
661 | };
662 |
663 | const response = await this.request>(options);
664 |
665 | return response;
666 | }
667 |
668 | /**
669 | * Get user reels feed
670 | * @param target_user_id
671 | * @param page_size
672 | * @param max_id
673 | * @returns
674 | */
675 | public async getUserReels(target_user_id: string, page_size: number, max_id: string): Promise {
676 | const options = {
677 | method: 'POST',
678 | uri: `https://i.instagram.com/api/v1/clips/user/`,
679 | form: {
680 | target_user_id,
681 | page_size,
682 | max_id,
683 | },
684 | headers: {
685 | 'X-CSRFToken': this.csrfToken,
686 | 'X-IG-App-ID': '936619743392459',
687 | },
688 | gzip: true,
689 | json: true,
690 | };
691 |
692 | const response = await this.request(options);
693 | return response;
694 | }
695 |
696 | /**
697 | * Get post metadata
698 | * @param uri
699 | * @returns
700 | */
701 | public async getPostMeta(uri: string): Promise {
702 | const options = {
703 | method: 'GET',
704 | uri,
705 | gzip: true,
706 | json: true,
707 | };
708 |
709 | const response = await this.request(options);
710 | return response;
711 | }
712 |
713 | /**
714 | * Get user stories
715 | * @param id
716 | * @returns
717 | */
718 | public async getStories(id: string): Promise {
719 | const options = {
720 | method: 'GET',
721 | uri: `https://i.instagram.com/api/v1/feed/reels_media/?reel_ids=${id}`,
722 | headers: {
723 | 'X-IG-App-ID': '936619743392459',
724 | },
725 | gzip: true,
726 | json: true,
727 | };
728 |
729 | const response = await this.request(options);
730 | return response;
731 | }
732 |
733 | /**
734 | * Get user metadata
735 | * @param uri
736 | * @returns
737 | */
738 | public async getUserMeta(uri: string): Promise {
739 | const options = {
740 | method: 'GET',
741 | uri,
742 | gzip: true,
743 | json: true,
744 | };
745 |
746 | const response = await this.request(options);
747 | return response;
748 | }
749 |
750 | private collectPosts(edges: Post[]): Promise {
751 | return new Promise((resolve) => {
752 | forEachLimit(
753 | edges,
754 | 5,
755 | (post, cb) => {
756 | switch (this.scrapeType) {
757 | case 'user':
758 | case 'hashtag':
759 | case 'location': {
760 | const description = post.node.edge_media_to_caption.edges.length
761 | ? post.node.edge_media_to_caption.edges[0].node.text
762 | : '';
763 | const hashtags = description.match(/(#\w+)/g);
764 | const mentions = description.match(/(@\w+)/g);
765 | const item: PostCollector = {
766 | id: post.node.id,
767 | shortcode: post.node.shortcode,
768 | type: post.node.__typename,
769 | is_video: post.node.is_video,
770 | ...(post.node.is_video ? { video_url: post.node.video_url } : {}),
771 | dimension: post.node.dimensions,
772 | display_url: post.node.display_url,
773 | thumbnail_src: post.node.thumbnail_src,
774 | owner: post.node.owner,
775 | description,
776 | comments: post.node.edge_media_to_comment.count,
777 | likes: post.node.edge_media_preview_like.count,
778 | ...(post.node.is_video ? { views: post.node.video_view_count } : {}),
779 | comments_disabled: post.node.comments_disabled,
780 | taken_at_timestamp: post.node.taken_at_timestamp,
781 | location: post.node.location,
782 | hashtags: hashtags || [],
783 | mentions: mentions || [],
784 | ...(this.scrapeType === 'user'
785 | ? {
786 | tagged_users: post.node.edge_media_to_tagged_user.edges.length
787 | ? post.node.edge_media_to_tagged_user.edges.map((taggedUser) => {
788 | return {
789 | user: {
790 | full_name: taggedUser.node.user.full_name,
791 | id: taggedUser.node.user.id,
792 | is_verified: taggedUser.node.user.is_verified,
793 | profile_pic_url: taggedUser.node.user.profile_pic_url,
794 | username: taggedUser.node.user.username,
795 | },
796 | x: taggedUser.node.x,
797 | y: taggedUser.node.y,
798 | };
799 | })
800 | : [],
801 | }
802 | : {}),
803 | };
804 |
805 | this.cbCollector(cb, item);
806 |
807 | break;
808 | }
809 | case 'comments': {
810 | const item: PostCollector = {
811 | id: post.node.id,
812 | text: post.node.text,
813 | created_at: post.node.created_at,
814 | did_report_as_spam: post.node.did_report_as_spam,
815 | owner: post.node.owner,
816 | likes: post.node.edge_liked_by.count,
817 | comments: post.node.edge_threaded_comments.count,
818 | };
819 |
820 | this.cbCollector(cb, item);
821 | break;
822 | }
823 | case 'likers':
824 | case 'followers':
825 | case 'following': {
826 | const item: PostCollector = {
827 | id: post.node.id,
828 | username: post.node.username,
829 | full_name: post.node.full_name,
830 | profile_pic_url: post.node.profile_pic_url,
831 | is_private: post.node.is_private,
832 | is_verified: post.node.is_verified,
833 | };
834 |
835 | this.cbCollector(cb, item);
836 | break;
837 | }
838 | default:
839 | break;
840 | }
841 | },
842 | () => {
843 | resolve('');
844 | },
845 | );
846 | });
847 | }
848 |
849 | /**
850 | * Collectors callback method
851 | * @param cb
852 | * @param item
853 | */
854 | private cbCollector(cb, item: PostCollector): any {
855 | if (item.is_video && this.mediaType === 'image') {
856 | cb(null);
857 | } else if (this.mediaType === 'video' && !item.is_video) {
858 | cb(null);
859 | } else {
860 | this.collector.push(item);
861 | cb(null);
862 | }
863 | }
864 |
865 | /**
866 | * Store progress to avoid downloading duplicates
867 | * Only available from the CLI
868 | */
869 | private async storeDownlodProgress() {
870 | const historyType = `${this.scrapeType}_${this.input}`;
871 | if (this.storeValue) {
872 | let history = {} as History;
873 |
874 | try {
875 | const readFromStore = (await fromCallback((cb) =>
876 | readFile(`${this.historyPath}/ig_history.json`, { encoding: 'utf-8' }, cb),
877 | )) as string;
878 | history = JSON.parse(readFromStore);
879 | } catch (error) {
880 | history[historyType] = {
881 | type: this.scrapeType,
882 | input: this.input,
883 | collected_items: 0,
884 | last_change: new Date(),
885 | file_location: `${this.historyPath}/ig_${this.storeValue}.json`,
886 | };
887 | }
888 |
889 | if (!history[historyType]) {
890 | history[historyType] = {
891 | type: this.scrapeType,
892 | input: this.input,
893 | collected_items: 0,
894 | last_change: new Date(),
895 | file_location: `${this.historyPath}/ig_${this.storeValue}.json`,
896 | };
897 | }
898 | let store: string[];
899 | try {
900 | const readFromStore = (await fromCallback((cb) =>
901 | readFile(`${this.historyPath}/ig_${this.storeValue}.json`, { encoding: 'utf-8' }, cb),
902 | )) as string;
903 | store = JSON.parse(readFromStore);
904 | } catch (error) {
905 | store = [];
906 | }
907 |
908 | this.collector = this.collector.map((item) => {
909 | if (store.indexOf(item.id) === -1) {
910 | store.push(item.id);
911 | } else {
912 | // eslint-disable-next-line no-param-reassign
913 | item.repeated = true;
914 | }
915 | return item;
916 | });
917 | this.collector = this.collector.filter((item) => !item.repeated);
918 |
919 | history[historyType] = {
920 | type: this.scrapeType,
921 | input: this.input,
922 | collected_items: history[historyType].collected_items + this.collector.length,
923 | last_change: new Date(),
924 | file_location: `${this.historyPath}/ig_${this.storeValue}.json`,
925 | };
926 |
927 | try {
928 | await fromCallback((cb) => writeFile(`${this.historyPath}/ig_${this.storeValue}.json`, JSON.stringify(store), cb));
929 | } catch (error) {
930 | // continue regardless of error
931 | }
932 |
933 | try {
934 | await fromCallback((cb) => writeFile(`${this.historyPath}/ig_history.json`, JSON.stringify(history), cb));
935 | } catch (error) {
936 | // continue regardless of error
937 | }
938 | }
939 | }
940 | }
941 |
--------------------------------------------------------------------------------
/src/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Downloader';
2 | export * from './InstaTouch';
3 |
--------------------------------------------------------------------------------
/src/entry.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-destructuring */
2 | /* eslint-disable no-param-reassign */
3 | /* eslint-disable no-throw-literal */
4 | /* eslint-disable no-restricted-syntax */
5 | import { tmpdir } from 'os';
6 | import { readFile, writeFile, unlink } from 'fs';
7 | import { fromCallback } from 'bluebird';
8 | import { forEachLimit } from 'async';
9 | import { InstaTouch } from './core';
10 | import {
11 | Constructor,
12 | ScrapeType,
13 | Options,
14 | Result,
15 | UserMetaFromWebApi,
16 | PostMetaFromWebApi,
17 | History,
18 | HistoryItem,
19 | UserStories,
20 | UserReelsFeed,
21 | } from './types';
22 | import CONST from './constant';
23 |
24 | const INIT_OPTIONS = {
25 | id: '',
26 | count: 50,
27 | download: false,
28 | asyncDownload: 5,
29 | mediaType: 'all',
30 | proxy: [],
31 | session: [],
32 | filepath: process.cwd(),
33 | filetype: 'na',
34 | progress: false,
35 | userAgent: CONST.userAgent,
36 | queryHash: '',
37 | url: '',
38 | cli: false,
39 | timeout: 0,
40 | endCursor: '',
41 | zip: false,
42 | bulk: true,
43 | headers: {},
44 | };
45 |
46 | /**
47 | * Load proxys from a file
48 | * @param file
49 | */
50 | const proxyFromFile = async (file: string) => {
51 | try {
52 | const data = (await fromCallback((cb) => readFile(file, { encoding: 'utf-8' }, cb))) as string;
53 | const proxyList = data.split('\n');
54 | if (!proxyList.length) {
55 | throw new Error('Proxy file is empty');
56 | }
57 | return proxyList;
58 | } catch (error) {
59 | throw error.message;
60 | }
61 | };
62 |
63 | const validateFullProfileUrl = (constructor: Constructor, input: string) => {
64 | if (!/^https:\/\/www.instagram.com\/[\w.+]+\/?$/.test(input)) {
65 | if (/instagram.com\/(p|reel)\//.test(input)) {
66 | constructor.url = `https://www.instagram.com/${input.split(/instagram.com\/(p|reel)\//)[1].split('/')[1]}/?__a=1&__d=dis`;
67 | } else {
68 | constructor.url = `https://www.instagram.com/${input}/?__a=1&__d=dis`;
69 | }
70 | } else {
71 | constructor.url = `${input}?__a=1&__d=dis`;
72 | constructor.input = input.split('instagram.com/')[1].split('/')[0];
73 | }
74 | };
75 |
76 | const validatePostUrl = (constructor: Constructor, input: string) => {
77 | if (!/(https?:\/\/(www\.)?)?instagram\.com(\/(p|reel)\/[\w-]+\/?)/.test(input)) {
78 | if (/instagram.com\/(p|reel)\//.test(input)) {
79 | constructor.url = `https://www.instagram.com/p/${input.split(/instagram.com\/(p|reel)\//)[1].split('/')[1]}/?__a=1&__d=dis`;
80 | } else {
81 | constructor.url = `https://www.instagram.com/p/${input}/?__a=1&__d=dis`;
82 | }
83 | } else {
84 | constructor.url = `${input}?__a=1&__d=dis`;
85 | constructor.input = input.split(/instagram.com\/(p|reel)\//)[2].split('/')[0];
86 | }
87 | };
88 |
89 | const promiseScraper = async (input: string, type: ScrapeType, options?: Options): Promise => {
90 | if (options && typeof options !== 'object') {
91 | throw new TypeError('Object is expected');
92 | }
93 | const constructor: Constructor = { ...INIT_OPTIONS, ...options, ...{ scrapeType: type, input } };
94 | switch (type) {
95 | case 'user':
96 | case 'stories':
97 | validateFullProfileUrl(constructor, input);
98 | break;
99 | case 'hashtag':
100 | constructor.url = `https://www.instagram.com/explore/tags/${input}/?__a=1&__d=dis`;
101 | break;
102 | case 'location':
103 | constructor.url = `https://www.instagram.com/explore/locations/${input}/?__a=1&__d=dis`;
104 | break;
105 | case 'comments':
106 | case 'likers':
107 | validatePostUrl(constructor, input);
108 | break;
109 | case 'followers':
110 | case 'following':
111 | options!.session = options!.session && [options!.session as string];
112 | validateFullProfileUrl(constructor, input);
113 |
114 | break;
115 | default:
116 | break;
117 | }
118 |
119 | const scraper = new InstaTouch(constructor);
120 |
121 | const result: Result = await scraper.startScraper();
122 | return result;
123 | };
124 |
125 | export const user = async (input: string, options?: Options): Promise => promiseScraper(input, 'user', options);
126 | export const hashtag = async (input: string, options?: Options): Promise => promiseScraper(input, 'hashtag', options);
127 | export const location = async (input: string, options?: Options): Promise => promiseScraper(input, 'location', options);
128 | export const comments = async (input: string, options?: Options): Promise => promiseScraper(input, 'comments', options);
129 | export const likers = async (input: string, options?: Options): Promise => promiseScraper(input, 'likers', options);
130 | export const followers = async (input: string, options?: Options): Promise => promiseScraper(input, 'followers', options);
131 | export const following = async (input: string, options?: Options): Promise => promiseScraper(input, 'following', options);
132 |
133 | export const getUserMeta = async (input: string, options?: Options): Promise => {
134 | if (options && typeof options !== 'object') {
135 | throw new TypeError('Object is expected');
136 | }
137 | const constructor: Constructor = { ...INIT_OPTIONS, ...options, ...{ scrapeType: 'user_meta', input } };
138 | const scraper = new InstaTouch(constructor);
139 |
140 | validateFullProfileUrl(constructor, input);
141 | const result = await scraper.getUserMeta(constructor.url);
142 | return result;
143 | };
144 |
145 | export const getPostMeta = async (input: string, options?: Options): Promise => {
146 | if (options && typeof options !== 'object') {
147 | throw new TypeError('Object is expected');
148 | }
149 | const constructor: Constructor = { ...INIT_OPTIONS, ...options, ...{ scrapeType: 'post_meta', input } };
150 | validatePostUrl(constructor, input);
151 | const scraper = new InstaTouch(constructor);
152 |
153 | const result = await scraper.getPostMeta(constructor.url);
154 | return result;
155 | };
156 |
157 | export const getStories = async (input: string, options?: Options): Promise => {
158 | if (options && typeof options !== 'object') {
159 | throw new TypeError('Object is expected');
160 | }
161 | const constructor: Constructor = { ...INIT_OPTIONS, ...options, ...{ scrapeType: 'post_meta', input } };
162 | validateFullProfileUrl(constructor, input);
163 | const scraper = new InstaTouch(constructor);
164 |
165 | const userMeta = await scraper.getUserMeta(constructor.url);
166 | const result = await scraper.getStories(userMeta.graphql.user.id);
167 | return { ...result, id: userMeta.graphql.user.id };
168 | };
169 |
170 | /**
171 | * Get user reels feed
172 | * @param input
173 | * @param options
174 | * @returns
175 | */
176 | export const getUserReels = async (input: string, options?: Options): Promise => {
177 | if (options && typeof options !== 'object') {
178 | throw new TypeError('Object is expected');
179 | }
180 | const constructor: Constructor = { ...INIT_OPTIONS, ...options, ...{ scrapeType: 'post_meta', input } };
181 | validateFullProfileUrl(constructor, input);
182 | const scraper = new InstaTouch(constructor);
183 |
184 | const userMeta = await scraper.getUserMeta(constructor.url);
185 | const result = await scraper.getUserReels(userMeta.graphql.user.id, constructor.count, constructor.endCursor!);
186 | return result;
187 | };
188 |
189 | // eslint-disable-next-line no-unused-vars
190 | export const history = async (input: string, options?: Options) => {
191 | let store: string;
192 |
193 | const historyPath = process.env.SCRAPING_FROM_DOCKER ? '/usr/app/files' : options?.historyPath || tmpdir();
194 | try {
195 | store = (await fromCallback((cb) => readFile(`${historyPath}/ig_history.json`, { encoding: 'utf-8' }, cb))) as string;
196 | } catch (error) {
197 | throw `History file doesn't exist`;
198 | }
199 | const historyStore: History = JSON.parse(store);
200 |
201 | if (options?.remove) {
202 | const split = options.remove.split(':');
203 | const type = split[0];
204 |
205 | if (type === 'all') {
206 | const remove: any = [];
207 | for (const key of Object.keys(historyStore)) {
208 | remove.push(fromCallback((cb) => unlink(historyStore[key].file_location, cb)));
209 | }
210 | remove.push(fromCallback((cb) => unlink(`${historyPath}/ig_history.json`, cb)));
211 |
212 | await Promise.all(remove);
213 |
214 | return { message: `History was completely removed` };
215 | }
216 |
217 | const key = type !== 'trend' ? options.remove.replace(':', '_') : 'trend';
218 |
219 | if (historyStore[key]) {
220 | const historyFile = historyStore[key].file_location;
221 |
222 | await fromCallback((cb) => unlink(historyFile, cb));
223 |
224 | delete historyStore[key];
225 |
226 | await fromCallback((cb) => writeFile(`${historyPath}/ig_history.json`, JSON.stringify(historyStore), cb));
227 |
228 | return { message: `Record ${key} was removed` };
229 | }
230 | throw `Can't find record: ${key.split('_').join(' ')}`;
231 | }
232 | const table: HistoryItem[] = [];
233 | for (const key of Object.keys(historyStore)) {
234 | table.push(historyStore[key]);
235 | }
236 | return { table };
237 | };
238 |
239 | interface Batcher {
240 | scrapeType: string;
241 | input: string;
242 | by_user_id?: boolean;
243 | }
244 |
245 | const batchProcessor = (batch: Batcher[], options: Options): Promise => {
246 | return new Promise((resolve) => {
247 | console.log('Instagram Bulk Scraping Started');
248 | const result: any[] = [];
249 | forEachLimit(
250 | batch,
251 | options.asyncBulk || 5,
252 | async (item) => {
253 | switch (item.scrapeType) {
254 | case 'user':
255 | try {
256 | const output = await user(item.input, { ...{ bulk: true }, ...options });
257 | result.push({ type: item.scrapeType, input: item.input, completed: true, scraped: output.collector.length });
258 | console.log(`Scraping completed: ${item.scrapeType} ${item.input}`);
259 | } catch (error) {
260 | result.push({ type: item.scrapeType, input: item.input, completed: false });
261 | console.log(`Error while scraping: ${item.input}`);
262 | }
263 | break;
264 | case 'hashtag':
265 | try {
266 | const output = await hashtag(item.input, { ...{ bulk: true }, ...options });
267 | result.push({ type: item.scrapeType, input: item.input, completed: true, scraped: output.collector.length });
268 | console.log(`Scraping completed: ${item.scrapeType} ${item.input}`);
269 | } catch (error) {
270 | result.push({ type: item.scrapeType, input: item.input, completed: false });
271 | console.log(`Error while scraping: ${item.input}`);
272 | }
273 | break;
274 | case 'location':
275 | try {
276 | const output = await location(item.input, { ...{ bulk: true }, ...options });
277 | result.push({ type: item.scrapeType, input: item.input, completed: true, scraped: output.collector.length });
278 | console.log(`Scraping completed: ${item.scrapeType} ${item.input}`);
279 | } catch (error) {
280 | result.push({ type: item.scrapeType, input: item.input, completed: false });
281 | console.log(`Error while scraping: ${item.input}`);
282 | }
283 | break;
284 | case 'comments':
285 | try {
286 | const output = await comments(item.input, { ...{ bulk: true }, ...options });
287 | result.push({ type: item.scrapeType, input: item.input, completed: true, scraped: output.collector.length });
288 | console.log(`Scraping completed: ${item.scrapeType} ${item.input}`);
289 | } catch (error) {
290 | result.push({ type: item.scrapeType, input: item.input, completed: false });
291 | console.log(`Error while scraping: ${item.input}`);
292 | }
293 | break;
294 | case 'likers':
295 | try {
296 | const output = await likers(item.input, { ...{ bulk: true }, ...options });
297 | result.push({ type: item.scrapeType, input: item.input, completed: true, scraped: output.collector.length });
298 | console.log(`Scraping completed: ${item.scrapeType} ${item.input}`);
299 | } catch (error) {
300 | result.push({ type: item.scrapeType, input: item.input, completed: false });
301 | console.log(`Error while scraping: ${item.input}`);
302 | }
303 | break;
304 | default:
305 | break;
306 | }
307 | },
308 | () => {
309 | resolve(result);
310 | },
311 | );
312 | });
313 | };
314 |
315 | export const fromfile = async (input: string, options: Options) => {
316 | let inputFile: string;
317 | try {
318 | inputFile = (await fromCallback((cb) => readFile(input, { encoding: 'utf-8' }, cb))) as string;
319 | } catch (error) {
320 | throw `Can't find fle: ${input}`;
321 | }
322 | const batch: Batcher[] = inputFile
323 | .split('\n')
324 | .filter((item) => item.indexOf('##') === -1 && item.length)
325 | .map((item) => {
326 | item = item.replace(/\s/g, '');
327 | if (item.indexOf('#') > -1) {
328 | return {
329 | scrapeType: 'hashtag',
330 | input: item.split('#')[1],
331 | };
332 | }
333 | if (item.indexOf('location|') > -1) {
334 | return {
335 | scrapeType: 'location',
336 | input: item.split('|')[1],
337 | };
338 | }
339 | if (item.indexOf('comments|') > -1) {
340 | return {
341 | scrapeType: 'comments',
342 | input: item.split('|')[1],
343 | by_user_id: true,
344 | };
345 | }
346 | if (item.indexOf('likers|') > -1) {
347 | return {
348 | scrapeType: 'likers',
349 | input: item.split('|')[1],
350 | by_user_id: true,
351 | };
352 | }
353 | return {
354 | scrapeType: 'user',
355 | input: item,
356 | };
357 | });
358 | if (!batch.length) {
359 | throw `File is empty: ${input}`;
360 | }
361 |
362 | if (options?.proxyFile) {
363 | options.proxy = await proxyFromFile(options?.proxyFile);
364 | }
365 |
366 | const result = await batchProcessor(batch, options);
367 |
368 | return { table: result };
369 | };
370 |
--------------------------------------------------------------------------------
/src/helpers/Bar.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /* eslint-disable */
3 | import ProgressBar from 'progress';
4 |
5 | export class MultipleBar {
6 | constructor() {
7 | this.stream = process.stderr;
8 | this.cursor = 1;
9 | this.bars = [];
10 | this.terminates = 0;
11 | }
12 |
13 | newBar(schema, options) {
14 | options.stream = this.stream;
15 | const bar = new ProgressBar(schema, options);
16 | this.bars.push(bar);
17 | const index = this.bars.length - 1;
18 |
19 | // alloc line
20 | this.move(index);
21 | this.stream.write('\n');
22 | this.cursor += 1;
23 |
24 | // replace original
25 | const self = this;
26 | bar.otick = bar.tick;
27 | bar.oterminate = bar.terminate;
28 | bar.tick = (value, options) => {
29 | self.tick(index, value, options);
30 | };
31 | bar.terminate = () => {
32 | self.terminates += 1;
33 | if (self.terminates === self.bars.length) {
34 | self.terminate();
35 | }
36 | };
37 |
38 | return bar;
39 | }
40 |
41 | terminate() {
42 | this.move(this.bars.length);
43 | this.stream.clearLine();
44 | if (!this.stream.isTTY) return;
45 | this.stream.cursorTo(0);
46 | }
47 |
48 | move(index) {
49 | if (!this.stream.isTTY) return;
50 | this.stream.moveCursor(0, index - this.cursor);
51 | this.cursor = index;
52 | }
53 |
54 | tick(index, value, options) {
55 | const bar = this.bars[index];
56 | if (bar) {
57 | this.move(index);
58 | bar.otick(value, options);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/helpers/Random.ts:
--------------------------------------------------------------------------------
1 | export const randomString = (len: number) => {
2 | let text = '';
3 | const char_list = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
4 | for (let i = 0; i < len; i += 1) {
5 | text += char_list.charAt(Math.floor(Math.random() * char_list.length));
6 | }
7 | return text;
8 | };
9 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Bar';
2 | export * from './Random';
3 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './entry';
2 |
--------------------------------------------------------------------------------
/src/types/Cli.ts:
--------------------------------------------------------------------------------
1 | import { ScrapeType } from '.';
2 |
3 | export interface HistoryItem {
4 | type: ScrapeType;
5 | input: string;
6 | collected_items: number;
7 | last_change: Date;
8 | file_location: string;
9 | }
10 |
11 | export interface History {
12 | [key: string]: HistoryItem;
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/Downloader.ts:
--------------------------------------------------------------------------------
1 | import { PostCollector } from '.';
2 |
3 | export interface DownloaderConstructor {
4 | progress: boolean;
5 | proxy: string[] | string;
6 | userAgent: string;
7 | filepath: string;
8 | bulk: boolean;
9 | }
10 |
11 | export interface ZipValues {
12 | zip: boolean;
13 | folder: string;
14 | collector: PostCollector[];
15 | fileName: string;
16 | asyncDownload: number;
17 | }
18 |
--------------------------------------------------------------------------------
/src/types/Ig.ts:
--------------------------------------------------------------------------------
1 | export interface Main {
2 | config: {
3 | csrf_token: string;
4 | };
5 | graphql: {
6 | [key in Key]: GraphQl[];
7 | };
8 | }
9 |
10 | export interface GraphQl {
11 | graphql: T;
12 | }
13 |
14 | export interface GraphQlResponse {
15 | data: T;
16 | }
17 |
18 | export interface UserMetaFromWebApi {
19 | graphql: User;
20 | }
21 |
22 | export interface PostMetaFromWebApi {
23 | items: {
24 | shortcode_media: PostMeta;
25 | }[];
26 | }
27 |
28 | export interface ApiV2Response {
29 | data: T;
30 | num_results: number;
31 | more_available: boolean;
32 | next_max_id: string;
33 | }
34 |
35 | export interface UserReelsFeed {
36 | items: {
37 | media: {
38 | taken_at: number;
39 | pk: string;
40 | id: string;
41 | device_timestamp: number;
42 | media_type: number;
43 | };
44 | }[];
45 | paging_info: {
46 | max_id: string;
47 | more_available: boolean;
48 | };
49 | status: string;
50 | }
51 |
52 | export interface UserStories {
53 | id: string;
54 | reels_media: {
55 | id: number;
56 | latest_reel_media: number;
57 | expiring_at: number;
58 | items: {
59 | taken_at: number;
60 | media_type: number;
61 | }[];
62 | }[];
63 | reels: {
64 | id: number;
65 | latest_reel_media: number;
66 | expiring_at: number;
67 | items: {
68 | taken_at: number;
69 | media_type: number;
70 | }[];
71 | }[];
72 | status: string;
73 | }
74 |
75 | export interface User {
76 | user: {
77 | biography: string;
78 | id: string;
79 | edge_owner_to_timeline_media: Edges;
80 | edge_followed_by: Edges;
81 | edge_follow: Edges;
82 | external_url: string;
83 | full_name: string;
84 | business_category_name: string;
85 | category_id: string;
86 | overall_category_name: string;
87 | is_private: boolean;
88 | is_verified: boolean;
89 | profile_pic_url: string;
90 | profile_pic_url_hd: string;
91 | username: string;
92 | };
93 | }
94 |
95 | export interface Location {
96 | location: {
97 | name: string;
98 | id: string;
99 | has_public_page: string;
100 | lat: number;
101 | lng: number;
102 | blurb: string;
103 | website: string;
104 | profile_pic_url: string;
105 | edge_location_to_media: Edges;
106 | };
107 | }
108 |
109 | export interface Hashtag {
110 | hashtag: {
111 | description: string;
112 | id: string;
113 | name: string;
114 | edge_hashtag_to_media: Edges;
115 | };
116 | }
117 |
118 | export interface Comments {
119 | shortcode_media: {
120 | shortcode: string;
121 | id: string;
122 | edge_media_to_parent_comment: Edges;
123 | };
124 | }
125 |
126 | export interface Likers {
127 | shortcode_media: {
128 | shortcode: string;
129 | id: string;
130 | edge_liked_by: Edges;
131 | };
132 | }
133 |
134 | export interface Edges {
135 | count: number;
136 | page_info: {
137 | has_next_page: boolean;
138 | end_cursor: string;
139 | };
140 | edges: Post[];
141 | }
142 |
143 | export type PostType = 'GraphSidecar' | 'GraphVideo' | 'GraphImage';
144 |
145 | export interface Post {
146 | node: PostMeta;
147 | }
148 |
149 | export interface CommentsMeta {
150 | id: string;
151 | text: string;
152 | created_at: number;
153 | did_report_as_spam: boolean;
154 | owner: {
155 | id: number;
156 | is_verified: boolean;
157 | profile_pic_url: string;
158 | username: string;
159 | };
160 | edge_liked_by: {
161 | count: number;
162 | };
163 | edge_threaded_comments: {
164 | count: number;
165 | };
166 | }
167 |
168 | export interface PostMeta {
169 | __typename: PostType;
170 | id: string;
171 | shortcode: string;
172 | dimensions: { height: number; width: number };
173 | display_url: string;
174 | gating_info: string;
175 | video_url: string;
176 | video_duration: number;
177 | edge_media_to_tagged_user: {
178 | edges: {
179 | node: {
180 | user: { full_name: string; id: number; is_verified: boolean; profile_pic_url: string; username: string };
181 | x: number;
182 | y: number;
183 | };
184 | }[];
185 | };
186 | caption_is_edited: boolean;
187 | has_ranked_comments: boolean;
188 | fact_check_overall_rating: string;
189 | fact_check_information: string;
190 | media_preview: string;
191 | owner: { id: string; username: string; is_verified: boolean; profile_pic_url: string };
192 | is_video: boolean;
193 | accessibility_caption: string;
194 | edge_media_to_caption: {
195 | edges: { node: { text: string } }[];
196 | };
197 | edge_media_to_comment: { count: number };
198 | comments_disabled: boolean;
199 | taken_at_timestamp: number;
200 | edge_liked_by: { count: number };
201 | edge_media_preview_like: { count: number };
202 | location: { id: string; has_public_page: boolean; name: string; slug: string };
203 | thumbnail_src: string;
204 | thumbnail_resources: { src: string; config_width: number; config_height: number }[];
205 | felix_profile_grid_crop: string;
206 | video_view_count: number;
207 | text: string;
208 | created_at: number;
209 | did_report_as_spam: boolean;
210 | edge_threaded_comments: { count: number };
211 | profile_pic_url: string;
212 | full_name: string;
213 | is_verified: boolean;
214 | is_private: boolean;
215 | username: string;
216 | }
217 |
--------------------------------------------------------------------------------
/src/types/InstaTouch.ts:
--------------------------------------------------------------------------------
1 | import { SocksProxyAgent } from 'socks-proxy-agent';
2 | import { Edges } from './Ig';
3 |
4 | export type ScrapeType = 'user' | 'hashtag' | 'location' | 'comments' | 'likers' | 'followers' | 'following' | 'user_meta' | 'post_meta' | 'stories';
5 |
6 | export type MediaType = 'image' | 'video' | 'all';
7 |
8 | export interface Proxy {
9 | socks: boolean;
10 | proxy: string | SocksProxyAgent;
11 | }
12 |
13 | export interface Constructor {
14 | download: boolean;
15 | filepath: string;
16 | filetype: string;
17 | proxy: string[] | string;
18 | session: string[] | string;
19 | asyncDownload: number;
20 | cli: boolean;
21 | progress?: boolean;
22 | bulk?: boolean;
23 | input: string;
24 | count: number;
25 | zip?: boolean;
26 | scrapeType: ScrapeType;
27 | by_user_id?: boolean;
28 | store_history?: boolean;
29 | headers: {};
30 | userAgent: string;
31 | test?: boolean;
32 | noWaterMark?: boolean;
33 | fileName?: string;
34 | sessionId?: string[];
35 | mediaType: string;
36 | queryHash: string;
37 | id: string;
38 | filename?: string;
39 | url: string;
40 | timeout: number;
41 | endCursor?: string;
42 | historyPath?: string;
43 | extractVideoUrl?: boolean;
44 | user_id?: boolean;
45 | }
46 |
47 | export interface Options {
48 | timeout?: number;
49 | proxyFile?: string;
50 | proxy?: string[] | string;
51 | session?: string[] | string;
52 | mediaType?: MediaType;
53 | download?: boolean;
54 | asyncDownload?: number;
55 | filepath?: string;
56 | headers?: {};
57 | filetype?: string;
58 | progress?: boolean;
59 | count?: number;
60 | userAgent?: string;
61 | remove?: string;
62 | filename?: string;
63 | endCursor?: string;
64 | historyPath?: string;
65 | asyncBulk?: number;
66 | bulk?: boolean;
67 | extractVideoUrl?: boolean;
68 | user_id?: boolean;
69 | }
70 |
71 | export interface PostCollector {
72 | id: string;
73 | shortcode?: string;
74 | type?: string;
75 | is_video?: boolean;
76 | dimension?: { height: number; width: number };
77 | display_url?: string;
78 | thumbnail_src?: string;
79 | video_url?: string;
80 | owner?: { id: string; username: string };
81 | description?: string;
82 | comments?: number;
83 | likes?: number;
84 | views?: number;
85 | comments_disabled?: boolean;
86 | taken_at_timestamp?: number;
87 | tagged_users?: {
88 | user: {
89 | full_name: string;
90 | id: number;
91 | is_verified: boolean;
92 | profile_pic_url: string;
93 | username: string;
94 | };
95 | x: number;
96 | y: number;
97 | }[];
98 | location?: { id: string; has_public_page: boolean; name: string; slug: string };
99 | hashtags?: string[];
100 | mentions?: string[];
101 | text?: string;
102 | created_at?: number;
103 | did_report_as_spam?: boolean;
104 | is_private?: boolean;
105 | is_verified?: boolean;
106 | username?: string;
107 | full_name?: string;
108 | profile_pic_url?: string;
109 | downloaded?: boolean;
110 | repeated?: boolean;
111 | }
112 |
113 | export interface CommentCollector {
114 | id: string;
115 | text: string;
116 | created_at: number;
117 | did_report_as_spam: boolean;
118 | owner: {
119 | id: number;
120 | is_verified: boolean;
121 | profile_pic_url: string;
122 | username: string;
123 | };
124 | likes: number;
125 | comments: number;
126 | }
127 | export interface Result {
128 | has_more: boolean;
129 | count: number;
130 | end_cursor: string;
131 | collector: PostCollector[];
132 | original: Edges;
133 | zip?: string;
134 | id?: string;
135 | json?: string;
136 | csv?: string;
137 | auth_error?: boolean;
138 | }
139 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './InstaTouch';
2 | export * from './Ig';
3 | export * from './Downloader';
4 | export * from './Cli';
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "emitDecoratorMetadata": true,
5 | "lib": ["es7"],
6 | "module": "commonjs",
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ES2017",
10 | "removeComments": true,
11 | "noImplicitAny": false,
12 | "moduleResolution": "node",
13 | "declaration": true,
14 | "noUnusedLocals": true,
15 | "outDir": "./build",
16 | "baseUrl": "src",
17 | "strict": true
18 | },
19 | "include": ["src/**/*.*"],
20 | "exclude": ["node_modules", "src/**/*.test.ts", "**/__mocks__/*", "**/__mockData__/"]
21 | }
22 |
--------------------------------------------------------------------------------