├── .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 |
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 | }
--------------------------------------------------------------------------------