├── .DS_Store ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── download_post_list.txt ├── index.js ├── package-lock.json ├── package.json └── user_config_DEFAULT.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephrcox/easy-reddit-downloader/53c08152e0480b886035c3ef6c8e55ecc42d9040/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | buy_me_a_coffee: josephrcox 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | /logs/* 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | 129 | # user downloads 130 | downloads 131 | 132 | # testing 133 | testing_folder 134 | 135 | # config 136 | user_config.json 137 | reddit_post_downloader.code-workspace 138 | 139 | .DS_Store 140 | .DS_Store 141 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joseph Cox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy Reddit Post Downloader 2 | *A NodeJS-based Reddit post downloader utilizing only the public Reddit API, **no OAuth or login required!*** 3 | 4 | Screenshot 2022-12-22 at 2 51 23 PM 5 | 6 | ## Features 7 | 1. Downloading of all post types from any public subreddit that you would like. This includes downloading YouTube videos from Reddit posts! 8 | 2. No OAuth or login required! 🔓 9 | 3. **SUPER FAST** 🏃 with an average speed of 20 posts/second. (your mileage may vary) 10 | 4. Downloading of user accounts 🙋‍♂️ 11 | 5. Ability to run the script indefinitely at your set interval (run daily, hourly, or even every 30 seconds) - Great for catching new posts before they are removed. 12 | 6. Automatic HTML file creation for link posts. 🔗 13 | 7. Very few Node dependencies so it's fast to clone and run. 14 | 8. Customizable file naming and file structure. 📁 15 | 9. View top comments and nested comments in text/self posts, all stored locally on your computer. 16 | 17 | ## Setup & Usage 18 | 1. Clone or download this repo 19 | 2. Install NodeJS from https://nodejs.org/en/download/ (if you don't already have it) 20 | 3. Install all of the node dependencies. `cd` into the repo and type the command below. 21 | 22 | `npm i` 23 | 24 | Then, you are ready to go! Type either of the commands below to start it up. 25 | 26 | `node index.js` or `npm run start` 27 | 28 | After the script begins, you will be asked a few questions about what you want to download. Fill out the questions (be careful not to have any typos) and it will download all post types from the subreddit(s) you entered with your sorting options. 29 | 30 | ### user_config.json 31 | After the first launch, a file called `user_config.json` will be created for you. You can modify this file at anytime to set your personal preferences. This will not be reset during Git pulls or updates, so it is perfect if you want a very specific setup for a long time. 32 | 33 | If you mess up the user_config.json file, there will always be a default version called `user_config_DEFAULT.json'. This may change in the future to enable more features. 34 | 35 | #### File-naming-scheme 36 | This is the naming scheme for the files that are downloaded. To reduce the chance of duplicate posts and errors, we recommend using the default naming scheme. 37 | 38 | Problems can happen if you just show the author, because if that author writes multiple posts, then it would overwrite the previous posts. Similiar problems can happen if you attempt to only use the post title in subreddits that have a lot of posts with the same names. 39 | 40 | ## Post types 41 | ### Text/self posts 42 | All self posts are stored as `md` (markdown) files which contain the full title, the author of the post, and the post description. 43 | 44 | It also downloads the parent comments on the post, as well as the top nested comments beneathe that. Here is an example snippet from one post: 45 | ``` 46 | How would you feel about a feature where if someone upvotes a crosspost, the original post is upvoted automatically? by Ka1- 47 | ------------------------------------------------ 48 | 49 | --COMMENTS-- 50 | 51 | samoyedboi: 52 | what about subs that crosspost from other subs to mock them? 53 | > StoryDrive: 54 | > Yeah, I was hoping someone had already commented this. I'd hate to upvote a critique of something I disagree with only for the despicable thing to also get upvoted. 55 | 56 | 57 | 58 | [deleted]: 59 | The karmaceutical companies would be outraged. 60 | > TheEnKrypt: 61 | > Let's hope Big Karma doesn't hear about this then. We really need to do something about Unidan Shkreli smh. 62 | 63 | 64 | ``` 65 | 66 | ### Media posts 67 | Many media formats are supported including JPG, PNG, JPEG, GIF, GIFv, MP4, and MOV. These are downloaded in their original format, except for GIF and GIFv which are converted in real-time to MP4 format. There is sometimes a slight loss in quality here but its not noticble in my testing, with the added benefit of being able to scrub through the GIF. 68 | 69 | ### Link posts 70 | Link posts are saved as HTML files that can be opened in a web browser. The HTML file is basic and immediately redirects the user with Javascript to the webpage of the link. 71 | 72 | For example, if there was a post that went to `https://www.google.com/` then the HTML file contents would be this: 73 | 74 | ``` 75 | 76 | 77 | 80 | 81 | 82 | ``` 83 | 84 | ### YouTube videos 85 | Starting with version v0.3.0, YouTube videos can be downloaded in MP4 format. You must have ffmpeg installed on your computer for this to work, download it here - https://ffmpeg.org/. 86 | 87 | You also must set `download_youtube_videos_experimental` to true in your `user_config.json` file. 88 | 89 | ## Downloading users 90 | With the release of v0.2.0, I added the ability to download posts from a specific user. 91 | 92 | To download a user, enter the username in this format when asked what subreddits you would like to download: 93 | `u/elonmusk`. So, for example if you would like to download a few subs and also a couple of users, this is what it could look like: 94 | ``` 95 | Welcome to Reddit Post Downloader! 96 | Contribute @ https://github.com/josephrcox/easy-reddit-downloader 97 | 98 | ✔ Which subreddits or users would you like to download? You may submit multiple separated by commas (no spaces). … u/elonmusk,pics,u/spez 99 | ``` 100 | Note: at this time, comments from a specific user are not supported. If you would like this, please submit an issue with the `enhancement` tag. 101 | 102 | ## Downloading from a post list (download specific URLs) 103 | With version v0.2.1, I added the ability to download specific posts from a list of URLs. 104 | 105 | To download a list of posts: 106 | 1. Enter your post list in `download_post_list.txt` in the root directory of the project. Make sure to follow the format as shown in the first few comment lines of the file. 107 | 2. Go into `user_config.json` and set `download_post_list_options` > `enabled: true`. 108 | 3. Run the application as normal. 109 | 110 | You may also choose to download these specific posts at a set interval using the `repeatForever` boolean and `timeBetweenRuns` integer in the `download_post_list_options` object in `user_config.json`. Make sure to enter the timeBetweenRuns in milliseconds. 111 | 112 | ## FAQ 113 | ### Is there any authentication needed? Do I need to login? Do they know that I am downloading all of these posts? 114 | No. This uses the public Reddit API provided by adding `.json` to regular Reddit pages. 115 | This means no authorization is required and it's easy for anyone with an internet connection to use. 116 | 117 | This also means that besides linking to your IP address, Reddit has no way of knowing that you are downloading all of these posts. 118 | 119 | ### What post types are supported and should download? 120 | - Any text/self post 121 | - Any image (posted directly on reddit, imgur, or other services) 122 | - Most video or image-video formats (tested with MP4, GIF, GIFV which converts to MP4, MOV). These can fail if they take too long to load, are from a third-party site, or are deleted. 123 | - Any link post (which generates an HTML file that redirects the user to the link page) 124 | 125 | ### Do I need to enter the "/r/" or "r/" before a subreddit name? 126 | No. 127 | 128 | ### Why am I asked how many posts to dig through? What does this number mean? 129 | The higher the number the.... 130 | - More posts you will download. 131 | - More data you will use (keep in mind if on a data-limited plan). 132 | - Less subreddits you will download/second. 133 | 134 | *Just because you put 1000, you may not get 1000 posts. There are lots of reasons why this can happen, and it should not be treated as a bug. The average "success rate" right now is about 70%. So if you request 1000 posts, you will likely get 700. If you want 1000 or more posts, then it may be wise to request 1500 or so.* 135 | 136 | ### Is there any tracking with what I download? 137 | No. There is no Google analytics or other tracking that goes into the posts or subreddits that you choose to download. 138 | 139 | ### Can this get me banned or restricted from Reddit? 140 | No, but there are no promises or guarantees. This is a public API and is not against any Reddit rules to consume for personal use. 141 | 142 | ### Can I run this without NodeJS installed? 143 | No. It is required, and there is no website or web interface for this. 144 | 145 | ### Can I run this on my computer? 146 | Any computer that can run NodeJS can run this, although a stable internet connection and room for the posts to download will decrease the chance of random errors. If you face problems, submit an issue! 147 | 148 | ### Why did you (josephrcox) make this? 149 | In the past, I have wanted to download subreddits for offline consumption. This makes it easy to do so and does not need OAUTH which I found annoying with many other tools. I also just wanted a fun tiny project to work on during vacation so I spent a couple of hours making and refining this. 150 | 151 | ## Upcoming features 152 | Please see the issues tab to see upcoming features and vote on what you want the most by commenting! 153 | 154 | ## Example log 155 | ``` 156 | Welcome to Reddit Post Downloader! 157 | Contribute @ https://github.com/josephrcox/easy-reddit-downloader 158 | ALERT: A new version (v1.1.9) is available. 159 | Please update to the latest version with 'git pull'. 160 | 161 | What subreddit would you like to download? You may submit multiple separated by commas (no spaces). 162 | askreddit,pics 163 | How many posts do you want to go through?(more posts = more downloads, but takes longer) 164 | 10 165 | How would you like to sort? (top, new, hot, rising, controversial) 166 | top 167 | What time period? (hour, day, week, month, year, all) 168 | all 169 | How often should this be run? 170 | Manually enter number other than the options below for manual entry, i.e. "500" for every 0.5 second 171 | 1.) one time 172 | 2.) every 0.5 minute 173 | 3.) every minute 174 | 4.) every 5 minutes 175 | 5.) every 30 minutes 176 | 6.) every hour 177 | 7.) every 3 hours 178 | 8.) every day 179 | 1 180 | 181 | 182 | Requesting posts from 183 | https://www.reddit.com/r/askreddit/top/.json?sort=top&t=all&limit=10&after= 184 | 185 | 186 | Still downloading posts... (1/10) 187 | {"subreddit":"AskReddit","self":1,"media":0,"link":0,"failed":0} 188 | 189 | ------------------------------------------------ 190 | Still downloading posts... (2/10) 191 | {"subreddit":"AskReddit","self":2,"media":0,"link":0,"failed":0} 192 | 193 | ------------------------------------------------ 194 | Still downloading posts... (3/10) 195 | {"subreddit":"AskReddit","self":3,"media":0,"link":0,"failed":0} 196 | 197 | ------------------------------------------------ 198 | Still downloading posts... (4/10) 199 | {"subreddit":"AskReddit","self":4,"media":0,"link":0,"failed":0} 200 | 201 | ------------------------------------------------ 202 | Still downloading posts... (5/10) 203 | {"subreddit":"AskReddit","self":5,"media":0,"link":0,"failed":0} 204 | 205 | ------------------------------------------------ 206 | Still downloading posts... (6/10) 207 | {"subreddit":"AskReddit","self":6,"media":0,"link":0,"failed":0} 208 | 209 | ------------------------------------------------ 210 | Still downloading posts... (7/10) 211 | {"subreddit":"AskReddit","self":7,"media":0,"link":0,"failed":0} 212 | 213 | ------------------------------------------------ 214 | Still downloading posts... (8/10) 215 | {"subreddit":"AskReddit","self":8,"media":0,"link":0,"failed":0} 216 | 217 | ------------------------------------------------ 218 | Still downloading posts... (9/10) 219 | {"subreddit":"AskReddit","self":9,"media":0,"link":0,"failed":0} 220 | 221 | ------------------------------------------------ 222 | 🎉 All done downloading posts from AskReddit! 223 | {"subreddit":"AskReddit","self":10,"media":0,"link":0,"failed":0} 224 | 225 | 📈 Downloading took 3.823 seconds, at about 0.382 seconds/post 226 | 227 | 228 | Requesting posts from 229 | https://www.reddit.com/r/pics/top/.json?sort=top&t=all&limit=10&after= 230 | 231 | 232 | Still downloading posts... (1/10) 233 | {"subreddit":"pics","self":0,"media":0,"link":1,"failed":0} 234 | 235 | ------------------------------------------------ 236 | Still downloading posts... (2/10) 237 | {"subreddit":"pics","self":0,"media":0,"link":2,"failed":0} 238 | 239 | ------------------------------------------------ 240 | Still downloading posts... (3/10) 241 | {"subreddit":"pics","self":0,"media":1,"link":2,"failed":0} 242 | 243 | ------------------------------------------------ 244 | Still downloading posts... (4/10) 245 | {"subreddit":"pics","self":0,"media":2,"link":2,"failed":0} 246 | 247 | ------------------------------------------------ 248 | Still downloading posts... (5/10) 249 | {"subreddit":"pics","self":0,"media":3,"link":2,"failed":0} 250 | 251 | ------------------------------------------------ 252 | Still downloading posts... (6/10) 253 | {"subreddit":"pics","self":0,"media":4,"link":2,"failed":0} 254 | 255 | ------------------------------------------------ 256 | Still downloading posts... (7/10) 257 | {"subreddit":"pics","self":0,"media":5,"link":2,"failed":0} 258 | 259 | ------------------------------------------------ 260 | Still downloading posts... (8/10) 261 | {"subreddit":"pics","self":0,"media":6,"link":2,"failed":0} 262 | 263 | ------------------------------------------------ 264 | Still downloading posts... (9/10) 265 | {"subreddit":"pics","self":0,"media":7,"link":2,"failed":0} 266 | 267 | ------------------------------------------------ 268 | 🎉 All done downloading posts from pics! 269 | {"subreddit":"pics","self":0,"media":8,"link":2,"failed":0} 270 | 271 | 📈 Downloading took 1.62 seconds, at about 0.162 seconds/post 272 | What subreddit would you like to download? You may submit multiple separated by commas (no spaces). 273 | 274 | ``` 275 | -------------------------------------------------------------------------------- /download_post_list.txt: -------------------------------------------------------------------------------- 1 | # Below, please list any posts that you wish to download. # 2 | # They must follow this format below: # 3 | # https://www.reddit.com/r/gadgets/comments/ptt967/eu_proposes_mandatory_usbc_on_all_devices/ # 4 | # Lines with "#" at the start will be ignored (treated as comments). # -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const { version } = require('./package.json'); 3 | 4 | // NodeJS Dependencies 5 | const fs = require('fs'); 6 | const prompts = require('prompts'); 7 | const chalk = require('chalk'); 8 | const axios = require('axios'); 9 | 10 | const ytdl = require('ytdl-core'); 11 | const ffmpeg = require('fluent-ffmpeg'); 12 | 13 | let config = require('./user_config_DEFAULT.json'); 14 | 15 | // Variables used for logging 16 | let userLogs = ''; 17 | const logFormat = 'txt'; 18 | let date = new Date(); 19 | let date_string = `${date.getFullYear()} ${ 20 | date.getMonth() + 1 21 | } ${date.getDate()} at ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; 22 | let startTime = null; 23 | let lastAPICallForSubreddit = false; 24 | let currentAPICall = null; 25 | 26 | let currentSubredditIndex = 0; // Used to track which subreddit the user is downloading from 27 | let responseSize = -1; // Used to track the size of the response from the API call, aka how many posts are in the response 28 | 29 | // User-defined variables, these can be preset with the help of testingMode 30 | let timeBetweenRuns = 0; // in milliseconds, the time between runs. This is only used if repeatForever is true 31 | let subredditList = []; // List of subreddits in this format: ['subreddit1', 'subreddit2', 'subreddit3'] 32 | let numberOfPosts = -1; // How many posts to go through, more posts = more downloads, but takes longer 33 | let sorting = 'top'; // How to sort the posts (top, new, hot, rising, controversial) 34 | let time = 'all'; // What time period to sort by (hour, day, week, month, year, all) 35 | let repeatForever = false; // If true, the program will repeat every timeBetweenRuns milliseconds 36 | let downloadDirectory = ''; // Where to download the files to, defined when 37 | let downloadDirectoryBase = './downloads'; // Default download path, can be overridden 38 | const postDelayMilliseconds = 250; 39 | 40 | let currentUserAfter = ''; // Used to track the after value for the API call, this is used to get the next X posts 41 | 42 | // Default object to track the downloaded posts by type, 43 | // and the subreddit downloading from. 44 | let downloadedPosts = { 45 | subreddit: '', 46 | self: 0, 47 | media: 0, 48 | link: 0, 49 | failed: 0, 50 | skipped_due_to_duplicate: 0, 51 | skipped_due_to_fileType: 0, 52 | }; 53 | 54 | // Read the user_config.json file for user configuration options 55 | if (fs.existsSync('./user_config.json')) { 56 | config = require('./user_config.json'); 57 | checkConfig(); 58 | } else { 59 | // create ./user_config.json if it doesn't exist, by duplicating user_config_DEFAULT.json and renaming it 60 | fs.copyFile('./user_config_DEFAULT.json', './user_config.json', (err) => { 61 | if (err) throw err; 62 | log('user_config.json was created. Edit it to manage user options.', true); 63 | config = require('./user_config.json'); 64 | }); 65 | checkConfig(); 66 | } 67 | 68 | // check if download_post_list.txt exists, if it doesn't, create it 69 | if (!fs.existsSync('./download_post_list.txt')) { 70 | fs.writeFile('./download_post_list.txt', '', (err) => { 71 | if (err) throw err; 72 | 73 | let fileDefaultContent = `# Below, please list any posts that you wish to download. # \n# They must follow this format below: # \n# https://www.reddit.com/r/gadgets/comments/ptt967/eu_proposes_mandatory_usbc_on_all_devices/ # \n# Lines with "#" at the start will be ignored (treated as comments). #`; 74 | 75 | // write a few lines to the file 76 | fs.appendFile('./download_post_list.txt', fileDefaultContent, (err) => { 77 | if (err) throw err; 78 | log('download_post_list.txt was created with default content.', true); 79 | }); 80 | }); 81 | } 82 | 83 | // Testing Mode for developer testing. This enables you to hardcode 84 | // the variables above and skip the prompt. 85 | // To edit, go into the user_config.json file. 86 | const testingMode = config.testingMode; 87 | if (testingMode) { 88 | subredditList = config.testingModeOptions.subredditList; 89 | numberOfPosts = config.testingModeOptions.numberOfPosts; 90 | sorting = config.testingModeOptions.sorting; 91 | time = config.testingModeOptions.time; 92 | repeatForever = config.testingModeOptions.repeatForever; 93 | timeBetweenRuns = config.testingModeOptions.timeBetweenRuns; 94 | if (config.testingModeOptions.downloadDirectory) { 95 | downloadDirectoryBase = config.testingModeOptions.downloadDirectory; 96 | } 97 | } 98 | 99 | // Start actions 100 | console.clear(); // Clear the console 101 | log( 102 | chalk.cyan( 103 | '👋 Welcome to the easiest & most customizable Reddit Post Downloader!', 104 | ), 105 | false, 106 | ); 107 | log( 108 | chalk.yellow( 109 | '😎 Contribute @ https://github.com/josephrcox/easy-reddit-downloader', 110 | ), 111 | false, 112 | ); 113 | log( 114 | chalk.blue( 115 | '🤔 Confused? Check out the README @ https://github.com/josephrcox/easy-reddit-downloader#readme\n', 116 | ), 117 | false, 118 | ); 119 | // For debugging logs 120 | log('User config: ' + JSON.stringify(config), true); 121 | if (config.testingMode) { 122 | log('Testing mode options: ' + JSON.stringify(config.testingMode), true); 123 | } 124 | 125 | function checkConfig() { 126 | let warnTheUser = false; 127 | let quitApplicaton = false; 128 | 129 | let count = 130 | (config.file_naming_scheme.showDate === true) + 131 | (config.file_naming_scheme.showAuthor === true) + 132 | (config.file_naming_scheme.showTitle === true); 133 | if (count === 0) { 134 | quitApplicaton = true; 135 | } else if (count < 2) { 136 | warnTheUser = true; 137 | } 138 | 139 | if (warnTheUser) { 140 | log( 141 | chalk.red( 142 | 'WARNING: Your file naming scheme (user_config.json) is poorly set, we recommend changing it.', 143 | ), 144 | false, 145 | ); 146 | } 147 | 148 | if (quitApplicaton) { 149 | log( 150 | chalk.red( 151 | 'ALERT: Your file naming scheme (user_config.json) does not have any options set. You can not download posts without filenames. Aborting. ', 152 | ), 153 | false, 154 | ); 155 | process.exit(1); 156 | } 157 | 158 | if (quitApplicaton || warnTheUser) { 159 | log( 160 | chalk.red( 161 | 'Read about recommended naming schemes here - https://github.com/josephrcox/easy-reddit-downloader/blob/main/README.md#File-naming-scheme', 162 | ), 163 | false, 164 | ); 165 | } 166 | } 167 | 168 | // Make a GET request to the GitHub API to get the latest release 169 | request.get( 170 | 'https://api.github.com/repos/josephrcox/easy-reddit-downloader/releases/latest', 171 | { headers: { 'User-Agent': 'Downloader' } }, 172 | (error, response, body) => { 173 | if (error) { 174 | log(error, true); 175 | } else { 176 | // Parse the re∏sponse body to get the version number of the latest release 177 | const latestRelease = JSON.parse(body); 178 | const latestVersion = latestRelease.tag_name; 179 | 180 | // Compare the current version to the latest release version 181 | if (version !== latestVersion) { 182 | log( 183 | `Hey! A new version (${latestVersion}) is available. \nConsider updating to the latest version with 'git pull'.\n`, 184 | false, 185 | ); 186 | startScript(); 187 | } else { 188 | log('You are on the latest stable version (' + version + ')\n', true); 189 | startScript(); 190 | } 191 | } 192 | }, 193 | ); 194 | 195 | function startScript() { 196 | if (!testingMode && !config.download_post_list_options.enabled) { 197 | startPrompt(); 198 | } else { 199 | if (config.download_post_list_options.enabled) { 200 | downloadFromPostListFile(); 201 | } else { 202 | downloadSubredditPosts(subredditList[0], ''); // skip the prompt and get right to the API calls 203 | } 204 | } 205 | } 206 | 207 | async function startPrompt() { 208 | const questions = [ 209 | { 210 | type: 'text', 211 | name: 'subreddit', 212 | message: 213 | 'Which subreddits or users would you like to download? You may submit multiple separated by commas (no spaces).', 214 | validate: (value) => 215 | value.length < 1 ? `Please enter at least one subreddit or user` : true, 216 | }, 217 | { 218 | type: 'number', 219 | name: 'numberOfPosts', 220 | message: 221 | 'How many posts would you like to attempt to download? If you would like to download all posts, enter 0.', 222 | initial: 0, 223 | validate: (value) => 224 | // check if value is a number 225 | !isNaN(value) ? true : `Please enter a number`, 226 | }, 227 | { 228 | type: 'text', 229 | name: 'sorting', 230 | message: 231 | 'How would you like to sort? (top, new, hot, rising, controversial)', 232 | initial: 'top', 233 | validate: (value) => 234 | value.toLowerCase() === 'top' || 235 | value.toLowerCase() === 'new' || 236 | value.toLowerCase() === 'hot' || 237 | value.toLowerCase() === 'rising' || 238 | value.toLowerCase() === 'controversial' 239 | ? true 240 | : `Please enter a valid sorting method`, 241 | }, 242 | { 243 | type: 'text', 244 | name: 'time', 245 | message: 'During what time period? (hour, day, week, month, year, all)', 246 | initial: 'month', 247 | validate: (value) => 248 | value.toLowerCase() === 'hour' || 249 | value.toLowerCase() === 'day' || 250 | value.toLowerCase() === 'week' || 251 | value.toLowerCase() === 'month' || 252 | value.toLowerCase() === 'year' || 253 | value.toLowerCase() === 'all' 254 | ? true 255 | : `Please enter a valid time period`, 256 | }, 257 | { 258 | type: 'toggle', 259 | name: 'repeatForever', 260 | message: 'Would you like to run this on repeat?', 261 | initial: false, 262 | active: 'yes', 263 | inactive: 'no', 264 | }, 265 | { 266 | type: (prev) => (prev == true ? 'number' : null), 267 | name: 'timeBetweenRuns', 268 | message: 'How often would you like to run this? (in ms)', 269 | }, 270 | { 271 | type: 'text', 272 | name: 'downloadDirectory', 273 | message: 'Change the download path, defaults to ./downloads', 274 | initial: '', 275 | }, 276 | ]; 277 | 278 | const result = await prompts(questions); 279 | subredditList = result.subreddit.split(','); // the user enters subreddits separated by commas 280 | repeatForever = result.repeatForever; 281 | numberOfPosts = result.numberOfPosts; 282 | sorting = result.sorting.replace(/\s/g, ''); 283 | time = result.time.replace(/\s/g, ''); 284 | if (result.downloadDirectory) { 285 | downloadDirectoryBase = result.downloadDirectory; 286 | } 287 | 288 | // clean up the subreddit list in case the user puts in invalid chars 289 | for (let i = 0; i < subredditList.length; i++) { 290 | subredditList[i] = subredditList[i].replace(/\s/g, ''); 291 | } 292 | 293 | if (numberOfPosts === 0) { 294 | numberOfPosts = 9999999999999999999999; 295 | } 296 | 297 | if (repeatForever) { 298 | if (result.repeat < 0) { 299 | result.repeat = 0; 300 | } 301 | timeBetweenRuns = result.timeBetweenRuns; // the user enters the time between runs in ms 302 | } 303 | 304 | // With the data gathered, call the APIs and download the posts 305 | startTime = new Date(); 306 | downloadSubredditPosts(subredditList[0], ''); 307 | } 308 | 309 | function makeDirectories() { 310 | // Make needed directories for downloads, 311 | // clean and nsfw are made nomatter the subreddits downloaded 312 | if (!fs.existsSync(downloadDirectoryBase)) { 313 | fs.mkdirSync(downloadDirectoryBase); 314 | } 315 | if (config.separate_clean_nsfw) { 316 | if (!fs.existsSync(downloadDirectoryBase + '/clean')) { 317 | fs.mkdirSync(downloadDirectoryBase + '/clean'); 318 | } 319 | if (!fs.existsSync(downloadDirectoryBase + '/nsfw')) { 320 | fs.mkdirSync(downloadDirectoryBase + '/nsfw'); 321 | } 322 | } 323 | } 324 | 325 | async function downloadSubredditPosts(subreddit, lastPostId) { 326 | let isUser = false; 327 | if ( 328 | subreddit.includes('u/') || 329 | subreddit.includes('user/') || 330 | subreddit.includes('/u/') 331 | ) { 332 | isUser = true; 333 | subreddit = subreddit.split('u/').pop(); 334 | return downloadUser(subreddit, lastPostId); 335 | } 336 | let postsRemaining = numberOfPostsRemaining()[0]; 337 | if (postsRemaining <= 0) { 338 | // If we have downloaded enough posts, move on to the next subreddit 339 | if (subredditList.length > 1) { 340 | return downloadNextSubreddit(); 341 | } else { 342 | // If we have downloaded all the subreddits, end the program 343 | return checkIfDone('', true); 344 | } 345 | return; 346 | } else if (postsRemaining > 100) { 347 | // If we have more posts to download than the limit of 100, set it to 100 348 | postsRemaining = 100; 349 | } 350 | 351 | // if lastPostId is undefined, set it to an empty string. Common on first run. 352 | if (lastPostId == undefined) { 353 | lastPostId = ''; 354 | } 355 | makeDirectories(); 356 | 357 | try { 358 | if (subreddit == undefined) { 359 | if (subredditList.length > 1) { 360 | return downloadNextSubreddit(); 361 | } else { 362 | return checkIfDone(); 363 | } 364 | } 365 | 366 | // Use log function to log a string 367 | // as well as a boolean if the log should be displayed to the user. 368 | if (isUser) { 369 | log( 370 | `\n\n👀 Requesting posts from 371 | https://www.reddit.com/user/${subreddit.replace( 372 | 'u/', 373 | '', 374 | )}/${sorting}/.json?sort=${sorting}&t=${time}&limit=${postsRemaining}&after=${lastPostId}\n`, 375 | true, 376 | ); 377 | } else { 378 | log( 379 | `\n\n👀 Requesting posts from 380 | https://www.reddit.com/r/${subreddit}/${sorting}/.json?sort=${sorting}&t=${time}&limit=${postsRemaining}&after=${lastPostId}\n`, 381 | true, 382 | ); 383 | } 384 | 385 | // Get the top posts from the subreddit 386 | let response = null; 387 | let data = null; 388 | 389 | try { 390 | response = await axios.get( 391 | `https://www.reddit.com/r/${subreddit}/${sorting}/.json?sort=${sorting}&t=${time}&limit=${postsRemaining}&after=${lastPostId}`, 392 | ); 393 | 394 | data = await response.data; 395 | 396 | currentAPICall = data; 397 | if (data.message == 'Not Found' || data.data.children.length == 0) { 398 | throw error; 399 | } 400 | if (data.data.children.length < postsRemaining) { 401 | lastAPICallForSubreddit = true; 402 | postsRemaining = data.data.children.length; 403 | } else { 404 | lastAPICallForSubreddit = false; 405 | } 406 | } catch (err) { 407 | log( 408 | `\n\nERROR: There was a problem fetching posts for ${subreddit}. This is likely because the subreddit is private, banned, or doesn't exist.`, 409 | true, 410 | ); 411 | if (subredditList.length > 1) { 412 | if (currentSubredditIndex > subredditList.length - 1) { 413 | currentSubredditIndex = -1; 414 | } 415 | currentSubredditIndex += 1; 416 | return downloadSubredditPosts(subredditList[currentSubredditIndex], ''); 417 | } else { 418 | return checkIfDone('', true); 419 | } 420 | } 421 | 422 | // if the first post on the subreddit is NSFW, then there is a fair chance 423 | // that the rest of the posts are NSFW. 424 | let isOver18 = data.data.children[0].data.over_18 ? 'nsfw' : 'clean'; 425 | downloadedPosts.subreddit = data.data.children[0].data.subreddit; 426 | 427 | if (!config.separate_clean_nsfw) { 428 | downloadDirectory = 429 | downloadDirectoryBase + `/${data.data.children[0].data.subreddit}`; 430 | } else { 431 | downloadDirectory = 432 | downloadDirectoryBase + 433 | `/${isOver18}/${data.data.children[0].data.subreddit}`; 434 | } 435 | 436 | // Make sure the image directory exists 437 | // If no directory is found, create one 438 | if (!fs.existsSync(downloadDirectory)) { 439 | fs.mkdirSync(downloadDirectory); 440 | } 441 | 442 | responseSize = data.data.children.length; 443 | 444 | for (const child of data.data.children) { 445 | await sleep(); 446 | try { 447 | const post = child.data; 448 | await downloadPost(post); // Make sure to await this as well 449 | } catch (e) { 450 | log(e, true); 451 | } 452 | } 453 | } catch (error) { 454 | // throw the error 455 | throw error; 456 | } 457 | } 458 | 459 | async function downloadUser(user, currentUserAfter) { 460 | let lastPostId = currentUserAfter; 461 | let postsRemaining = numberOfPostsRemaining()[0]; 462 | if (postsRemaining <= 0) { 463 | // If we have downloaded enough posts, move on to the next subreddit 464 | if (subredditList.length > 1) { 465 | return downloadNextSubreddit(); 466 | } else { 467 | // If we have downloaded all the subreddits, end the program 468 | return checkIfDone('', true); 469 | } 470 | return; 471 | } else if (postsRemaining > 100) { 472 | // If we have more posts to download than the limit of 100, set it to 100 473 | postsRemaining = 100; 474 | } 475 | 476 | // if lastPostId is undefined, set it to an empty string. Common on first run. 477 | if (lastPostId == undefined) { 478 | lastPostId = ''; 479 | } 480 | makeDirectories(); 481 | 482 | try { 483 | if (user == undefined) { 484 | if (subredditList.length > 1) { 485 | return downloadNextSubreddit(); 486 | } else { 487 | return checkIfDone(); 488 | } 489 | } 490 | 491 | // Use log function to log a string 492 | // as well as a boolean if the log should be displayed to the user. 493 | let reqUrl = `https://www.reddit.com/user/${user.replace( 494 | 'u/', 495 | '', 496 | )}/submitted/.json?limit=${postsRemaining}&after=${lastPostId}`; 497 | log( 498 | `\n\n👀 Requesting posts from 499 | ${reqUrl}\n`, 500 | false, 501 | ); 502 | 503 | // Get the top posts from the subreddit 504 | let response = null; 505 | let data = null; 506 | 507 | try { 508 | response = await axios.get(`${reqUrl}`); 509 | 510 | data = await response.data; 511 | currentUserAfter = data.data.after; 512 | 513 | currentAPICall = data; 514 | if (data.message == 'Not Found' || data.data.children.length == 0) { 515 | throw error; 516 | } 517 | if (data.data.children.length < postsRemaining) { 518 | lastAPICallForSubreddit = true; 519 | postsRemaining = data.data.children.length; 520 | } else { 521 | lastAPICallForSubreddit = false; 522 | } 523 | } catch (err) { 524 | log( 525 | `\n\nERROR: There was a problem fetching posts for ${user}. This is likely because the subreddit is private, banned, or doesn't exist.`, 526 | true, 527 | ); 528 | if (subredditList.length > 1) { 529 | if (currentSubredditIndex > subredditList.length - 1) { 530 | currentSubredditIndex = -1; 531 | } 532 | currentSubredditIndex += 1; 533 | return downloadSubredditPosts(subredditList[currentSubredditIndex], ''); 534 | } else { 535 | return checkIfDone('', true); 536 | } 537 | } 538 | 539 | downloadDirectory = 540 | downloadDirectoryBase + `/user_${user.replace('u/', '')}`; 541 | 542 | // Make sure the image directory exists 543 | // If no directory is found, create one 544 | if (!fs.existsSync(downloadDirectory)) { 545 | fs.mkdirSync(downloadDirectory); 546 | } 547 | 548 | responseSize = data.data.children.length; 549 | 550 | for (const child of data.data.children) { 551 | await sleep(); 552 | try { 553 | const post = child.data; 554 | await downloadPost(post); // Make sure to await this as well 555 | } catch (e) { 556 | log(e, true); 557 | } 558 | } 559 | } catch (error) { 560 | // throw the error 561 | throw error; 562 | } 563 | } 564 | 565 | async function downloadFromPostListFile() { 566 | // this is called when config.download_from_post_list_file is true 567 | // this will read the download_post_list.txt file and download all the posts in it 568 | // downloading skips any lines starting with "#" as they are used for documentation 569 | 570 | // read the file 571 | let file = fs.readFileSync('./download_post_list.txt', 'utf8'); 572 | // split the file into an array of lines 573 | let lines = file.split('\n'); 574 | // remove any lines that start with "#" 575 | lines = lines.filter((line) => !line.startsWith('#')); 576 | // remove any empty lines 577 | lines = lines.filter((line) => line != ''); 578 | // remove any lines that are just whitespace 579 | lines = lines.filter((line) => line.trim() != ''); 580 | // remove any lines that don't start with "https://www.reddit.com" 581 | lines = lines.filter((line) => line.startsWith('https://www.reddit.com')); 582 | // remove any lines that don't have "/comments/" in them 583 | lines = lines.filter((line) => line.includes('/comments/')); 584 | numberOfPosts = lines.length; 585 | 586 | repeatForever = config.download_post_list_options.repeatForever; 587 | timeBetweenRuns = config.download_post_list_options.timeBetweenRuns; 588 | 589 | if (numberOfPosts === 0) { 590 | log( 591 | chalk.red( 592 | 'ERROR: There are no posts in the download_post_list.txt file. Please add some posts to the file and try again.\n', 593 | ), 594 | false, 595 | ); 596 | log( 597 | chalk.yellow( 598 | 'If you are trying to download posts from a subreddit, please set "download_post_list_options.enabled" to false in the user_config.json file.\n', 599 | ), 600 | false, 601 | ); 602 | process.exit(1); 603 | } 604 | 605 | log( 606 | chalk.green( 607 | `Starting download of ${numberOfPosts} posts from the download_post_list.txt file.\n`, 608 | ), 609 | ); 610 | // iterate over the lines and download the posts 611 | for (let i = 0; i < lines.length; i++) { 612 | const line = lines[i]; 613 | const reqUrl = line + '.json'; 614 | axios.get(reqUrl).then(async (response) => { 615 | const post = response.data[0].data.children[0].data; 616 | let isOver18 = post.over_18 ? 'nsfw' : 'clean'; 617 | downloadedPosts.subreddit = post.subreddit; 618 | makeDirectories(); 619 | 620 | if (!config.separate_clean_nsfw) { 621 | downloadDirectory = downloadDirectoryBase + `/${post.subreddit}`; 622 | } else { 623 | downloadDirectory = 624 | downloadDirectoryBase + `/${isOver18}/${post.subreddit}`; 625 | } 626 | 627 | // Make sure the image directory exists 628 | // If no directory is found, create one 629 | if (!fs.existsSync(downloadDirectory)) { 630 | fs.mkdirSync(downloadDirectory); 631 | } 632 | downloadPost(post); 633 | }); 634 | await sleep(); 635 | } 636 | } 637 | 638 | function getPostType(post, postTypeOptions) { 639 | log(`Analyzing post with title: ${post.title}) and URL: ${post.url}`, true); 640 | if (post.post_hint === 'self' || post.is_self) { 641 | postType = 0; 642 | } else if ( 643 | post.post_hint === 'image' || 644 | (post.post_hint === 'rich:video' && !post.domain.includes('youtu')) || 645 | post.post_hint === 'hosted:video' || 646 | (post.post_hint === 'link' && 647 | post.domain.includes('imgur') && 648 | !post.url_overridden_by_dest.includes('gallery')) || 649 | post.domain.includes('i.redd.it') || 650 | post.domain.includes('i.reddituploads.com') 651 | ) { 652 | postType = 1; 653 | } else if (post.poll_data != undefined) { 654 | postType = 3; // UNSUPPORTED 655 | } else if (post.domain.includes('reddit.com') && post.is_gallery) { 656 | postType = 4; 657 | } else { 658 | postType = 2; 659 | } 660 | log( 661 | `Post has type: ${postTypeOptions[postType]} due to their post hint: ${post.post_hint} and domain: ${post.domain}`, 662 | true, 663 | ); 664 | return postType; 665 | } 666 | 667 | async function downloadMediaFile(downloadURL, filePath, postName) { 668 | try { 669 | const response = await axios({ 670 | method: 'GET', 671 | url: downloadURL, 672 | responseType: 'stream', 673 | }); 674 | 675 | response.data.pipe(fs.createWriteStream(filePath)); 676 | 677 | return new Promise((resolve, reject) => { 678 | response.data.on('end', () => { 679 | downloadedPosts.media += 1; 680 | checkIfDone(postName); 681 | resolve(); 682 | }); 683 | 684 | response.data.on('error', (error) => { 685 | reject(error); 686 | }); 687 | }); 688 | } catch (error) { 689 | downloadedPosts.failed += 1; 690 | checkIfDone(postName); 691 | if (error.code === 'ENOTFOUND') { 692 | log( 693 | 'ERROR: Hostname not found for: ' + downloadURL + '\n... skipping post', 694 | true, 695 | ); 696 | } else { 697 | log('ERROR: ' + error, true); 698 | } 699 | } 700 | } 701 | 702 | function sleep() { 703 | return new Promise((resolve) => setTimeout(resolve, postDelayMilliseconds)); 704 | } 705 | 706 | async function downloadPost(post) { 707 | let postTypeOptions = ['self', 'media', 'link', 'poll', 'gallery']; 708 | let postType = -1; // default to no postType until one is found 709 | 710 | // Determine the type of post. If no type is found, default to link as a last resort. 711 | // If it accidentally downloads a self or media post as a link, it will still 712 | // save properly. 713 | postType = getPostType(post, postTypeOptions); 714 | 715 | // Array of possible (supported) image and video formats 716 | const imageFormats = ['jpeg', 'jpg', 'gif', 'png', 'mp4', 'webm', 'gifv']; 717 | 718 | // All posts should have URLs, so just make sure that it does. 719 | // If the post doesn't have a URL, then it should be skipped. 720 | if (postType == 4) { 721 | // Don't download the gallery if we don't want to 722 | if (!config.download_gallery_posts) { 723 | log(`Skipping gallery post with title: ${post.title}`, true); 724 | downloadedPosts.skipped_due_to_fileType += 1; 725 | return checkIfDone(post.name); 726 | } 727 | 728 | // The title will be the directory name 729 | const postTitleScrubbed = getFileName(post); 730 | let newDownloads = Object.keys(post.media_metadata).length; 731 | // gallery_data retains the order of the gallery, so we loop over this 732 | // media_id can be used as the key in media_metadata 733 | for (const { media_id, id } of post.gallery_data.items) { 734 | const media = post.media_metadata[media_id]; 735 | // s=highest quality (for some reason), u=URL 736 | // URL contains & instead of & 737 | const downloadUrl = media['s']['u'].replaceAll('&', '&'); 738 | const shortUrl = downloadUrl.split('?')[0]; 739 | const fileType = shortUrl.split('.').pop(); 740 | 741 | // Create directory for gallery 742 | const postDirectory = `${downloadDirectory}/${postTitleScrubbed}`; 743 | if (!fs.existsSync(postDirectory)) { 744 | fs.mkdirSync(postDirectory); 745 | } 746 | const filePath = `${postTitleScrubbed}/${id}.${fileType}`; 747 | const toDownload = await shouldWeDownload(post.subreddit, filePath); 748 | 749 | if (!toDownload) { 750 | if (--newDownloads === 0) { 751 | downloadedPosts.skipped_due_to_duplicate += 1; 752 | if (checkIfDone(post.name)) { 753 | return; 754 | } 755 | } 756 | } else { 757 | downloadMediaFile( 758 | downloadUrl, 759 | `${downloadDirectory}/${filePath}`, 760 | post.name, 761 | ); 762 | } 763 | } 764 | } else if (postType != 3 && post.url !== undefined) { 765 | let downloadURL = post.url; 766 | // Get the file type of the post via the URL. If it ends in .jpg, then it's a jpg. 767 | let fileType = downloadURL.split('.').pop(); 768 | // Post titles can be really long and have invalid characters, so we need to clean them up. 769 | let postTitleScrubbed = sanitizeFileName(post.title); 770 | postTitleScrubbed = getFileName(post); 771 | 772 | if (postType === 0) { 773 | // DOWNLOAD A SELF POST 774 | let toDownload = await shouldWeDownload( 775 | post.subreddit, 776 | `${postTitleScrubbed}.txt`, 777 | ); 778 | if (!toDownload) { 779 | downloadedPosts.skipped_due_to_duplicate += 1; 780 | return checkIfDone(post.name); 781 | } else { 782 | if (!config.download_self_posts) { 783 | log(`Skipping self post with title: ${post.title}`, true); 784 | downloadedPosts.skipped_due_to_fileType += 1; 785 | return checkIfDone(post.name); 786 | } else { 787 | // DOWNLOAD A SELF POST 788 | let comments_string = ''; 789 | let postResponse = null; 790 | let data = null; 791 | try { 792 | postResponse = await axios.get(`${post.url}.json`); 793 | data = postResponse.data; 794 | } catch (error) { 795 | log(`Axios failure with ${post.url}`, true); 796 | return checkIfDone(post.name); 797 | } 798 | 799 | // With text/self posts, we want to download the top comments as well. 800 | // This is done by requesting the post's JSON data, and then iterating through each comment. 801 | // We also iterate through the top nested comments (only one level deep). 802 | // So we have a file output with the post title, the post text, the author, and the top comments. 803 | 804 | comments_string += post.title + ' by ' + post.author + '\n\n'; 805 | comments_string += post.selftext + '\n'; 806 | comments_string += 807 | '------------------------------------------------\n\n'; 808 | if (config.download_comments) { 809 | // If the user wants to download comments 810 | comments_string += '--COMMENTS--\n\n'; 811 | data[1].data.children.forEach((child) => { 812 | const comment = child.data; 813 | comments_string += comment.author + ':\n'; 814 | comments_string += comment.body + '\n'; 815 | if (comment.replies) { 816 | const top_reply = comment.replies.data.children[0].data; 817 | comments_string += '\t>\t' + top_reply.author + ':\n'; 818 | comments_string += '\t>\t' + top_reply.body + '\n'; 819 | } 820 | comments_string += '\n\n\n'; 821 | }); 822 | } 823 | 824 | fs.writeFile( 825 | `${downloadDirectory}/${postTitleScrubbed}.txt`, 826 | comments_string, 827 | function (err) { 828 | if (err) { 829 | log(err, true); 830 | } 831 | downloadedPosts.self += 1; 832 | if (checkIfDone(post.name)) { 833 | return; 834 | } 835 | }, 836 | ); 837 | } 838 | } 839 | } else if (postType === 1) { 840 | // DOWNLOAD A MEDIA POST 841 | if (post.preview != undefined) { 842 | // Reddit stores fallback URL previews for some GIFs. 843 | // Changing the URL to download to the fallback URL will download the GIF, in MP4 format. 844 | if (post.preview.reddit_video_preview != undefined) { 845 | log( 846 | "Using fallback URL for Reddit's GIF preview." + 847 | post.preview.reddit_video_preview, 848 | true, 849 | ); 850 | downloadURL = post.preview.reddit_video_preview.fallback_url; 851 | fileType = 'mp4'; 852 | } else if (post.url_overridden_by_dest.includes('.gifv')) { 853 | // Luckily, you can just swap URLs on imgur with .gifv 854 | // with ".mp4" to get the MP4 version. Amazing! 855 | log('Replacing gifv with mp4', true); 856 | downloadURL = post.url_overridden_by_dest.replace('.gifv', '.mp4'); 857 | fileType = 'mp4'; 858 | } else { 859 | let sourceURL = post.preview.images[0].source.url; 860 | // set fileType to whatever imageFormat item is in the sourceURL 861 | for (let i = 0; i < imageFormats.length; i++) { 862 | if ( 863 | sourceURL.toLowerCase().includes(imageFormats[i].toLowerCase()) 864 | ) { 865 | fileType = imageFormats[i]; 866 | break; 867 | } 868 | } 869 | } 870 | } 871 | if (post.media != undefined && post.post_hint == 'hosted:video') { 872 | // If the post has a media object, then it's a video. 873 | // We need to get the URL from the media object. 874 | // This is because the URL in the post object is a fallback URL. 875 | // The media object has the actual URL. 876 | downloadURL = post.media.reddit_video.fallback_url; 877 | fileType = 'mp4'; 878 | } else if ( 879 | post.media != undefined && 880 | post.post_hint == 'rich:video' && 881 | post.media.oembed.thumbnail_url != undefined 882 | ) { 883 | // Common for gfycat links 884 | downloadURL = post.media.oembed.thumbnail_url; 885 | fileType = 'gif'; 886 | } 887 | if (!config.download_media_posts) { 888 | log(`Skipping media post with title: ${post.title}`, true); 889 | downloadedPosts.skipped_due_to_fileType += 1; 890 | return checkIfDone(post.name); 891 | } else { 892 | let toDownload = await shouldWeDownload( 893 | post.subreddit, 894 | `${postTitleScrubbed}.${fileType}`, 895 | ); 896 | if (!toDownload) { 897 | downloadedPosts.skipped_due_to_duplicate += 1; 898 | if (checkIfDone(post.name)) { 899 | return; 900 | } 901 | } else { 902 | downloadMediaFile( 903 | downloadURL, 904 | `${downloadDirectory}/${postTitleScrubbed}.${fileType}`, 905 | post.name, 906 | ); 907 | } 908 | } 909 | } else if (postType === 2) { 910 | if (!config.download_link_posts) { 911 | log(`Skipping link post with title: ${post.title}`, true); 912 | downloadedPosts.skipped_due_to_fileType += 1; 913 | return checkIfDone(post.name); 914 | } else { 915 | let toDownload = await shouldWeDownload( 916 | post.subreddit, 917 | `${postTitleScrubbed}.html`, 918 | ); 919 | if (!toDownload) { 920 | downloadedPosts.skipped_due_to_duplicate += 1; 921 | if (checkIfDone(post.name)) { 922 | return; 923 | } 924 | } else { 925 | // DOWNLOAD A LINK POST 926 | // With link posts, we create a simple HTML file that redirects to the post's URL. 927 | // This enables the user to still "open" the link file, and it will redirect to the post. 928 | // No comments or other data is stored. 929 | 930 | if ( 931 | post.domain.includes('youtu') && 932 | config.download_youtube_videos_experimental 933 | ) { 934 | log( 935 | `Downloading ${postTitleScrubbed} from YouTube... This may take a while...`, 936 | false, 937 | ); 938 | let url = post.url; 939 | try { 940 | // Validate YouTube URL 941 | if (!ytdl.validateURL(url)) { 942 | throw new Error('Invalid YouTube URL'); 943 | } 944 | 945 | // Get video info 946 | const info = await ytdl.getInfo(url); 947 | log(info, true); 948 | 949 | // Choose the highest quality format available 950 | const format = ytdl.chooseFormat(info.formats, { 951 | quality: 'highest', 952 | }); 953 | 954 | // Create a filename based on the video title 955 | const fileName = `${postTitleScrubbed}.mp4`; 956 | 957 | // Download audio stream 958 | const audio = ytdl(url, { filter: 'audioonly' }); 959 | const audioPath = `${downloadDirectory}/${fileName}.mp3`; 960 | audio.pipe(fs.createWriteStream(audioPath)); 961 | 962 | // Download video stream 963 | const video = ytdl(url, { format }); 964 | const videoPath = `${downloadDirectory}/${fileName}.mp4`; 965 | video.pipe(fs.createWriteStream(videoPath)); 966 | 967 | // Wait for both streams to finish downloading 968 | await Promise.all([ 969 | new Promise((resolve) => audio.on('end', resolve)), 970 | new Promise((resolve) => video.on('end', resolve)), 971 | ]); 972 | 973 | // Merge audio and video using ffmpeg 974 | ffmpeg() 975 | .input(videoPath) 976 | .input(audioPath) 977 | .output(`${downloadDirectory}/${fileName}`) 978 | .on('end', () => { 979 | console.log('Download complete'); 980 | // Remove temporary audio and video files 981 | fs.unlinkSync(audioPath); 982 | fs.unlinkSync(videoPath); 983 | downloadedPosts.link += 1; 984 | if (checkIfDone(post.name)) { 985 | return; 986 | } 987 | }) 988 | .run(); 989 | } catch (error) { 990 | log( 991 | `Failed to download ${postTitleScrubbed} from YouTube. Do you have FFMPEG installed? https://ffmpeg.org/ `, 992 | false, 993 | ); 994 | let htmlFile = ``; 995 | 996 | fs.writeFile( 997 | `${downloadDirectory}/${postTitleScrubbed}.html`, 998 | htmlFile, 999 | function (err) { 1000 | if (err) throw err; 1001 | downloadedPosts.link += 1; 1002 | if (checkIfDone(post.name)) { 1003 | return; 1004 | } 1005 | }, 1006 | ); 1007 | } 1008 | } else { 1009 | let htmlFile = ``; 1010 | 1011 | fs.writeFile( 1012 | `${downloadDirectory}/${postTitleScrubbed}.html`, 1013 | htmlFile, 1014 | function (err) { 1015 | if (err) throw err; 1016 | downloadedPosts.link += 1; 1017 | if (checkIfDone(post.name)) { 1018 | return; 1019 | } 1020 | }, 1021 | ); 1022 | } 1023 | } 1024 | } 1025 | } else { 1026 | log('Failed to download: ' + post.title + 'with URL: ' + post.url, true); 1027 | downloadedPosts.failed += 1; 1028 | if (checkIfDone(post.name)) { 1029 | return; 1030 | } 1031 | } 1032 | } else { 1033 | log('Failed to download: ' + post.title + 'with URL: ' + post.url, true); 1034 | downloadedPosts.failed += 1; 1035 | if (checkIfDone(post.name)) { 1036 | return; 1037 | } 1038 | } 1039 | } 1040 | 1041 | function downloadNextSubreddit() { 1042 | if (currentSubredditIndex > subredditList.length) { 1043 | checkIfDone('', true); 1044 | } else { 1045 | currentSubredditIndex += 1; 1046 | downloadSubredditPosts(subredditList[currentSubredditIndex]); 1047 | } 1048 | } 1049 | 1050 | function shouldWeDownload(subreddit, postTitleWithPrefixAndExtension) { 1051 | if ( 1052 | config.redownload_posts === true || 1053 | config.redownload_posts === undefined 1054 | ) { 1055 | if (config.redownload_posts === undefined) { 1056 | log( 1057 | chalk.red( 1058 | "ALERT: Please note that the 'redownload_posts' option is now available in user_config. See the default JSON for example usage.", 1059 | ), 1060 | true, 1061 | ); 1062 | } 1063 | return true; 1064 | } else { 1065 | // Check if the post in the subreddit folder already exists. 1066 | // If it does, we don't need to download it again. 1067 | let postExists = fs.existsSync( 1068 | `${downloadDirectory}/${postTitleWithPrefixAndExtension}`, 1069 | ); 1070 | return !postExists; 1071 | } 1072 | } 1073 | 1074 | function onErr(err) { 1075 | log(err, true); 1076 | return 1; 1077 | } 1078 | 1079 | // checkIfDone is called frequently to see if we have downloaded the number of posts 1080 | // that the user requested to download. 1081 | // We could check this inline but it's easier to read if it's a separate function, 1082 | // and this ensures that we only check after the files are done being downloaded to the PC, not 1083 | // just when the request is sent. 1084 | function checkIfDone(lastPostId, override) { 1085 | // If we are downloading from a post list, simply ignore this function. 1086 | if (config.download_post_list_options.enabled) { 1087 | if (numberOfPostsRemaining()[0] > 0) { 1088 | // Still downloading from post list 1089 | log( 1090 | `Still downloading posts from ${chalk.cyan( 1091 | subredditList[currentSubredditIndex], 1092 | )}... (${numberOfPostsRemaining()[1]}/all)`, 1093 | false, 1094 | ); 1095 | } else { 1096 | // Done downloading from post list 1097 | log(`Finished downloading posts from download_post_list.txt`, false); 1098 | downloadedPosts = { 1099 | subreddit: '', 1100 | self: 0, 1101 | media: 0, 1102 | link: 0, 1103 | failed: 0, 1104 | skipped_due_to_duplicate: 0, 1105 | skipped_due_to_fileType: 0, 1106 | }; 1107 | if (config.download_post_list_options.repeatForever) { 1108 | log( 1109 | `⏲️ Waiting ${ 1110 | config.download_post_list_options.timeBetweenRuns / 1000 1111 | } seconds before rerunning...`, 1112 | false, 1113 | ); 1114 | setTimeout(function () { 1115 | startTime = new Date(); 1116 | downloadFromPostListFile(); 1117 | }, timeBetweenRuns); 1118 | } 1119 | } 1120 | } else if ( 1121 | (lastAPICallForSubreddit && 1122 | lastPostId === 1123 | currentAPICall.data.children[responseSize - 1].data.name) || 1124 | numberOfPostsRemaining()[0] === 0 || 1125 | override || 1126 | (numberOfPostsRemaining()[1] === responseSize && responseSize < 100) 1127 | ) { 1128 | let endTime = new Date(); 1129 | let timeDiff = endTime - startTime; 1130 | timeDiff /= 1000; 1131 | let msPerPost = (timeDiff / numberOfPostsRemaining()[1]) 1132 | .toString() 1133 | .substring(0, 5); 1134 | if (numberOfPosts >= 99999999999999999999) { 1135 | log( 1136 | `Still downloading posts from ${chalk.cyan( 1137 | subredditList[currentSubredditIndex], 1138 | )}... (${numberOfPostsRemaining()[1]}/all)`, 1139 | false, 1140 | ); 1141 | } else { 1142 | log( 1143 | `Still downloading posts from ${chalk.cyan( 1144 | subredditList[currentSubredditIndex], 1145 | )}... (${numberOfPostsRemaining()[1]}/${numberOfPosts})`, 1146 | false, 1147 | ); 1148 | } 1149 | if (numberOfPostsRemaining()[0] === 0) { 1150 | log('Validating that all posts were downloaded...', false); 1151 | setTimeout(() => { 1152 | log( 1153 | '🎉 All done downloading posts from ' + 1154 | subredditList[currentSubredditIndex] + 1155 | '!', 1156 | false, 1157 | ); 1158 | 1159 | log(JSON.stringify(downloadedPosts), true); 1160 | if (currentSubredditIndex === subredditList.length - 1) { 1161 | log( 1162 | `\n📈 Downloading took ${timeDiff} seconds, at about ${msPerPost} seconds/post`, 1163 | false, 1164 | ); 1165 | } 1166 | 1167 | // default values for next run (important if being run multiple times) 1168 | downloadedPosts = { 1169 | subreddit: '', 1170 | self: 0, 1171 | media: 0, 1172 | link: 0, 1173 | failed: 0, 1174 | skipped_due_to_duplicate: 0, 1175 | skipped_due_to_fileType: 0, 1176 | }; 1177 | 1178 | if (currentSubredditIndex < subredditList.length - 1) { 1179 | downloadNextSubreddit(); 1180 | } else if (repeatForever) { 1181 | currentSubredditIndex = 0; 1182 | log( 1183 | `⏲️ Waiting ${timeBetweenRuns / 1000} seconds before rerunning...`, 1184 | false, 1185 | ); 1186 | setTimeout(function () { 1187 | downloadSubredditPosts(subredditList[0], ''); 1188 | startTime = new Date(); 1189 | }, timeBetweenRuns); 1190 | } else { 1191 | startPrompt(); 1192 | } 1193 | return true; 1194 | }, 1000); 1195 | } 1196 | } else { 1197 | if (numberOfPosts >= 99999999999999999999) { 1198 | log( 1199 | `Still downloading posts from ${chalk.cyan( 1200 | subredditList[currentSubredditIndex], 1201 | )}... (${numberOfPostsRemaining()[1]}/all)`, 1202 | false, 1203 | ); 1204 | } else { 1205 | log( 1206 | `Still downloading posts from ${chalk.cyan( 1207 | subredditList[currentSubredditIndex], 1208 | )}... (${numberOfPostsRemaining()[1]}/${numberOfPosts})`, 1209 | false, 1210 | ); 1211 | } 1212 | 1213 | for (let i = 0; i < Object.keys(downloadedPosts).length; i++) { 1214 | log( 1215 | `\t- ${Object.keys(downloadedPosts)[i]}: ${ 1216 | Object.values(downloadedPosts)[i] 1217 | }`, 1218 | true, 1219 | ); 1220 | } 1221 | log('\n', true); 1222 | 1223 | if (numberOfPostsRemaining()[1] % 100 == 0) { 1224 | return downloadSubredditPosts( 1225 | subredditList[currentSubredditIndex], 1226 | lastPostId, 1227 | ); 1228 | } 1229 | return false; 1230 | } 1231 | } 1232 | 1233 | function getFileName(post) { 1234 | let fileName = ''; 1235 | if ( 1236 | config.file_naming_scheme.showDate || 1237 | config.file_naming_scheme.showDate === undefined 1238 | ) { 1239 | let timestamp = post.created; 1240 | var date = new Date(timestamp * 1000); 1241 | var year = date.getFullYear(); 1242 | var month = (date.getMonth() + 1).toString().padStart(2, '0'); 1243 | var day = date.getDate().toString().padStart(2, '0'); 1244 | fileName += `${year}-${month}-${day}`; 1245 | } 1246 | if ( 1247 | config.file_naming_scheme.showScore || 1248 | config.file_naming_scheme.showScore === undefined 1249 | ) { 1250 | fileName += `_score=${post.score}`; 1251 | } 1252 | if ( 1253 | config.file_naming_scheme.showSubreddit || 1254 | config.file_naming_scheme.showSubreddit === undefined 1255 | ) { 1256 | fileName += `_${post.subreddit}`; 1257 | } 1258 | if ( 1259 | config.file_naming_scheme.showAuthor || 1260 | config.file_naming_scheme.showAuthor === undefined 1261 | ) { 1262 | fileName += `_${post.author}`; 1263 | } 1264 | if ( 1265 | config.file_naming_scheme.showTitle || 1266 | config.file_naming_scheme.showTitle === undefined 1267 | ) { 1268 | let title = sanitizeFileName(post.title); 1269 | fileName += `_${title}`; 1270 | } 1271 | 1272 | // remove special chars from name 1273 | fileName = fileName.replace(/(?:\r\n|\r|\n|\t)/g, ''); 1274 | 1275 | if (fileName.search(/\ufe0e/g) >= -1) { 1276 | fileName = fileName.replace(/\ufe0e/g, ''); 1277 | } 1278 | 1279 | if (fileName.search(/\ufe0f/g) >= -1) { 1280 | fileName = fileName.replace(/\ufe0f/g, ''); 1281 | } 1282 | 1283 | // The max length for most systems is about 255. To give some wiggle room, I'm doing 240 1284 | if (fileName.length > 240) { 1285 | fileName = fileName.substring(0, 240); 1286 | } 1287 | 1288 | return fileName; 1289 | } 1290 | 1291 | function numberOfPostsRemaining() { 1292 | let total = 1293 | downloadedPosts.self + 1294 | downloadedPosts.media + 1295 | downloadedPosts.link + 1296 | downloadedPosts.failed + 1297 | downloadedPosts.skipped_due_to_duplicate + 1298 | downloadedPosts.skipped_due_to_fileType; 1299 | return [numberOfPosts - total, total]; 1300 | } 1301 | 1302 | function log(message, detailed) { 1303 | // This function takes a message string and a boolean. 1304 | // If the boolean is true, the message will be logged to the console, otherwise it 1305 | // will only be logged to the log file. 1306 | userLogs += message + '\r\n'; 1307 | let visibleToUser = true; 1308 | if (detailed) { 1309 | visibleToUser = config.detailed_logs; 1310 | } 1311 | 1312 | if (visibleToUser) { 1313 | console.log(message); 1314 | } 1315 | if (config.local_logs && subredditList.length > 0) { 1316 | if (!fs.existsSync('./logs')) { 1317 | fs.mkdirSync('./logs'); 1318 | } 1319 | 1320 | let logFileName = ''; 1321 | if (config.local_logs_naming_scheme.showDateAndTime) { 1322 | logFileName += `${date_string} - `; 1323 | } 1324 | if (config.local_logs_naming_scheme.showSubreddits) { 1325 | let subredditListString = JSON.stringify(subredditList).replace( 1326 | /[^a-zA-Z0-9,]/g, 1327 | '', 1328 | ); 1329 | logFileName += `${subredditListString} - `; 1330 | } 1331 | if (config.local_logs_naming_scheme.showNumberOfPosts) { 1332 | if (numberOfPosts < 999999999999999999) { 1333 | logFileName += `ALL - `; 1334 | } else { 1335 | logFileName += `${numberOfPosts} - `; 1336 | } 1337 | } 1338 | 1339 | if (logFileName.endsWith(' - ')) { 1340 | logFileName = logFileName.substring(0, logFileName.length - 3); 1341 | } 1342 | 1343 | fs.writeFile( 1344 | `./logs/${logFileName}.${logFormat}`, 1345 | userLogs, 1346 | function (err) { 1347 | if (err) throw err; 1348 | }, 1349 | ); 1350 | } 1351 | } 1352 | 1353 | // sanitize function for file names so that they work on Mac, Windows, and Linux 1354 | function sanitizeFileName(fileName) { 1355 | return fileName 1356 | .replace(/[/\\?%*:|"<>]/g, '-') 1357 | .replace(/([^/])\/([^/])/g, '$1_$2'); 1358 | } 1359 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-reddit-downloader", 3 | "version": "v0.3.2", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "easy-reddit-downloader", 9 | "version": "v0.3.2", 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "^1.6.0", 13 | "chalk": "^4.1.2", 14 | "child_process": "^1.0.2", 15 | "fluent-ffmpeg": "^2.1.2", 16 | "prompts": "^2.4.2", 17 | "request": "^2.88.2", 18 | "ytdl-core": "^4.11.4" 19 | } 20 | }, 21 | "node_modules/ajv": { 22 | "version": "6.12.6", 23 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 24 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 25 | "dependencies": { 26 | "fast-deep-equal": "^3.1.1", 27 | "fast-json-stable-stringify": "^2.0.0", 28 | "json-schema-traverse": "^0.4.1", 29 | "uri-js": "^4.2.2" 30 | }, 31 | "funding": { 32 | "type": "github", 33 | "url": "https://github.com/sponsors/epoberezkin" 34 | } 35 | }, 36 | "node_modules/ansi-styles": { 37 | "version": "4.3.0", 38 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 39 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 40 | "dependencies": { 41 | "color-convert": "^2.0.1" 42 | }, 43 | "engines": { 44 | "node": ">=8" 45 | }, 46 | "funding": { 47 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 48 | } 49 | }, 50 | "node_modules/asn1": { 51 | "version": "0.2.6", 52 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", 53 | "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", 54 | "dependencies": { 55 | "safer-buffer": "~2.1.0" 56 | } 57 | }, 58 | "node_modules/assert-plus": { 59 | "version": "1.0.0", 60 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 61 | "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", 62 | "engines": { 63 | "node": ">=0.8" 64 | } 65 | }, 66 | "node_modules/async": { 67 | "version": "3.2.4", 68 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", 69 | "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" 70 | }, 71 | "node_modules/asynckit": { 72 | "version": "0.4.0", 73 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 74 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 75 | }, 76 | "node_modules/aws-sign2": { 77 | "version": "0.7.0", 78 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 79 | "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", 80 | "engines": { 81 | "node": "*" 82 | } 83 | }, 84 | "node_modules/aws4": { 85 | "version": "1.11.0", 86 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", 87 | "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" 88 | }, 89 | "node_modules/axios": { 90 | "version": "1.6.0", 91 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", 92 | "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", 93 | "dependencies": { 94 | "follow-redirects": "^1.15.0", 95 | "form-data": "^4.0.0", 96 | "proxy-from-env": "^1.1.0" 97 | } 98 | }, 99 | "node_modules/bcrypt-pbkdf": { 100 | "version": "1.0.2", 101 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 102 | "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", 103 | "dependencies": { 104 | "tweetnacl": "^0.14.3" 105 | } 106 | }, 107 | "node_modules/caseless": { 108 | "version": "0.12.0", 109 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 110 | "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" 111 | }, 112 | "node_modules/chalk": { 113 | "version": "4.1.2", 114 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 115 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 116 | "dependencies": { 117 | "ansi-styles": "^4.1.0", 118 | "supports-color": "^7.1.0" 119 | }, 120 | "engines": { 121 | "node": ">=10" 122 | }, 123 | "funding": { 124 | "url": "https://github.com/chalk/chalk?sponsor=1" 125 | } 126 | }, 127 | "node_modules/child_process": { 128 | "version": "1.0.2", 129 | "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", 130 | "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" 131 | }, 132 | "node_modules/color-convert": { 133 | "version": "2.0.1", 134 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 135 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 136 | "dependencies": { 137 | "color-name": "~1.1.4" 138 | }, 139 | "engines": { 140 | "node": ">=7.0.0" 141 | } 142 | }, 143 | "node_modules/color-name": { 144 | "version": "1.1.4", 145 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 146 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 147 | }, 148 | "node_modules/combined-stream": { 149 | "version": "1.0.8", 150 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 151 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 152 | "dependencies": { 153 | "delayed-stream": "~1.0.0" 154 | }, 155 | "engines": { 156 | "node": ">= 0.8" 157 | } 158 | }, 159 | "node_modules/core-util-is": { 160 | "version": "1.0.2", 161 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 162 | "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" 163 | }, 164 | "node_modules/dashdash": { 165 | "version": "1.14.1", 166 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 167 | "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", 168 | "dependencies": { 169 | "assert-plus": "^1.0.0" 170 | }, 171 | "engines": { 172 | "node": ">=0.10" 173 | } 174 | }, 175 | "node_modules/delayed-stream": { 176 | "version": "1.0.0", 177 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 178 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 179 | "engines": { 180 | "node": ">=0.4.0" 181 | } 182 | }, 183 | "node_modules/ecc-jsbn": { 184 | "version": "0.1.2", 185 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 186 | "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", 187 | "dependencies": { 188 | "jsbn": "~0.1.0", 189 | "safer-buffer": "^2.1.0" 190 | } 191 | }, 192 | "node_modules/extend": { 193 | "version": "3.0.2", 194 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 195 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 196 | }, 197 | "node_modules/extsprintf": { 198 | "version": "1.3.0", 199 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 200 | "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", 201 | "engines": [ 202 | "node >=0.6.0" 203 | ] 204 | }, 205 | "node_modules/fast-deep-equal": { 206 | "version": "3.1.3", 207 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 208 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 209 | }, 210 | "node_modules/fast-json-stable-stringify": { 211 | "version": "2.1.0", 212 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 213 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 214 | }, 215 | "node_modules/fluent-ffmpeg": { 216 | "version": "2.1.2", 217 | "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", 218 | "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", 219 | "dependencies": { 220 | "async": ">=0.2.9", 221 | "which": "^1.1.1" 222 | }, 223 | "engines": { 224 | "node": ">=0.8.0" 225 | } 226 | }, 227 | "node_modules/follow-redirects": { 228 | "version": "1.15.6", 229 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 230 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", 231 | "funding": [ 232 | { 233 | "type": "individual", 234 | "url": "https://github.com/sponsors/RubenVerborgh" 235 | } 236 | ], 237 | "engines": { 238 | "node": ">=4.0" 239 | }, 240 | "peerDependenciesMeta": { 241 | "debug": { 242 | "optional": true 243 | } 244 | } 245 | }, 246 | "node_modules/forever-agent": { 247 | "version": "0.6.1", 248 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 249 | "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", 250 | "engines": { 251 | "node": "*" 252 | } 253 | }, 254 | "node_modules/form-data": { 255 | "version": "4.0.0", 256 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 257 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 258 | "dependencies": { 259 | "asynckit": "^0.4.0", 260 | "combined-stream": "^1.0.8", 261 | "mime-types": "^2.1.12" 262 | }, 263 | "engines": { 264 | "node": ">= 6" 265 | } 266 | }, 267 | "node_modules/getpass": { 268 | "version": "0.1.7", 269 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 270 | "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", 271 | "dependencies": { 272 | "assert-plus": "^1.0.0" 273 | } 274 | }, 275 | "node_modules/har-schema": { 276 | "version": "2.0.0", 277 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 278 | "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", 279 | "engines": { 280 | "node": ">=4" 281 | } 282 | }, 283 | "node_modules/har-validator": { 284 | "version": "5.1.5", 285 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 286 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 287 | "deprecated": "this library is no longer supported", 288 | "dependencies": { 289 | "ajv": "^6.12.3", 290 | "har-schema": "^2.0.0" 291 | }, 292 | "engines": { 293 | "node": ">=6" 294 | } 295 | }, 296 | "node_modules/has-flag": { 297 | "version": "4.0.0", 298 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 299 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 300 | "engines": { 301 | "node": ">=8" 302 | } 303 | }, 304 | "node_modules/http-signature": { 305 | "version": "1.2.0", 306 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 307 | "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", 308 | "dependencies": { 309 | "assert-plus": "^1.0.0", 310 | "jsprim": "^1.2.2", 311 | "sshpk": "^1.7.0" 312 | }, 313 | "engines": { 314 | "node": ">=0.8", 315 | "npm": ">=1.3.7" 316 | } 317 | }, 318 | "node_modules/is-typedarray": { 319 | "version": "1.0.0", 320 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 321 | "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" 322 | }, 323 | "node_modules/isexe": { 324 | "version": "2.0.0", 325 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 326 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 327 | }, 328 | "node_modules/isstream": { 329 | "version": "0.1.2", 330 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 331 | "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" 332 | }, 333 | "node_modules/jsbn": { 334 | "version": "0.1.1", 335 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 336 | "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" 337 | }, 338 | "node_modules/json-schema": { 339 | "version": "0.4.0", 340 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 341 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 342 | }, 343 | "node_modules/json-schema-traverse": { 344 | "version": "0.4.1", 345 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 346 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 347 | }, 348 | "node_modules/json-stringify-safe": { 349 | "version": "5.0.1", 350 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 351 | "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" 352 | }, 353 | "node_modules/jsprim": { 354 | "version": "1.4.2", 355 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 356 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 357 | "dependencies": { 358 | "assert-plus": "1.0.0", 359 | "extsprintf": "1.3.0", 360 | "json-schema": "0.4.0", 361 | "verror": "1.10.0" 362 | }, 363 | "engines": { 364 | "node": ">=0.6.0" 365 | } 366 | }, 367 | "node_modules/kleur": { 368 | "version": "3.0.3", 369 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 370 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", 371 | "engines": { 372 | "node": ">=6" 373 | } 374 | }, 375 | "node_modules/m3u8stream": { 376 | "version": "0.8.6", 377 | "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", 378 | "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", 379 | "dependencies": { 380 | "miniget": "^4.2.2", 381 | "sax": "^1.2.4" 382 | }, 383 | "engines": { 384 | "node": ">=12" 385 | } 386 | }, 387 | "node_modules/mime-db": { 388 | "version": "1.52.0", 389 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 390 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 391 | "engines": { 392 | "node": ">= 0.6" 393 | } 394 | }, 395 | "node_modules/mime-types": { 396 | "version": "2.1.35", 397 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 398 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 399 | "dependencies": { 400 | "mime-db": "1.52.0" 401 | }, 402 | "engines": { 403 | "node": ">= 0.6" 404 | } 405 | }, 406 | "node_modules/miniget": { 407 | "version": "4.2.2", 408 | "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.2.tgz", 409 | "integrity": "sha512-a7voNL1N5lDMxvTMExOkg+Fq89jM2vY8pAi9ZEWzZtfNmdfP6RXkvUtFnCAXoCv2T9k1v/fUJVaAEuepGcvLYA==", 410 | "engines": { 411 | "node": ">=12" 412 | } 413 | }, 414 | "node_modules/oauth-sign": { 415 | "version": "0.9.0", 416 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 417 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 418 | "engines": { 419 | "node": "*" 420 | } 421 | }, 422 | "node_modules/performance-now": { 423 | "version": "2.1.0", 424 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 425 | "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" 426 | }, 427 | "node_modules/prompts": { 428 | "version": "2.4.2", 429 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", 430 | "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", 431 | "dependencies": { 432 | "kleur": "^3.0.3", 433 | "sisteransi": "^1.0.5" 434 | }, 435 | "engines": { 436 | "node": ">= 6" 437 | } 438 | }, 439 | "node_modules/proxy-from-env": { 440 | "version": "1.1.0", 441 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 442 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 443 | }, 444 | "node_modules/psl": { 445 | "version": "1.9.0", 446 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", 447 | "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" 448 | }, 449 | "node_modules/punycode": { 450 | "version": "2.1.1", 451 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 452 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 453 | "engines": { 454 | "node": ">=6" 455 | } 456 | }, 457 | "node_modules/qs": { 458 | "version": "6.5.3", 459 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", 460 | "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", 461 | "engines": { 462 | "node": ">=0.6" 463 | } 464 | }, 465 | "node_modules/request": { 466 | "version": "2.88.2", 467 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 468 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 469 | "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", 470 | "dependencies": { 471 | "aws-sign2": "~0.7.0", 472 | "aws4": "^1.8.0", 473 | "caseless": "~0.12.0", 474 | "combined-stream": "~1.0.6", 475 | "extend": "~3.0.2", 476 | "forever-agent": "~0.6.1", 477 | "form-data": "~2.3.2", 478 | "har-validator": "~5.1.3", 479 | "http-signature": "~1.2.0", 480 | "is-typedarray": "~1.0.0", 481 | "isstream": "~0.1.2", 482 | "json-stringify-safe": "~5.0.1", 483 | "mime-types": "~2.1.19", 484 | "oauth-sign": "~0.9.0", 485 | "performance-now": "^2.1.0", 486 | "qs": "~6.5.2", 487 | "safe-buffer": "^5.1.2", 488 | "tough-cookie": "~2.5.0", 489 | "tunnel-agent": "^0.6.0", 490 | "uuid": "^3.3.2" 491 | }, 492 | "engines": { 493 | "node": ">= 6" 494 | } 495 | }, 496 | "node_modules/request/node_modules/form-data": { 497 | "version": "2.3.3", 498 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 499 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 500 | "dependencies": { 501 | "asynckit": "^0.4.0", 502 | "combined-stream": "^1.0.6", 503 | "mime-types": "^2.1.12" 504 | }, 505 | "engines": { 506 | "node": ">= 0.12" 507 | } 508 | }, 509 | "node_modules/safe-buffer": { 510 | "version": "5.2.1", 511 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 512 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 513 | "funding": [ 514 | { 515 | "type": "github", 516 | "url": "https://github.com/sponsors/feross" 517 | }, 518 | { 519 | "type": "patreon", 520 | "url": "https://www.patreon.com/feross" 521 | }, 522 | { 523 | "type": "consulting", 524 | "url": "https://feross.org/support" 525 | } 526 | ] 527 | }, 528 | "node_modules/safer-buffer": { 529 | "version": "2.1.2", 530 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 531 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 532 | }, 533 | "node_modules/sax": { 534 | "version": "1.2.4", 535 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 536 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 537 | }, 538 | "node_modules/sisteransi": { 539 | "version": "1.0.5", 540 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 541 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" 542 | }, 543 | "node_modules/sshpk": { 544 | "version": "1.17.0", 545 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", 546 | "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", 547 | "dependencies": { 548 | "asn1": "~0.2.3", 549 | "assert-plus": "^1.0.0", 550 | "bcrypt-pbkdf": "^1.0.0", 551 | "dashdash": "^1.12.0", 552 | "ecc-jsbn": "~0.1.1", 553 | "getpass": "^0.1.1", 554 | "jsbn": "~0.1.0", 555 | "safer-buffer": "^2.0.2", 556 | "tweetnacl": "~0.14.0" 557 | }, 558 | "bin": { 559 | "sshpk-conv": "bin/sshpk-conv", 560 | "sshpk-sign": "bin/sshpk-sign", 561 | "sshpk-verify": "bin/sshpk-verify" 562 | }, 563 | "engines": { 564 | "node": ">=0.10.0" 565 | } 566 | }, 567 | "node_modules/supports-color": { 568 | "version": "7.2.0", 569 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 570 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 571 | "dependencies": { 572 | "has-flag": "^4.0.0" 573 | }, 574 | "engines": { 575 | "node": ">=8" 576 | } 577 | }, 578 | "node_modules/tough-cookie": { 579 | "version": "2.5.0", 580 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 581 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 582 | "dependencies": { 583 | "psl": "^1.1.28", 584 | "punycode": "^2.1.1" 585 | }, 586 | "engines": { 587 | "node": ">=0.8" 588 | } 589 | }, 590 | "node_modules/tunnel-agent": { 591 | "version": "0.6.0", 592 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 593 | "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 594 | "dependencies": { 595 | "safe-buffer": "^5.0.1" 596 | }, 597 | "engines": { 598 | "node": "*" 599 | } 600 | }, 601 | "node_modules/tweetnacl": { 602 | "version": "0.14.5", 603 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 604 | "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" 605 | }, 606 | "node_modules/uri-js": { 607 | "version": "4.4.1", 608 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 609 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 610 | "dependencies": { 611 | "punycode": "^2.1.0" 612 | } 613 | }, 614 | "node_modules/uuid": { 615 | "version": "3.4.0", 616 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 617 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 618 | "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", 619 | "bin": { 620 | "uuid": "bin/uuid" 621 | } 622 | }, 623 | "node_modules/verror": { 624 | "version": "1.10.0", 625 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 626 | "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", 627 | "engines": [ 628 | "node >=0.6.0" 629 | ], 630 | "dependencies": { 631 | "assert-plus": "^1.0.0", 632 | "core-util-is": "1.0.2", 633 | "extsprintf": "^1.2.0" 634 | } 635 | }, 636 | "node_modules/which": { 637 | "version": "1.3.1", 638 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 639 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 640 | "dependencies": { 641 | "isexe": "^2.0.0" 642 | }, 643 | "bin": { 644 | "which": "bin/which" 645 | } 646 | }, 647 | "node_modules/ytdl-core": { 648 | "version": "4.11.4", 649 | "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.4.tgz", 650 | "integrity": "sha512-tsVvqt++B5LSTMnCKQb4H/PFBewKj7gGPJ6KIM5gOFGMKNZj4qglGAl4QGFG8cNPP6wY54P80FDID5eN2di0GQ==", 651 | "dependencies": { 652 | "m3u8stream": "^0.8.6", 653 | "miniget": "^4.2.2", 654 | "sax": "^1.1.3" 655 | }, 656 | "engines": { 657 | "node": ">=12" 658 | } 659 | } 660 | }, 661 | "dependencies": { 662 | "ajv": { 663 | "version": "6.12.6", 664 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 665 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 666 | "requires": { 667 | "fast-deep-equal": "^3.1.1", 668 | "fast-json-stable-stringify": "^2.0.0", 669 | "json-schema-traverse": "^0.4.1", 670 | "uri-js": "^4.2.2" 671 | } 672 | }, 673 | "ansi-styles": { 674 | "version": "4.3.0", 675 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 676 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 677 | "requires": { 678 | "color-convert": "^2.0.1" 679 | } 680 | }, 681 | "asn1": { 682 | "version": "0.2.6", 683 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", 684 | "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", 685 | "requires": { 686 | "safer-buffer": "~2.1.0" 687 | } 688 | }, 689 | "assert-plus": { 690 | "version": "1.0.0", 691 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 692 | "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" 693 | }, 694 | "async": { 695 | "version": "3.2.4", 696 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", 697 | "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" 698 | }, 699 | "asynckit": { 700 | "version": "0.4.0", 701 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 702 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 703 | }, 704 | "aws-sign2": { 705 | "version": "0.7.0", 706 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 707 | "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" 708 | }, 709 | "aws4": { 710 | "version": "1.11.0", 711 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", 712 | "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" 713 | }, 714 | "axios": { 715 | "version": "1.6.0", 716 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", 717 | "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", 718 | "requires": { 719 | "follow-redirects": "^1.15.0", 720 | "form-data": "^4.0.0", 721 | "proxy-from-env": "^1.1.0" 722 | } 723 | }, 724 | "bcrypt-pbkdf": { 725 | "version": "1.0.2", 726 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 727 | "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", 728 | "requires": { 729 | "tweetnacl": "^0.14.3" 730 | } 731 | }, 732 | "caseless": { 733 | "version": "0.12.0", 734 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 735 | "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" 736 | }, 737 | "chalk": { 738 | "version": "4.1.2", 739 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 740 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 741 | "requires": { 742 | "ansi-styles": "^4.1.0", 743 | "supports-color": "^7.1.0" 744 | } 745 | }, 746 | "child_process": { 747 | "version": "1.0.2", 748 | "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", 749 | "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" 750 | }, 751 | "color-convert": { 752 | "version": "2.0.1", 753 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 754 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 755 | "requires": { 756 | "color-name": "~1.1.4" 757 | } 758 | }, 759 | "color-name": { 760 | "version": "1.1.4", 761 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 762 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 763 | }, 764 | "combined-stream": { 765 | "version": "1.0.8", 766 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 767 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 768 | "requires": { 769 | "delayed-stream": "~1.0.0" 770 | } 771 | }, 772 | "core-util-is": { 773 | "version": "1.0.2", 774 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 775 | "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" 776 | }, 777 | "dashdash": { 778 | "version": "1.14.1", 779 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 780 | "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", 781 | "requires": { 782 | "assert-plus": "^1.0.0" 783 | } 784 | }, 785 | "delayed-stream": { 786 | "version": "1.0.0", 787 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 788 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 789 | }, 790 | "ecc-jsbn": { 791 | "version": "0.1.2", 792 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 793 | "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", 794 | "requires": { 795 | "jsbn": "~0.1.0", 796 | "safer-buffer": "^2.1.0" 797 | } 798 | }, 799 | "extend": { 800 | "version": "3.0.2", 801 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 802 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 803 | }, 804 | "extsprintf": { 805 | "version": "1.3.0", 806 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 807 | "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" 808 | }, 809 | "fast-deep-equal": { 810 | "version": "3.1.3", 811 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 812 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 813 | }, 814 | "fast-json-stable-stringify": { 815 | "version": "2.1.0", 816 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 817 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 818 | }, 819 | "fluent-ffmpeg": { 820 | "version": "2.1.2", 821 | "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", 822 | "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", 823 | "requires": { 824 | "async": ">=0.2.9", 825 | "which": "^1.1.1" 826 | } 827 | }, 828 | "follow-redirects": { 829 | "version": "1.15.6", 830 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 831 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" 832 | }, 833 | "forever-agent": { 834 | "version": "0.6.1", 835 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 836 | "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" 837 | }, 838 | "form-data": { 839 | "version": "4.0.0", 840 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 841 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 842 | "requires": { 843 | "asynckit": "^0.4.0", 844 | "combined-stream": "^1.0.8", 845 | "mime-types": "^2.1.12" 846 | } 847 | }, 848 | "getpass": { 849 | "version": "0.1.7", 850 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 851 | "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", 852 | "requires": { 853 | "assert-plus": "^1.0.0" 854 | } 855 | }, 856 | "har-schema": { 857 | "version": "2.0.0", 858 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 859 | "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" 860 | }, 861 | "har-validator": { 862 | "version": "5.1.5", 863 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 864 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 865 | "requires": { 866 | "ajv": "^6.12.3", 867 | "har-schema": "^2.0.0" 868 | } 869 | }, 870 | "has-flag": { 871 | "version": "4.0.0", 872 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 873 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 874 | }, 875 | "http-signature": { 876 | "version": "1.2.0", 877 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 878 | "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", 879 | "requires": { 880 | "assert-plus": "^1.0.0", 881 | "jsprim": "^1.2.2", 882 | "sshpk": "^1.7.0" 883 | } 884 | }, 885 | "is-typedarray": { 886 | "version": "1.0.0", 887 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 888 | "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" 889 | }, 890 | "isexe": { 891 | "version": "2.0.0", 892 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 893 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 894 | }, 895 | "isstream": { 896 | "version": "0.1.2", 897 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 898 | "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" 899 | }, 900 | "jsbn": { 901 | "version": "0.1.1", 902 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 903 | "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" 904 | }, 905 | "json-schema": { 906 | "version": "0.4.0", 907 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 908 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 909 | }, 910 | "json-schema-traverse": { 911 | "version": "0.4.1", 912 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 913 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 914 | }, 915 | "json-stringify-safe": { 916 | "version": "5.0.1", 917 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 918 | "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" 919 | }, 920 | "jsprim": { 921 | "version": "1.4.2", 922 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 923 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 924 | "requires": { 925 | "assert-plus": "1.0.0", 926 | "extsprintf": "1.3.0", 927 | "json-schema": "0.4.0", 928 | "verror": "1.10.0" 929 | } 930 | }, 931 | "kleur": { 932 | "version": "3.0.3", 933 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 934 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" 935 | }, 936 | "m3u8stream": { 937 | "version": "0.8.6", 938 | "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", 939 | "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", 940 | "requires": { 941 | "miniget": "^4.2.2", 942 | "sax": "^1.2.4" 943 | } 944 | }, 945 | "mime-db": { 946 | "version": "1.52.0", 947 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 948 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 949 | }, 950 | "mime-types": { 951 | "version": "2.1.35", 952 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 953 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 954 | "requires": { 955 | "mime-db": "1.52.0" 956 | } 957 | }, 958 | "miniget": { 959 | "version": "4.2.2", 960 | "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.2.tgz", 961 | "integrity": "sha512-a7voNL1N5lDMxvTMExOkg+Fq89jM2vY8pAi9ZEWzZtfNmdfP6RXkvUtFnCAXoCv2T9k1v/fUJVaAEuepGcvLYA==" 962 | }, 963 | "oauth-sign": { 964 | "version": "0.9.0", 965 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 966 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 967 | }, 968 | "performance-now": { 969 | "version": "2.1.0", 970 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 971 | "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" 972 | }, 973 | "prompts": { 974 | "version": "2.4.2", 975 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", 976 | "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", 977 | "requires": { 978 | "kleur": "^3.0.3", 979 | "sisteransi": "^1.0.5" 980 | } 981 | }, 982 | "proxy-from-env": { 983 | "version": "1.1.0", 984 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 985 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 986 | }, 987 | "psl": { 988 | "version": "1.9.0", 989 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", 990 | "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" 991 | }, 992 | "punycode": { 993 | "version": "2.1.1", 994 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 995 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 996 | }, 997 | "qs": { 998 | "version": "6.5.3", 999 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", 1000 | "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" 1001 | }, 1002 | "request": { 1003 | "version": "2.88.2", 1004 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 1005 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 1006 | "requires": { 1007 | "aws-sign2": "~0.7.0", 1008 | "aws4": "^1.8.0", 1009 | "caseless": "~0.12.0", 1010 | "combined-stream": "~1.0.6", 1011 | "extend": "~3.0.2", 1012 | "forever-agent": "~0.6.1", 1013 | "form-data": "~2.3.2", 1014 | "har-validator": "~5.1.3", 1015 | "http-signature": "~1.2.0", 1016 | "is-typedarray": "~1.0.0", 1017 | "isstream": "~0.1.2", 1018 | "json-stringify-safe": "~5.0.1", 1019 | "mime-types": "~2.1.19", 1020 | "oauth-sign": "~0.9.0", 1021 | "performance-now": "^2.1.0", 1022 | "qs": "~6.5.2", 1023 | "safe-buffer": "^5.1.2", 1024 | "tough-cookie": "~2.5.0", 1025 | "tunnel-agent": "^0.6.0", 1026 | "uuid": "^3.3.2" 1027 | }, 1028 | "dependencies": { 1029 | "form-data": { 1030 | "version": "2.3.3", 1031 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 1032 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 1033 | "requires": { 1034 | "asynckit": "^0.4.0", 1035 | "combined-stream": "^1.0.6", 1036 | "mime-types": "^2.1.12" 1037 | } 1038 | } 1039 | } 1040 | }, 1041 | "safe-buffer": { 1042 | "version": "5.2.1", 1043 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1044 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1045 | }, 1046 | "safer-buffer": { 1047 | "version": "2.1.2", 1048 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1049 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1050 | }, 1051 | "sax": { 1052 | "version": "1.2.4", 1053 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 1054 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 1055 | }, 1056 | "sisteransi": { 1057 | "version": "1.0.5", 1058 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 1059 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" 1060 | }, 1061 | "sshpk": { 1062 | "version": "1.17.0", 1063 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", 1064 | "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", 1065 | "requires": { 1066 | "asn1": "~0.2.3", 1067 | "assert-plus": "^1.0.0", 1068 | "bcrypt-pbkdf": "^1.0.0", 1069 | "dashdash": "^1.12.0", 1070 | "ecc-jsbn": "~0.1.1", 1071 | "getpass": "^0.1.1", 1072 | "jsbn": "~0.1.0", 1073 | "safer-buffer": "^2.0.2", 1074 | "tweetnacl": "~0.14.0" 1075 | } 1076 | }, 1077 | "supports-color": { 1078 | "version": "7.2.0", 1079 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1080 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1081 | "requires": { 1082 | "has-flag": "^4.0.0" 1083 | } 1084 | }, 1085 | "tough-cookie": { 1086 | "version": "2.5.0", 1087 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 1088 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 1089 | "requires": { 1090 | "psl": "^1.1.28", 1091 | "punycode": "^2.1.1" 1092 | } 1093 | }, 1094 | "tunnel-agent": { 1095 | "version": "0.6.0", 1096 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1097 | "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 1098 | "requires": { 1099 | "safe-buffer": "^5.0.1" 1100 | } 1101 | }, 1102 | "tweetnacl": { 1103 | "version": "0.14.5", 1104 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1105 | "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" 1106 | }, 1107 | "uri-js": { 1108 | "version": "4.4.1", 1109 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1110 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1111 | "requires": { 1112 | "punycode": "^2.1.0" 1113 | } 1114 | }, 1115 | "uuid": { 1116 | "version": "3.4.0", 1117 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 1118 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 1119 | }, 1120 | "verror": { 1121 | "version": "1.10.0", 1122 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1123 | "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", 1124 | "requires": { 1125 | "assert-plus": "^1.0.0", 1126 | "core-util-is": "1.0.2", 1127 | "extsprintf": "^1.2.0" 1128 | } 1129 | }, 1130 | "which": { 1131 | "version": "1.3.1", 1132 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1133 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1134 | "requires": { 1135 | "isexe": "^2.0.0" 1136 | } 1137 | }, 1138 | "ytdl-core": { 1139 | "version": "4.11.4", 1140 | "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.4.tgz", 1141 | "integrity": "sha512-tsVvqt++B5LSTMnCKQb4H/PFBewKj7gGPJ6KIM5gOFGMKNZj4qglGAl4QGFG8cNPP6wY54P80FDID5eN2di0GQ==", 1142 | "requires": { 1143 | "m3u8stream": "^0.8.6", 1144 | "miniget": "^4.2.2", 1145 | "sax": "^1.1.3" 1146 | } 1147 | } 1148 | } 1149 | } 1150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-reddit-downloader", 3 | "version": "v0.3.2", 4 | "description": "", 5 | "main": "script.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "Joseph R. Cox", 12 | "license": "MIT", 13 | "dependencies": { 14 | "axios": "^1.6.0", 15 | "chalk": "^4.1.2", 16 | "child_process": "^1.0.2", 17 | "fluent-ffmpeg": "^2.1.2", 18 | "prompts": "^2.4.2", 19 | "request": "^2.88.2", 20 | "ytdl-core": "^4.11.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /user_config_DEFAULT.json: -------------------------------------------------------------------------------- 1 | { 2 | "testingMode": false, 3 | "testingModeOptions": { 4 | "subredditList": ["AskReddit","pics"], 5 | "numberOfPosts": 25, 6 | "sorting": "new", 7 | "time": "month", 8 | "repeatForever": true, 9 | "timeBetweenRuns": 30000 10 | }, 11 | "download_post_list_options": { 12 | "enabled": false, 13 | "repeatForever": false, 14 | "timeBetweenRuns": 3000 15 | }, 16 | "local_logs": true, 17 | "local_logs_naming_scheme": { 18 | "showDateAndTime": true, 19 | "showSubreddits": true, 20 | "showNumberOfPosts": true 21 | }, 22 | "file_naming_scheme": { 23 | "showDateAndTime": true, 24 | "showAuthor": true, 25 | "showTitle": true, 26 | "showScore": true, 27 | "showSubreddit": true 28 | }, 29 | "download_self_posts": true, 30 | "download_media_posts": true, 31 | "download_link_posts": true, 32 | "download_gallery_posts": true, 33 | "download_youtube_videos_experimental": false, 34 | "download_comments": true, 35 | "separate_clean_nsfw": false, 36 | "redownload_posts": false, 37 | "detailed_logs": false 38 | } --------------------------------------------------------------------------------