├── .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 | ![NPM](https://img.shields.io/npm/l/instatouch.svg?style=for-the-badge) ![npm](https://img.shields.io/npm/v/instatouch.svg?style=for-the-badge) ![Codacy grade](https://img.shields.io/codacy/grade/037f8049f7e048a2b03a95fda8863f39.svg?style=for-the-badge) 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 | Buy Me A Coffee 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 | ![Demo](https://i.imgur.com/DDRmH2y.gif) 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 | ![Demo](https://i.imgur.com/D9sH95B.png) 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 | ![Demo](https://i.imgur.com/9Gt4xgL.png) 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 | ![History](https://i.imgur.com/VnDKh72.png) 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 | --------------------------------------------------------------------------------