podcast-dl 8.0.8 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +97 -95
- package/bin/async.js +29 -11
- package/bin/bin.js +6 -0
- package/bin/commander.js +34 -3
- package/bin/logger.js +92 -92
- package/bin/naming.js +73 -72
- package/bin/util.js +24 -10
- package/bin/validate.js +43 -35
- package/package.json +62 -62
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020 Joshua Pohl
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Joshua Pohl
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,95 +1,97 @@
|
|
|
1
|
-
# podcast-dl
|
|
2
|
-
|
|
3
|
-
A CLI for downloading podcasts with a focus on archiving.
|
|
4
|
-
|
|
5
|
-
## How to Use
|
|
6
|
-
|
|
7
|
-
### npx
|
|
8
|
-
|
|
9
|
-
**[Node Required](https://nodejs.org/en/)**
|
|
10
|
-
|
|
11
|
-
`npx podcast-dl --url <PODCAST_RSS_URL>`
|
|
12
|
-
|
|
13
|
-
### Binaries
|
|
14
|
-
|
|
15
|
-
[Visit the releases page](https://github.com/lightpohl/podcast-dl/releases) and download the latest binary for your system.
|
|
16
|
-
|
|
17
|
-
`podcast-dl --url <PODCAST_RSS_URL>`
|
|
18
|
-
|
|
19
|
-
### [More Examples](./docs/examples.md)
|
|
20
|
-
|
|
21
|
-
## Options
|
|
22
|
-
|
|
23
|
-
Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
|
|
24
|
-
|
|
25
|
-
| Option | Type | Required | Description |
|
|
26
|
-
| ------------------------ | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
-
| --url | String | true | URL to podcast RSS feed. |
|
|
28
|
-
| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
|
|
29
|
-
| --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
|
|
30
|
-
| --
|
|
31
|
-
| --
|
|
32
|
-
| --
|
|
33
|
-
| --include-
|
|
34
|
-
| --include-episode-
|
|
35
|
-
| --
|
|
36
|
-
| --
|
|
37
|
-
| --
|
|
38
|
-
| --
|
|
39
|
-
| --
|
|
40
|
-
| --episode-
|
|
41
|
-
| --
|
|
42
|
-
| --
|
|
43
|
-
| --
|
|
44
|
-
| --
|
|
45
|
-
| --
|
|
46
|
-
| --
|
|
47
|
-
| --
|
|
48
|
-
| --
|
|
49
|
-
| --
|
|
50
|
-
| --
|
|
51
|
-
| --
|
|
52
|
-
| --
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`--
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
- `
|
|
77
|
-
- `
|
|
78
|
-
- `
|
|
79
|
-
- `
|
|
80
|
-
- `
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
- `
|
|
1
|
+
# podcast-dl
|
|
2
|
+
|
|
3
|
+
A CLI for downloading podcasts with a focus on archiving.
|
|
4
|
+
|
|
5
|
+
## How to Use
|
|
6
|
+
|
|
7
|
+
### npx
|
|
8
|
+
|
|
9
|
+
**[Node Required](https://nodejs.org/en/)**
|
|
10
|
+
|
|
11
|
+
`npx podcast-dl --url <PODCAST_RSS_URL>`
|
|
12
|
+
|
|
13
|
+
### Binaries
|
|
14
|
+
|
|
15
|
+
[Visit the releases page](https://github.com/lightpohl/podcast-dl/releases) and download the latest binary for your system.
|
|
16
|
+
|
|
17
|
+
`podcast-dl --url <PODCAST_RSS_URL>`
|
|
18
|
+
|
|
19
|
+
### [More Examples](./docs/examples.md)
|
|
20
|
+
|
|
21
|
+
## Options
|
|
22
|
+
|
|
23
|
+
Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
|
|
24
|
+
|
|
25
|
+
| Option | Type | Required | Description |
|
|
26
|
+
| ------------------------ | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
+
| --url | String | true | URL to podcast RSS feed. |
|
|
28
|
+
| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
|
|
29
|
+
| --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
|
|
30
|
+
| --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. |
|
|
31
|
+
| --archive | [String] | false | Download or write out items not listed in archive file. Generates archive file at path if not found. Defaults to "./{{podcast_title}}/archive.json" when used as a boolean option. See "Template Options" for more details. |
|
|
32
|
+
| --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
|
|
33
|
+
| --include-meta | | false | Write out podcast metadata to JSON. |
|
|
34
|
+
| --include-episode-meta | | false | Write out individual episode metadata to JSON. |
|
|
35
|
+
| --include-episode-images | | false | Download found episode images. |
|
|
36
|
+
| --offset | Number | false | Offset starting download position. Default is 0. |
|
|
37
|
+
| --limit | Number | false | Max number of episodes to download. Downloads all by default. |
|
|
38
|
+
| --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
|
|
39
|
+
| --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
|
|
40
|
+
| --episode-regex | String | false | Match episode title against provided regex before starting download. |
|
|
41
|
+
| --episode-digits | Number | false | Minimum number of digits to use for episode numbering (e.g. 3 would generate "001" instead of "1"). Default is 0. |
|
|
42
|
+
| --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". |
|
|
43
|
+
| --add-mp3-metadata | | false | Attempts to add a base level of MP3 metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
|
|
44
|
+
| --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of MP3s. (**ffmpeg required**) |
|
|
45
|
+
| --mono | | false | Attempts to force MP3s into mono. (**ffmpeg required**) |
|
|
46
|
+
| --override | | false | Override local files on collision. |
|
|
47
|
+
| --reverse | | false | Reverse download direction and start at last RSS item. |
|
|
48
|
+
| --info | | false | Print retrieved podcast info instead of downloading. |
|
|
49
|
+
| --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
|
|
50
|
+
| --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
|
|
51
|
+
| --parser-config | String | false | Path to JSON file that will be parsed and used to override the default config passed to [rss-parser](https://github.com/rbren/rss-parser#xml-options). |
|
|
52
|
+
| --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
|
|
53
|
+
| --version | | false | Output the version number. |
|
|
54
|
+
| --help | | false | Output usage information. |
|
|
55
|
+
|
|
56
|
+
## Archive
|
|
57
|
+
|
|
58
|
+
- If passed the `--archive [path]` option, `podcast-dl` will generate/use a JSON archive at the provided path.
|
|
59
|
+
- Before downloading an episode or writing out metadata, it'll check if the item was saved previously and abort the save if found.
|
|
60
|
+
|
|
61
|
+
## Template Options
|
|
62
|
+
|
|
63
|
+
Options that support templates allow users to specify a template for the generated filename(s) or option. The provided template will replace all matched keywords with the related data described below. Each keyword must be wrapped in two braces like so:
|
|
64
|
+
|
|
65
|
+
`--out-dir "./{{podcast_title}}"`
|
|
66
|
+
|
|
67
|
+
`--episode-template "{{release_date}}-{{title}}"`
|
|
68
|
+
|
|
69
|
+
### `--out-dir` & `--archive`
|
|
70
|
+
|
|
71
|
+
- `podcast_title`: Title of the podcast feed.
|
|
72
|
+
- `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
|
|
73
|
+
|
|
74
|
+
### `--episode-template`
|
|
75
|
+
|
|
76
|
+
- `title`: The title of the episode.
|
|
77
|
+
- `release_date`: The release date of the episode in `YYYYMMDD` format.
|
|
78
|
+
- `episode_num`: The location number of where the episodes appears in the feed.
|
|
79
|
+
- `url`: URL of episode audio file.
|
|
80
|
+
- `duration`: Provided `mm:ss` duration (if found).
|
|
81
|
+
- `podcast_title`: Title of the podcast feed.
|
|
82
|
+
- `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
|
|
83
|
+
|
|
84
|
+
### `--exec`
|
|
85
|
+
|
|
86
|
+
- `episode_path`: The path to the downloaded episode.
|
|
87
|
+
- `episode_path_base`: The path to the folder of the downloaded episode.
|
|
88
|
+
- `episode_filname`: The filename of the episode.
|
|
89
|
+
- `episode_filename_base`: The filename of the episode without its extension.
|
|
90
|
+
|
|
91
|
+
## Log Levels
|
|
92
|
+
|
|
93
|
+
By default, all logs and errors are outputted to the console. The amount of logs can be controlled using the environment variable `LOG_LEVEL` with the following options:
|
|
94
|
+
|
|
95
|
+
- `static`: All logs and errors are outputted to the console, but disables any animations.
|
|
96
|
+
- `quiet`: Only important info and non-critical errors will be logged (e.g. episode download started).
|
|
97
|
+
- `silent`: Only critical error messages will be be logged.
|
package/bin/async.js
CHANGED
|
@@ -28,15 +28,19 @@ const pipeline = promisify(stream.pipeline);
|
|
|
28
28
|
|
|
29
29
|
const BYTES_IN_MB = 1000000;
|
|
30
30
|
|
|
31
|
-
const download = async ({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
const download = async (options) => {
|
|
32
|
+
const {
|
|
33
|
+
marker,
|
|
34
|
+
url,
|
|
35
|
+
outputPath,
|
|
36
|
+
key,
|
|
37
|
+
archive,
|
|
38
|
+
override,
|
|
39
|
+
onAfterDownload,
|
|
40
|
+
attempt = 1,
|
|
41
|
+
maxAttempts = 3,
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
40
44
|
const logMessage = getLogMessageWithMarker(marker);
|
|
41
45
|
if (!override && fs.existsSync(outputPath)) {
|
|
42
46
|
logMessage("Download exists locally. Skipping...");
|
|
@@ -100,7 +104,17 @@ const download = async ({
|
|
|
100
104
|
);
|
|
101
105
|
} catch (error) {
|
|
102
106
|
removeFile();
|
|
103
|
-
|
|
107
|
+
|
|
108
|
+
if (attempt <= maxAttempts) {
|
|
109
|
+
logMessage(`Download attempt #${attempt} failed. Retrying...`);
|
|
110
|
+
|
|
111
|
+
await download({
|
|
112
|
+
...options,
|
|
113
|
+
attempt: attempt + 1,
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
const fileSize = fs.statSync(tempOutputPath).size;
|
|
@@ -137,10 +151,12 @@ let downloadItemsAsync = async ({
|
|
|
137
151
|
addMp3MetadataFlag,
|
|
138
152
|
archive,
|
|
139
153
|
archiveUrl,
|
|
154
|
+
attempts,
|
|
140
155
|
basePath,
|
|
141
156
|
bitrate,
|
|
142
157
|
episodeTemplate,
|
|
143
158
|
episodeDigits,
|
|
159
|
+
episodeSourceOrder,
|
|
144
160
|
exec,
|
|
145
161
|
feed,
|
|
146
162
|
includeEpisodeMeta,
|
|
@@ -158,7 +174,7 @@ let downloadItemsAsync = async ({
|
|
|
158
174
|
const marker = threads > 1 ? `[${threadIndex}] ${item.title}` : item.title;
|
|
159
175
|
const logMessage = getLogMessageWithMarker(marker);
|
|
160
176
|
const { url: episodeAudioUrl, ext: audioFileExt } =
|
|
161
|
-
getEpisodeAudioUrlAndExt(item);
|
|
177
|
+
getEpisodeAudioUrlAndExt(item, episodeSourceOrder);
|
|
162
178
|
|
|
163
179
|
if (!episodeAudioUrl) {
|
|
164
180
|
hasErrors = true;
|
|
@@ -189,6 +205,7 @@ let downloadItemsAsync = async ({
|
|
|
189
205
|
ext: audioFileExt,
|
|
190
206
|
}),
|
|
191
207
|
}),
|
|
208
|
+
maxAttempts: attempts,
|
|
192
209
|
outputPath: outputPodcastPath,
|
|
193
210
|
url: episodeAudioUrl,
|
|
194
211
|
onAfterDownload: async () => {
|
|
@@ -228,6 +245,7 @@ let downloadItemsAsync = async ({
|
|
|
228
245
|
archive,
|
|
229
246
|
override,
|
|
230
247
|
marker: extra.url,
|
|
248
|
+
maxAttempts: attempts,
|
|
231
249
|
key: extra.key,
|
|
232
250
|
outputPath: extra.outputPath,
|
|
233
251
|
url: extra.url,
|
package/bin/bin.js
CHANGED
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
outDir,
|
|
36
36
|
episodeTemplate,
|
|
37
37
|
episodeDigits,
|
|
38
|
+
episodeSourceOrder,
|
|
38
39
|
includeMeta,
|
|
39
40
|
includeEpisodeMeta,
|
|
40
41
|
includeEpisodeImages,
|
|
@@ -50,6 +51,7 @@ const {
|
|
|
50
51
|
exec,
|
|
51
52
|
mono,
|
|
52
53
|
threads,
|
|
54
|
+
attempts,
|
|
53
55
|
parserConfig,
|
|
54
56
|
proxy,
|
|
55
57
|
addMp3Metadata: addMp3MetadataFlag,
|
|
@@ -138,6 +140,7 @@ const main = async () => {
|
|
|
138
140
|
key: getArchiveKey({ prefix: archiveUrl, name: podcastImageName }),
|
|
139
141
|
outputPath: outputImagePath,
|
|
140
142
|
url: podcastImageUrl,
|
|
143
|
+
maxAttempts: attempts,
|
|
141
144
|
});
|
|
142
145
|
} catch (error) {
|
|
143
146
|
logError("Unable to download podcast image", error);
|
|
@@ -181,6 +184,7 @@ const main = async () => {
|
|
|
181
184
|
before,
|
|
182
185
|
episodeDigits,
|
|
183
186
|
episodeRegex,
|
|
187
|
+
episodeSourceOrder,
|
|
184
188
|
episodeTemplate,
|
|
185
189
|
includeEpisodeImages,
|
|
186
190
|
});
|
|
@@ -197,10 +201,12 @@ const main = async () => {
|
|
|
197
201
|
addMp3MetadataFlag,
|
|
198
202
|
archive,
|
|
199
203
|
archiveUrl,
|
|
204
|
+
attempts,
|
|
200
205
|
basePath,
|
|
201
206
|
bitrate,
|
|
202
207
|
episodeTemplate,
|
|
203
208
|
episodeDigits,
|
|
209
|
+
episodeSourceOrder,
|
|
204
210
|
exec,
|
|
205
211
|
feed,
|
|
206
212
|
includeEpisodeMeta,
|
package/bin/commander.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ITEM_LIST_FORMATS } from "./util.js";
|
|
1
|
+
import { AUDIO_ORDER_TYPES, ITEM_LIST_FORMATS } from "./util.js";
|
|
2
2
|
import { createParseNumber, hasFfmpeg } from "./validate.js";
|
|
3
3
|
import { logErrorAndExit } from "./logger.js";
|
|
4
4
|
|
|
5
5
|
export const setupCommander = (commander, argv) => {
|
|
6
6
|
commander
|
|
7
|
-
.version("
|
|
7
|
+
.version("9.0.1")
|
|
8
8
|
.option("--url <string>", "url to podcast rss feed")
|
|
9
9
|
.option(
|
|
10
10
|
"--out-dir <path>",
|
|
@@ -26,6 +26,23 @@ export const setupCommander = (commander, argv) => {
|
|
|
26
26
|
createParseNumber({ min: 0, name: "--episode-digits" }),
|
|
27
27
|
1
|
|
28
28
|
)
|
|
29
|
+
.option(
|
|
30
|
+
"--episode-source-order <string>",
|
|
31
|
+
"attempted order to extract episode audio URL from rss feed",
|
|
32
|
+
(value) => {
|
|
33
|
+
const parsed = value.split(",").map((type) => type.trim());
|
|
34
|
+
const isValid = parsed.every((type) => !!AUDIO_ORDER_TYPES[type]);
|
|
35
|
+
|
|
36
|
+
if (!isValid) {
|
|
37
|
+
logErrorAndExit(
|
|
38
|
+
`Invalid type found in --episode-source-order: ${value}\n`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return parsed;
|
|
43
|
+
},
|
|
44
|
+
[AUDIO_ORDER_TYPES.enclosure, AUDIO_ORDER_TYPES.link]
|
|
45
|
+
)
|
|
29
46
|
.option("--include-meta", "write out podcast metadata to json")
|
|
30
47
|
.option(
|
|
31
48
|
"--include-episode-meta",
|
|
@@ -95,9 +112,23 @@ export const setupCommander = (commander, argv) => {
|
|
|
95
112
|
.option(
|
|
96
113
|
"--threads <number>",
|
|
97
114
|
"the number of downloads that can happen concurrently",
|
|
98
|
-
createParseNumber({
|
|
115
|
+
createParseNumber({
|
|
116
|
+
min: 1,
|
|
117
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
118
|
+
name: "threads",
|
|
119
|
+
}),
|
|
99
120
|
1
|
|
100
121
|
)
|
|
122
|
+
.option(
|
|
123
|
+
"--attempts <number>",
|
|
124
|
+
"the number of attempts for an individual download",
|
|
125
|
+
createParseNumber({
|
|
126
|
+
min: 1,
|
|
127
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
128
|
+
name: "attempts",
|
|
129
|
+
}),
|
|
130
|
+
3
|
|
131
|
+
)
|
|
101
132
|
.option(
|
|
102
133
|
"--parser-config <string>",
|
|
103
134
|
"path to JSON config to override RSS parser"
|
package/bin/logger.js
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
const ERROR_STATUSES = {
|
|
2
|
-
general: 1,
|
|
3
|
-
nothingDownloaded: 2,
|
|
4
|
-
completedWithErrors: 3,
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const LOG_LEVEL_TYPES = {
|
|
8
|
-
debug: "debug",
|
|
9
|
-
quiet: "quiet",
|
|
10
|
-
silent: "silent",
|
|
11
|
-
static: "static",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const LOG_LEVELS = {
|
|
15
|
-
debug: 0,
|
|
16
|
-
info: 1,
|
|
17
|
-
important: 2,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const getShouldOutputProgressIndicator = () => {
|
|
21
|
-
return (
|
|
22
|
-
process.stdout.isTTY &&
|
|
23
|
-
process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.static &&
|
|
24
|
-
process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.quiet &&
|
|
25
|
-
process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.silent
|
|
26
|
-
);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const logMessage = (message = "", logLevel = 1) => {
|
|
30
|
-
if (
|
|
31
|
-
!process.env.LOG_LEVEL ||
|
|
32
|
-
process.env.LOG_LEVEL === LOG_LEVEL_TYPES.debug ||
|
|
33
|
-
process.env.LOG_LEVEL === LOG_LEVEL_TYPES.static
|
|
34
|
-
) {
|
|
35
|
-
console.log(message);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
process.env.LOG_LEVEL === LOG_LEVEL_TYPES.quiet &&
|
|
45
|
-
logLevel > LOG_LEVELS.info
|
|
46
|
-
) {
|
|
47
|
-
console.log(message);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const getLogMessageWithMarker = (marker) => {
|
|
53
|
-
return (message, logLevel) => {
|
|
54
|
-
if (marker) {
|
|
55
|
-
logMessage(`${marker} | ${message}`, logLevel);
|
|
56
|
-
} else {
|
|
57
|
-
logMessage(message, logLevel);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const logError = (msg, error) => {
|
|
63
|
-
if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
console.error(msg);
|
|
68
|
-
|
|
69
|
-
if (error) {
|
|
70
|
-
console.error(error.message);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const logErrorAndExit = (msg, error) => {
|
|
75
|
-
console.error(msg);
|
|
76
|
-
|
|
77
|
-
if (error) {
|
|
78
|
-
console.error(error.message);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
process.exit(ERROR_STATUSES.general);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export {
|
|
85
|
-
ERROR_STATUSES,
|
|
86
|
-
getShouldOutputProgressIndicator,
|
|
87
|
-
getLogMessageWithMarker,
|
|
88
|
-
LOG_LEVELS,
|
|
89
|
-
logMessage,
|
|
90
|
-
logError,
|
|
91
|
-
logErrorAndExit,
|
|
92
|
-
};
|
|
1
|
+
const ERROR_STATUSES = {
|
|
2
|
+
general: 1,
|
|
3
|
+
nothingDownloaded: 2,
|
|
4
|
+
completedWithErrors: 3,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const LOG_LEVEL_TYPES = {
|
|
8
|
+
debug: "debug",
|
|
9
|
+
quiet: "quiet",
|
|
10
|
+
silent: "silent",
|
|
11
|
+
static: "static",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const LOG_LEVELS = {
|
|
15
|
+
debug: 0,
|
|
16
|
+
info: 1,
|
|
17
|
+
important: 2,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getShouldOutputProgressIndicator = () => {
|
|
21
|
+
return (
|
|
22
|
+
process.stdout.isTTY &&
|
|
23
|
+
process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.static &&
|
|
24
|
+
process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.quiet &&
|
|
25
|
+
process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.silent
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const logMessage = (message = "", logLevel = 1) => {
|
|
30
|
+
if (
|
|
31
|
+
!process.env.LOG_LEVEL ||
|
|
32
|
+
process.env.LOG_LEVEL === LOG_LEVEL_TYPES.debug ||
|
|
33
|
+
process.env.LOG_LEVEL === LOG_LEVEL_TYPES.static
|
|
34
|
+
) {
|
|
35
|
+
console.log(message);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
process.env.LOG_LEVEL === LOG_LEVEL_TYPES.quiet &&
|
|
45
|
+
logLevel > LOG_LEVELS.info
|
|
46
|
+
) {
|
|
47
|
+
console.log(message);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getLogMessageWithMarker = (marker) => {
|
|
53
|
+
return (message, logLevel) => {
|
|
54
|
+
if (marker) {
|
|
55
|
+
logMessage(`${marker} | ${message}`, logLevel);
|
|
56
|
+
} else {
|
|
57
|
+
logMessage(message, logLevel);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const logError = (msg, error) => {
|
|
63
|
+
if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.error(msg);
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
console.error(error.message);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const logErrorAndExit = (msg, error) => {
|
|
75
|
+
console.error(msg);
|
|
76
|
+
|
|
77
|
+
if (error) {
|
|
78
|
+
console.error(error.message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
process.exit(ERROR_STATUSES.general);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
ERROR_STATUSES,
|
|
86
|
+
getShouldOutputProgressIndicator,
|
|
87
|
+
getLogMessageWithMarker,
|
|
88
|
+
LOG_LEVELS,
|
|
89
|
+
logMessage,
|
|
90
|
+
logError,
|
|
91
|
+
logErrorAndExit,
|
|
92
|
+
};
|
package/bin/naming.js
CHANGED
|
@@ -1,72 +1,73 @@
|
|
|
1
|
-
import filenamify from "filenamify";
|
|
2
|
-
import dayjs from "dayjs";
|
|
3
|
-
|
|
4
|
-
const INVALID_CHAR_REPLACE = "_";
|
|
5
|
-
const MAX_LENGTH_FILENAME =
|
|
6
|
-
|
|
7
|
-
const getSafeName = (name) => {
|
|
8
|
-
return filenamify(name, {
|
|
9
|
-
replacement: INVALID_CHAR_REPLACE,
|
|
10
|
-
maxLength: MAX_LENGTH_FILENAME,
|
|
11
|
-
});
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const getFilename = ({ item, ext, url, feed, template, width }) => {
|
|
15
|
-
const episodeNum = feed.items.length - item._originalIndex;
|
|
16
|
-
const formattedPubDate = item.pubDate
|
|
17
|
-
? dayjs(new Date(item.pubDate)).format("YYYYMMDD")
|
|
18
|
-
: null;
|
|
19
|
-
|
|
20
|
-
const templateReplacementsTuples = [
|
|
21
|
-
["title", item.title || ""],
|
|
22
|
-
["release_date", formattedPubDate || ""],
|
|
23
|
-
["episode_num", `${episodeNum}`.padStart(width, "0")],
|
|
24
|
-
["url", url],
|
|
25
|
-
["podcast_title", feed.title || ""],
|
|
26
|
-
["podcast_link", feed.link || ""],
|
|
27
|
-
["duration", (item.itunes && item.itunes.duration) || ""],
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
let name = template;
|
|
31
|
-
templateReplacementsTuples.forEach((replacementTuple) => {
|
|
32
|
-
const [matcher, replacement] = replacementTuple;
|
|
33
|
-
const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
|
|
34
|
-
|
|
35
|
-
name = replacement
|
|
36
|
-
? name.replace(replaceRegex, replacement)
|
|
37
|
-
: name.replace(replaceRegex, "");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
name = `${name}${ext}`;
|
|
41
|
-
return getSafeName(name);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const getFolderName = ({ feed, template }) => {
|
|
45
|
-
const templateReplacementsTuples = [
|
|
46
|
-
["podcast_title", feed.title || ""],
|
|
47
|
-
["podcast_link", feed.link || ""],
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
let name = template;
|
|
51
|
-
templateReplacementsTuples.forEach((replacementTuple) => {
|
|
52
|
-
const [matcher, replacement] = replacementTuple;
|
|
53
|
-
const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
|
|
54
|
-
|
|
55
|
-
name = replacement
|
|
56
|
-
? name.replace(replaceRegex, getSafeName(replacement))
|
|
57
|
-
: name.replace(replaceRegex, "");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return name;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const getArchiveFilename = ({ pubDate, name, ext }) => {
|
|
64
|
-
const formattedPubDate = pubDate
|
|
65
|
-
? dayjs(new Date(pubDate)).format("YYYYMMDD")
|
|
66
|
-
: null;
|
|
67
|
-
|
|
68
|
-
const baseName = formattedPubDate ? `${formattedPubDate}-${name}` : name;
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
1
|
+
import filenamify from "filenamify";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
|
|
4
|
+
const INVALID_CHAR_REPLACE = "_";
|
|
5
|
+
const MAX_LENGTH_FILENAME = 251; // account for ".tmp" files
|
|
6
|
+
|
|
7
|
+
const getSafeName = (name) => {
|
|
8
|
+
return filenamify(name, {
|
|
9
|
+
replacement: INVALID_CHAR_REPLACE,
|
|
10
|
+
maxLength: MAX_LENGTH_FILENAME,
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getFilename = ({ item, ext, url, feed, template, width }) => {
|
|
15
|
+
const episodeNum = feed.items.length - item._originalIndex;
|
|
16
|
+
const formattedPubDate = item.pubDate
|
|
17
|
+
? dayjs(new Date(item.pubDate)).format("YYYYMMDD")
|
|
18
|
+
: null;
|
|
19
|
+
|
|
20
|
+
const templateReplacementsTuples = [
|
|
21
|
+
["title", item.title || ""],
|
|
22
|
+
["release_date", formattedPubDate || ""],
|
|
23
|
+
["episode_num", `${episodeNum}`.padStart(width, "0")],
|
|
24
|
+
["url", url],
|
|
25
|
+
["podcast_title", feed.title || ""],
|
|
26
|
+
["podcast_link", feed.link || ""],
|
|
27
|
+
["duration", (item.itunes && item.itunes.duration) || ""],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
let name = template;
|
|
31
|
+
templateReplacementsTuples.forEach((replacementTuple) => {
|
|
32
|
+
const [matcher, replacement] = replacementTuple;
|
|
33
|
+
const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
|
|
34
|
+
|
|
35
|
+
name = replacement
|
|
36
|
+
? name.replace(replaceRegex, replacement)
|
|
37
|
+
: name.replace(replaceRegex, "");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
name = `${name}${ext}`;
|
|
41
|
+
return getSafeName(name);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getFolderName = ({ feed, template }) => {
|
|
45
|
+
const templateReplacementsTuples = [
|
|
46
|
+
["podcast_title", feed.title || ""],
|
|
47
|
+
["podcast_link", feed.link || ""],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
let name = template;
|
|
51
|
+
templateReplacementsTuples.forEach((replacementTuple) => {
|
|
52
|
+
const [matcher, replacement] = replacementTuple;
|
|
53
|
+
const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
|
|
54
|
+
|
|
55
|
+
name = replacement
|
|
56
|
+
? name.replace(replaceRegex, getSafeName(replacement))
|
|
57
|
+
: name.replace(replaceRegex, "");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return name;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getArchiveFilename = ({ pubDate, name, ext }) => {
|
|
64
|
+
const formattedPubDate = pubDate
|
|
65
|
+
? dayjs(new Date(pubDate)).format("YYYYMMDD")
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
const baseName = formattedPubDate ? `${formattedPubDate}-${name}` : name;
|
|
69
|
+
|
|
70
|
+
return `${baseName}${ext}`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export { getArchiveFilename, getFilename, getFolderName, getSafeName };
|
package/bin/util.js
CHANGED
|
@@ -104,6 +104,7 @@ const getItemsToDownload = ({
|
|
|
104
104
|
after,
|
|
105
105
|
episodeDigits,
|
|
106
106
|
episodeRegex,
|
|
107
|
+
episodeSourceOrder,
|
|
107
108
|
episodeTemplate,
|
|
108
109
|
includeEpisodeImages,
|
|
109
110
|
}) => {
|
|
@@ -151,7 +152,7 @@ const getItemsToDownload = ({
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
const { url: episodeAudioUrl, ext: audioFileExt } =
|
|
154
|
-
getEpisodeAudioUrlAndExt(feed.items[i]);
|
|
155
|
+
getEpisodeAudioUrlAndExt(feed.items[i], episodeSourceOrder);
|
|
155
156
|
const key = getArchiveKey({
|
|
156
157
|
prefix: archiveUrl,
|
|
157
158
|
name: getArchiveFilename({
|
|
@@ -369,17 +370,29 @@ const getIsAudioUrl = (url) => {
|
|
|
369
370
|
return VALID_AUDIO_EXTS.includes(ext);
|
|
370
371
|
};
|
|
371
372
|
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
const AUDIO_ORDER_TYPES = {
|
|
374
|
+
enclosure: "enclosure",
|
|
375
|
+
link: "link",
|
|
376
|
+
};
|
|
376
377
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
const getEpisodeAudioUrlAndExt = (
|
|
379
|
+
{ enclosure, link },
|
|
380
|
+
order = [AUDIO_ORDER_TYPES.enclosure, AUDIO_ORDER_TYPES.link]
|
|
381
|
+
) => {
|
|
382
|
+
for (const source of order) {
|
|
383
|
+
if (source === AUDIO_ORDER_TYPES.link && link && getIsAudioUrl(link)) {
|
|
384
|
+
return { url: link, ext: getUrlExt(link) };
|
|
385
|
+
}
|
|
380
386
|
|
|
381
|
-
|
|
382
|
-
|
|
387
|
+
if (source === AUDIO_ORDER_TYPES.enclosure && enclosure) {
|
|
388
|
+
if (getIsAudioUrl(enclosure.url)) {
|
|
389
|
+
return { url: enclosure.url, ext: getUrlExt(enclosure.url) };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (enclosure.url && AUDIO_TYPES_TO_EXTS[enclosure.type]) {
|
|
393
|
+
return { url: enclosure.url, ext: AUDIO_TYPES_TO_EXTS[enclosure.type] };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
383
396
|
}
|
|
384
397
|
|
|
385
398
|
return { url: null, ext: null };
|
|
@@ -527,6 +540,7 @@ const runExec = async ({
|
|
|
527
540
|
};
|
|
528
541
|
|
|
529
542
|
export {
|
|
543
|
+
AUDIO_ORDER_TYPES,
|
|
530
544
|
getArchive,
|
|
531
545
|
getIsInArchive,
|
|
532
546
|
getArchiveKey,
|
package/bin/validate.js
CHANGED
|
@@ -1,35 +1,43 @@
|
|
|
1
|
-
import { sync as commandExistsSync } from "command-exists";
|
|
2
|
-
|
|
3
|
-
import { logErrorAndExit } from "./logger.js";
|
|
4
|
-
|
|
5
|
-
const createParseNumber = ({ min, name, required = true }) => {
|
|
6
|
-
return (value) => {
|
|
7
|
-
if (!value && !required) {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
let number = parseInt(value);
|
|
13
|
-
|
|
14
|
-
if (isNaN(number)) {
|
|
15
|
-
logErrorAndExit(`${name} must be a number`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (number < min) {
|
|
19
|
-
logErrorAndExit(`${name} must be >= ${min}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
import { sync as commandExistsSync } from "command-exists";
|
|
2
|
+
|
|
3
|
+
import { logErrorAndExit } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
const createParseNumber = ({ min, max, name, required = true }) => {
|
|
6
|
+
return (value) => {
|
|
7
|
+
if (!value && !required) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
let number = parseInt(value);
|
|
13
|
+
|
|
14
|
+
if (isNaN(number)) {
|
|
15
|
+
logErrorAndExit(`${name} must be a number`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof min !== undefined && number < min) {
|
|
19
|
+
logErrorAndExit(`${name} must be >= ${min}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof max !== undefined && number > max) {
|
|
23
|
+
logErrorAndExit(
|
|
24
|
+
`${name} must be <= ${
|
|
25
|
+
max === Number.MAX_SAFE_INTEGER ? "Number.MAX_SAFE_INTEGER" : max
|
|
26
|
+
}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return number;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logErrorAndExit(`Unable to parse ${name}`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const hasFfmpeg = () => {
|
|
38
|
+
if (!commandExistsSync("ffmpeg")) {
|
|
39
|
+
logErrorAndExit('option specified requires "ffmpeg" be available');
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { createParseNumber, hasFfmpeg };
|
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "podcast-dl",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A CLI for downloading podcasts.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": "./bin/bin.js",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "rimraf ./binaries && rimraf ./dist && node build.cjs",
|
|
9
|
-
"lint": "eslint ./bin"
|
|
10
|
-
},
|
|
11
|
-
"lint-staged": {
|
|
12
|
-
"*.{js,json,md}": [
|
|
13
|
-
"prettier --write"
|
|
14
|
-
]
|
|
15
|
-
},
|
|
16
|
-
"husky": {
|
|
17
|
-
"hooks": {
|
|
18
|
-
"pre-commit": "lint-staged",
|
|
19
|
-
"pre-push": "npm run lint"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"keywords": [
|
|
23
|
-
"podcast",
|
|
24
|
-
"podcasts",
|
|
25
|
-
"downloader",
|
|
26
|
-
"cli"
|
|
27
|
-
],
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=14.17.6"
|
|
30
|
-
},
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "https://github.com/lightpohl/podcast-dl.git"
|
|
34
|
-
},
|
|
35
|
-
"files": [
|
|
36
|
-
"bin"
|
|
37
|
-
],
|
|
38
|
-
"author": "Joshua Pohl",
|
|
39
|
-
"license": "MIT",
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"eslint": "^6.8.0",
|
|
42
|
-
"eslint-config-prettier": "^6.11.0",
|
|
43
|
-
"husky": "^4.2.5",
|
|
44
|
-
"lint-staged": "^10.1.7",
|
|
45
|
-
"pkg": "^5.8.0",
|
|
46
|
-
"prettier": "2.3.2",
|
|
47
|
-
"rimraf": "^3.0.2",
|
|
48
|
-
"webpack": "^5.75.0"
|
|
49
|
-
},
|
|
50
|
-
"dependencies": {
|
|
51
|
-
"command-exists": "^1.2.9",
|
|
52
|
-
"commander": "^5.1.0",
|
|
53
|
-
"dayjs": "^1.8.25",
|
|
54
|
-
"filenamify": "^
|
|
55
|
-
"global-agent": "^3.0.0",
|
|
56
|
-
"got": "^11.0.2",
|
|
57
|
-
"p-limit": "^4.0.0",
|
|
58
|
-
"pluralize": "^8.0.0",
|
|
59
|
-
"rss-parser": "^3.12.0",
|
|
60
|
-
"throttle-debounce": "^3.0.1"
|
|
61
|
-
}
|
|
62
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "podcast-dl",
|
|
3
|
+
"version": "9.0.1",
|
|
4
|
+
"description": "A CLI for downloading podcasts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "./bin/bin.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "rimraf ./binaries && rimraf ./dist && node build.cjs",
|
|
9
|
+
"lint": "eslint ./bin"
|
|
10
|
+
},
|
|
11
|
+
"lint-staged": {
|
|
12
|
+
"*.{js,json,md}": [
|
|
13
|
+
"prettier --write"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"husky": {
|
|
17
|
+
"hooks": {
|
|
18
|
+
"pre-commit": "lint-staged",
|
|
19
|
+
"pre-push": "npm run lint"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"podcast",
|
|
24
|
+
"podcasts",
|
|
25
|
+
"downloader",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=14.17.6"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/lightpohl/podcast-dl.git"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin"
|
|
37
|
+
],
|
|
38
|
+
"author": "Joshua Pohl",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"eslint": "^6.8.0",
|
|
42
|
+
"eslint-config-prettier": "^6.11.0",
|
|
43
|
+
"husky": "^4.2.5",
|
|
44
|
+
"lint-staged": "^10.1.7",
|
|
45
|
+
"pkg": "^5.8.0",
|
|
46
|
+
"prettier": "2.3.2",
|
|
47
|
+
"rimraf": "^3.0.2",
|
|
48
|
+
"webpack": "^5.75.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"command-exists": "^1.2.9",
|
|
52
|
+
"commander": "^5.1.0",
|
|
53
|
+
"dayjs": "^1.8.25",
|
|
54
|
+
"filenamify": "^6.0.0",
|
|
55
|
+
"global-agent": "^3.0.0",
|
|
56
|
+
"got": "^11.0.2",
|
|
57
|
+
"p-limit": "^4.0.0",
|
|
58
|
+
"pluralize": "^8.0.0",
|
|
59
|
+
"rss-parser": "^3.12.0",
|
|
60
|
+
"throttle-debounce": "^3.0.1"
|
|
61
|
+
}
|
|
62
|
+
}
|