podcast-dl 10.3.3 → 11.0.0
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/README.md +36 -41
- package/bin/async.js +90 -98
- package/bin/bin.js +18 -52
- package/bin/commander.js +8 -11
- package/bin/exec.js +30 -0
- package/bin/ffmpeg.js +101 -0
- package/bin/items.js +203 -0
- package/bin/logger.js +8 -18
- package/bin/meta.js +33 -0
- package/bin/naming.js +6 -24
- package/bin/util.js +25 -459
- package/bin/validate.js +2 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -24,46 +24,41 @@ Either `--url` or `--file` must be provided.
|
|
|
24
24
|
|
|
25
25
|
Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
|
|
26
26
|
|
|
27
|
-
| Option | Type | Required | Description
|
|
28
|
-
| --------------------------------- | ------------------- | -------- |
|
|
29
|
-
| --url | String | true\* | URL to podcast RSS feed.
|
|
30
|
-
| --file | String | true\* | Path to local RSS file.
|
|
31
|
-
| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details.
|
|
32
|
-
| --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1.
|
|
33
|
-
| --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3.
|
|
34
|
-
| --
|
|
35
|
-
| --episode-template
|
|
36
|
-
| --
|
|
37
|
-
| --include-meta
|
|
38
|
-
| --include-episode-
|
|
39
|
-
| --include-episode-
|
|
40
|
-
| --
|
|
41
|
-
| --
|
|
42
|
-
| --
|
|
43
|
-
| --
|
|
44
|
-
| --
|
|
45
|
-
| --episode-regex
|
|
46
|
-
| --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.
|
|
47
|
-
| --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0.
|
|
48
|
-
| --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link".
|
|
49
|
-
| --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain".
|
|
50
|
-
| --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**)
|
|
51
|
-
| --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**)
|
|
52
|
-
| --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**)
|
|
53
|
-
| --override | | false | Override local files on collision.
|
|
54
|
-
| --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec.
|
|
55
|
-
| --reverse | | false | Reverse download direction and start at last RSS item.
|
|
56
|
-
| --info | | false | Print retrieved podcast info instead of downloading.
|
|
57
|
-
| --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported.
|
|
58
|
-
| --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details.
|
|
59
|
-
| --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).
|
|
60
|
-
| --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables).
|
|
61
|
-
| --help | | false | Output usage information.
|
|
62
|
-
|
|
63
|
-
## Archive
|
|
64
|
-
|
|
65
|
-
- If passed the `--archive [path]` option, `podcast-dl` will generate/use a JSON archive at the provided path.
|
|
66
|
-
- Before downloading an episode or writing out metadata, it'll check if the item was saved previously and abort the save if found.
|
|
27
|
+
| Option | Type | Required | Description |
|
|
28
|
+
| --------------------------------- | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| --url | String | true\* | URL to podcast RSS feed. |
|
|
30
|
+
| --file | String | true\* | Path to local RSS file. |
|
|
31
|
+
| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
|
|
32
|
+
| --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
|
|
33
|
+
| --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. |
|
|
34
|
+
| --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
|
|
35
|
+
| --episode-custom-template-options | <String...> | false | Provide custom options for the episode template. See "Template Options" for details. |
|
|
36
|
+
| --include-meta | | false | Write out podcast metadata to JSON. |
|
|
37
|
+
| --include-episode-meta | | false | Write out individual episode metadata **to** JSON. |
|
|
38
|
+
| --include-episode-images | | false | Download found episode images. |
|
|
39
|
+
| --include-episode-transcripts | | false | Download found episode transcripts. |
|
|
40
|
+
| --offset | Number | false | Offset starting download position. Default is 0. |
|
|
41
|
+
| --limit | Number | false | Max number of episodes to download. Downloads all by default. |
|
|
42
|
+
| --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
|
|
43
|
+
| --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
|
|
44
|
+
| --episode-regex | String | false | Match episode title against provided regex before starting download. |
|
|
45
|
+
| --episode-regex-exclude | String | false | Matched episode titles against provided regex will be excluded. |
|
|
46
|
+
| --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. |
|
|
47
|
+
| --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. |
|
|
48
|
+
| --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". |
|
|
49
|
+
| --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain". |
|
|
50
|
+
| --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
|
|
51
|
+
| --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) |
|
|
52
|
+
| --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) |
|
|
53
|
+
| --override | | false | Override local files on collision. |
|
|
54
|
+
| --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec. |
|
|
55
|
+
| --reverse | | false | Reverse download direction and start at last RSS item. |
|
|
56
|
+
| --info | | false | Print retrieved podcast info instead of downloading. |
|
|
57
|
+
| --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
|
|
58
|
+
| --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
|
|
59
|
+
| --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). |
|
|
60
|
+
| --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
|
|
61
|
+
| --help | | false | Output usage information. |
|
|
67
62
|
|
|
68
63
|
## Template Options
|
|
69
64
|
|
|
@@ -73,7 +68,7 @@ Options that support templates allow users to specify a template for the generat
|
|
|
73
68
|
|
|
74
69
|
`--episode-template "{{release_date}}-{{title}}"`
|
|
75
70
|
|
|
76
|
-
### `--out-dir`
|
|
71
|
+
### `--out-dir`
|
|
77
72
|
|
|
78
73
|
- `podcast_title`: Title of the podcast feed.
|
|
79
74
|
- `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
|
package/bin/async.js
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import got from "got";
|
|
1
3
|
import pLimit from "p-limit";
|
|
2
4
|
import _path from "path";
|
|
3
|
-
import { promisify } from "util";
|
|
4
5
|
import stream from "stream";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import got from "got";
|
|
7
6
|
import { throttle } from "throttle-debounce";
|
|
8
|
-
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
import { runExec } from "./exec.js";
|
|
9
|
+
import { runFfmpeg } from "./ffmpeg.js";
|
|
9
10
|
import {
|
|
10
|
-
logError,
|
|
11
11
|
LOG_LEVELS,
|
|
12
12
|
getLogMessageWithMarker,
|
|
13
13
|
getShouldOutputProgressIndicator,
|
|
14
|
+
logError,
|
|
14
15
|
} from "./logger.js";
|
|
15
|
-
import {
|
|
16
|
+
import { writeItemMeta } from "./meta.js";
|
|
17
|
+
import { getItemFilename } from "./naming.js";
|
|
16
18
|
import {
|
|
17
19
|
getEpisodeAudioUrlAndExt,
|
|
18
|
-
getArchiveKey,
|
|
19
20
|
getTempPath,
|
|
20
|
-
runFfmpeg,
|
|
21
|
-
runExec,
|
|
22
|
-
writeItemMeta,
|
|
23
|
-
writeToArchive,
|
|
24
|
-
getIsInArchive,
|
|
25
21
|
prepareOutputPath,
|
|
26
22
|
} from "./util.js";
|
|
27
23
|
|
|
@@ -31,13 +27,11 @@ const BYTES_IN_MB = 1000000;
|
|
|
31
27
|
const USER_AGENT =
|
|
32
28
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
|
|
33
29
|
|
|
34
|
-
const download = async (options) => {
|
|
30
|
+
export const download = async (options) => {
|
|
35
31
|
const {
|
|
36
32
|
marker,
|
|
37
33
|
url,
|
|
38
34
|
outputPath,
|
|
39
|
-
key,
|
|
40
|
-
archive,
|
|
41
35
|
override,
|
|
42
36
|
alwaysPostprocess,
|
|
43
37
|
onAfterDownload,
|
|
@@ -56,11 +50,6 @@ const download = async (options) => {
|
|
|
56
50
|
return;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
if (key && archive && getIsInArchive({ key, archive })) {
|
|
60
|
-
logMessage("Download exists in archive. Skipping...");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
53
|
let headResponse = null;
|
|
65
54
|
try {
|
|
66
55
|
headResponse = await got(url, {
|
|
@@ -151,20 +140,10 @@ const download = async (options) => {
|
|
|
151
140
|
if (onAfterDownload) {
|
|
152
141
|
await onAfterDownload();
|
|
153
142
|
}
|
|
154
|
-
|
|
155
|
-
if (key && archive) {
|
|
156
|
-
try {
|
|
157
|
-
writeToArchive({ key, archive });
|
|
158
|
-
} catch (error) {
|
|
159
|
-
throw new Error(`Error writing to archive: ${error.toString()}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
143
|
};
|
|
163
144
|
|
|
164
|
-
const downloadItemsAsync = async ({
|
|
145
|
+
export const downloadItemsAsync = async ({
|
|
165
146
|
addMp3MetadataFlag,
|
|
166
|
-
archive,
|
|
167
|
-
archivePrefix,
|
|
168
147
|
attempts,
|
|
169
148
|
basePath,
|
|
170
149
|
bitrate,
|
|
@@ -175,6 +154,7 @@ const downloadItemsAsync = async ({
|
|
|
175
154
|
episodeSourceOrder,
|
|
176
155
|
exec,
|
|
177
156
|
feed,
|
|
157
|
+
includeEpisodeImages,
|
|
178
158
|
includeEpisodeMeta,
|
|
179
159
|
mono,
|
|
180
160
|
override,
|
|
@@ -215,22 +195,54 @@ const downloadItemsAsync = async ({
|
|
|
215
195
|
|
|
216
196
|
try {
|
|
217
197
|
await download({
|
|
218
|
-
archive,
|
|
219
198
|
override,
|
|
220
199
|
alwaysPostprocess,
|
|
221
200
|
marker,
|
|
222
|
-
key: getArchiveKey({
|
|
223
|
-
prefix: archivePrefix,
|
|
224
|
-
name: getArchiveFilename({
|
|
225
|
-
name: item.title,
|
|
226
|
-
pubDate: item.pubDate,
|
|
227
|
-
ext: audioFileExt,
|
|
228
|
-
}),
|
|
229
|
-
}),
|
|
230
201
|
maxAttempts: attempts,
|
|
231
202
|
outputPath: outputPodcastPath,
|
|
232
203
|
url: episodeAudioUrl,
|
|
233
204
|
onAfterDownload: async () => {
|
|
205
|
+
if (item._episodeImage) {
|
|
206
|
+
try {
|
|
207
|
+
await download({
|
|
208
|
+
override,
|
|
209
|
+
marker: item._episodeImage.url,
|
|
210
|
+
maxAttempts: attempts,
|
|
211
|
+
outputPath: item._episodeImage.outputPath,
|
|
212
|
+
url: item._episodeImage.url,
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
hasErrors = true;
|
|
216
|
+
logError(
|
|
217
|
+
`${marker} | Error downloading ${
|
|
218
|
+
item._episodeImage.url
|
|
219
|
+
}: ${error.toString()}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (item._episodeTranscript) {
|
|
225
|
+
try {
|
|
226
|
+
await download({
|
|
227
|
+
override,
|
|
228
|
+
marker: item._episodeTranscript.url,
|
|
229
|
+
maxAttempts: attempts,
|
|
230
|
+
outputPath: item._episodeTranscript.outputPath,
|
|
231
|
+
url: item._episodeTranscript.url,
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
hasErrors = true;
|
|
235
|
+
logError(
|
|
236
|
+
`${marker} | Error downloading ${
|
|
237
|
+
item._episodeTranscript.url
|
|
238
|
+
}: ${error.toString()}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const hasEpisodeImage =
|
|
244
|
+
item._episodeImage && fs.existsSync(item._episodeImage.outputPath);
|
|
245
|
+
|
|
234
246
|
if (addMp3MetadataFlag || bitrate || mono) {
|
|
235
247
|
logMessage("Running ffmpeg...");
|
|
236
248
|
await runFfmpeg({
|
|
@@ -240,11 +252,18 @@ const downloadItemsAsync = async ({
|
|
|
240
252
|
mono,
|
|
241
253
|
itemIndex: item._originalIndex,
|
|
242
254
|
outputPath: outputPodcastPath,
|
|
255
|
+
episodeImageOutputPath: hasEpisodeImage
|
|
256
|
+
? item._episodeImage.outputPath
|
|
257
|
+
: undefined,
|
|
243
258
|
addMp3Metadata: addMp3MetadataFlag,
|
|
244
259
|
ext: audioFileExt,
|
|
245
260
|
});
|
|
246
261
|
}
|
|
247
262
|
|
|
263
|
+
if (!includeEpisodeImages && hasEpisodeImage) {
|
|
264
|
+
fs.unlinkSync(item._episodeImage.outputPath);
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
if (exec) {
|
|
249
268
|
logMessage("Running exec...");
|
|
250
269
|
await runExec({
|
|
@@ -256,6 +275,37 @@ const downloadItemsAsync = async ({
|
|
|
256
275
|
});
|
|
257
276
|
}
|
|
258
277
|
|
|
278
|
+
if (includeEpisodeMeta) {
|
|
279
|
+
const episodeMetaExt = ".meta.json";
|
|
280
|
+
const episodeMetaName = getItemFilename({
|
|
281
|
+
item,
|
|
282
|
+
feed,
|
|
283
|
+
url: episodeAudioUrl,
|
|
284
|
+
ext: episodeMetaExt,
|
|
285
|
+
template: episodeTemplate,
|
|
286
|
+
customTemplateOptions: episodeCustomTemplateOptions,
|
|
287
|
+
width: episodeDigits,
|
|
288
|
+
offset: episodeNumOffset,
|
|
289
|
+
});
|
|
290
|
+
const outputEpisodeMetaPath = _path.resolve(
|
|
291
|
+
basePath,
|
|
292
|
+
episodeMetaName
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
logMessage("Saving episode metadata...");
|
|
297
|
+
writeItemMeta({
|
|
298
|
+
marker,
|
|
299
|
+
override,
|
|
300
|
+
item,
|
|
301
|
+
outputPath: outputEpisodeMetaPath,
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
hasErrors = true;
|
|
305
|
+
logError(`${marker} | ${error.toString()}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
259
309
|
numEpisodesDownloaded += 1;
|
|
260
310
|
},
|
|
261
311
|
});
|
|
@@ -263,62 +313,6 @@ const downloadItemsAsync = async ({
|
|
|
263
313
|
hasErrors = true;
|
|
264
314
|
logError(`${marker} | Error downloading episode: ${error.toString()}`);
|
|
265
315
|
}
|
|
266
|
-
|
|
267
|
-
for (const extra of item._extra_downloads) {
|
|
268
|
-
try {
|
|
269
|
-
await download({
|
|
270
|
-
archive,
|
|
271
|
-
override,
|
|
272
|
-
marker: extra.url,
|
|
273
|
-
maxAttempts: attempts,
|
|
274
|
-
key: extra.key,
|
|
275
|
-
outputPath: extra.outputPath,
|
|
276
|
-
url: extra.url,
|
|
277
|
-
});
|
|
278
|
-
} catch (error) {
|
|
279
|
-
hasErrors = true;
|
|
280
|
-
logError(
|
|
281
|
-
`${marker} | Error downloading ${extra.url}: ${error.toString()}`
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (includeEpisodeMeta) {
|
|
287
|
-
const episodeMetaExt = ".meta.json";
|
|
288
|
-
const episodeMetaName = getItemFilename({
|
|
289
|
-
item,
|
|
290
|
-
feed,
|
|
291
|
-
url: episodeAudioUrl,
|
|
292
|
-
ext: episodeMetaExt,
|
|
293
|
-
template: episodeTemplate,
|
|
294
|
-
customTemplateOptions: episodeCustomTemplateOptions,
|
|
295
|
-
width: episodeDigits,
|
|
296
|
-
offset: episodeNumOffset,
|
|
297
|
-
});
|
|
298
|
-
const outputEpisodeMetaPath = _path.resolve(basePath, episodeMetaName);
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
logMessage("Saving episode metadata...");
|
|
302
|
-
writeItemMeta({
|
|
303
|
-
marker,
|
|
304
|
-
archive,
|
|
305
|
-
override,
|
|
306
|
-
item,
|
|
307
|
-
key: getArchiveKey({
|
|
308
|
-
prefix: archivePrefix,
|
|
309
|
-
name: getArchiveFilename({
|
|
310
|
-
pubDate: item.pubDate,
|
|
311
|
-
name: item.title,
|
|
312
|
-
ext: episodeMetaExt,
|
|
313
|
-
}),
|
|
314
|
-
}),
|
|
315
|
-
outputPath: outputEpisodeMetaPath,
|
|
316
|
-
});
|
|
317
|
-
} catch (error) {
|
|
318
|
-
hasErrors = true;
|
|
319
|
-
logError(`${marker} | ${error.toString()}`);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
316
|
};
|
|
323
317
|
|
|
324
318
|
const itemPromises = targetItems.map((item, index) =>
|
|
@@ -329,5 +323,3 @@ const downloadItemsAsync = async ({
|
|
|
329
323
|
|
|
330
324
|
return { numEpisodesDownloaded, hasErrors };
|
|
331
325
|
};
|
|
332
|
-
|
|
333
|
-
export { download, downloadItemsAsync };
|
package/bin/bin.js
CHANGED
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { program } from "commander";
|
|
3
4
|
import fs from "fs";
|
|
5
|
+
import { bootstrap as bootstrapProxy } from "global-agent";
|
|
4
6
|
import _path from "path";
|
|
5
|
-
import { program } from "commander";
|
|
6
7
|
import pluralize from "pluralize";
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import { download, downloadItemsAsync } from "./async.js";
|
|
9
9
|
import { setupCommander } from "./commander.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
getArchiveKey,
|
|
13
|
-
getFileFeed,
|
|
14
|
-
getImageUrl,
|
|
15
|
-
getItemsToDownload,
|
|
16
|
-
getUrlExt,
|
|
17
|
-
getUrlFeed,
|
|
18
|
-
logFeedInfo,
|
|
19
|
-
logItemsList,
|
|
20
|
-
writeFeedMeta,
|
|
21
|
-
} from "./util.js";
|
|
10
|
+
import { getItemsToDownload, logItemsList } from "./items.js";
|
|
22
11
|
import {
|
|
23
12
|
ERROR_STATUSES,
|
|
24
13
|
LOG_LEVELS,
|
|
25
|
-
logMessage,
|
|
26
14
|
logError,
|
|
27
15
|
logErrorAndExit,
|
|
16
|
+
logMessage,
|
|
28
17
|
} from "./logger.js";
|
|
18
|
+
import { writeFeedMeta } from "./meta.js";
|
|
29
19
|
import { getFolderName, getSimpleFilename } from "./naming.js";
|
|
30
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getFileFeed,
|
|
22
|
+
getImageUrl,
|
|
23
|
+
getUrlExt,
|
|
24
|
+
getUrlFeed,
|
|
25
|
+
logFeedInfo,
|
|
26
|
+
} from "./util.js";
|
|
31
27
|
|
|
32
28
|
const opts = setupCommander(program);
|
|
33
29
|
|
|
@@ -39,6 +35,7 @@ const {
|
|
|
39
35
|
episodeDigits,
|
|
40
36
|
episodeNumOffset,
|
|
41
37
|
episodeRegex,
|
|
38
|
+
episodeRegexExclude,
|
|
42
39
|
episodeSourceOrder,
|
|
43
40
|
episodeTemplate,
|
|
44
41
|
episodeCustomTemplateOptions,
|
|
@@ -65,8 +62,6 @@ const {
|
|
|
65
62
|
adjustBitrate: bitrate,
|
|
66
63
|
} = opts;
|
|
67
64
|
|
|
68
|
-
let { archive } = opts;
|
|
69
|
-
|
|
70
65
|
const main = async () => {
|
|
71
66
|
if (!url && !file) {
|
|
72
67
|
logErrorAndExit("No URL or file location provided");
|
|
@@ -84,15 +79,6 @@ const main = async () => {
|
|
|
84
79
|
? await getUrlFeed(url, parserConfig)
|
|
85
80
|
: await getFileFeed(file, parserConfig);
|
|
86
81
|
|
|
87
|
-
const archivePrefix = (() => {
|
|
88
|
-
if (feed.feedUrl || url) {
|
|
89
|
-
const { hostname, pathname } = new URL(feed.feedUrl || url);
|
|
90
|
-
return `${hostname}${pathname}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return feed.title || file;
|
|
94
|
-
})();
|
|
95
|
-
|
|
96
82
|
const basePath = _path.resolve(
|
|
97
83
|
process.cwd(),
|
|
98
84
|
getFolderName({ feed, template: outDir })
|
|
@@ -114,6 +100,7 @@ const main = async () => {
|
|
|
114
100
|
after,
|
|
115
101
|
before,
|
|
116
102
|
episodeRegex,
|
|
103
|
+
episodeRegexExclude,
|
|
117
104
|
});
|
|
118
105
|
} else {
|
|
119
106
|
logErrorAndExit("No episodes found to list");
|
|
@@ -131,14 +118,6 @@ const main = async () => {
|
|
|
131
118
|
fs.mkdirSync(basePath, { recursive: true });
|
|
132
119
|
}
|
|
133
120
|
|
|
134
|
-
if (archive) {
|
|
135
|
-
archive =
|
|
136
|
-
typeof archive === "boolean"
|
|
137
|
-
? "./{{podcast_title}}/archive.json"
|
|
138
|
-
: archive;
|
|
139
|
-
archive = getFolderName({ feed, template: archive });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
121
|
if (includeMeta) {
|
|
143
122
|
const podcastImageUrl = getImageUrl(feed);
|
|
144
123
|
|
|
@@ -155,15 +134,8 @@ const main = async () => {
|
|
|
155
134
|
try {
|
|
156
135
|
logMessage("\nDownloading podcast image...");
|
|
157
136
|
await download({
|
|
158
|
-
archive,
|
|
159
137
|
override,
|
|
160
138
|
marker: podcastImageUrl,
|
|
161
|
-
key: getArchiveKey({
|
|
162
|
-
prefix: archivePrefix,
|
|
163
|
-
name: `${
|
|
164
|
-
feed.title ? `${feed.title}.image` : "image"
|
|
165
|
-
}${podcastImageFileExt}`,
|
|
166
|
-
}),
|
|
167
139
|
outputPath: outputImagePath,
|
|
168
140
|
url: podcastImageUrl,
|
|
169
141
|
maxAttempts: attempts,
|
|
@@ -184,13 +156,8 @@ const main = async () => {
|
|
|
184
156
|
try {
|
|
185
157
|
logMessage("\nSaving podcast metadata...");
|
|
186
158
|
writeFeedMeta({
|
|
187
|
-
archive,
|
|
188
159
|
override,
|
|
189
160
|
feed,
|
|
190
|
-
key: getArchiveKey({
|
|
191
|
-
prefix: archivePrefix,
|
|
192
|
-
name: `${feed.title ? `${feed.title}.meta` : "meta"}.json`,
|
|
193
|
-
}),
|
|
194
161
|
outputPath: outputMetaPath,
|
|
195
162
|
});
|
|
196
163
|
} catch (error) {
|
|
@@ -207,8 +174,7 @@ const main = async () => {
|
|
|
207
174
|
}
|
|
208
175
|
|
|
209
176
|
const targetItems = getItemsToDownload({
|
|
210
|
-
|
|
211
|
-
archivePrefix,
|
|
177
|
+
addMp3MetadataFlag,
|
|
212
178
|
basePath,
|
|
213
179
|
feed,
|
|
214
180
|
limit,
|
|
@@ -219,6 +185,7 @@ const main = async () => {
|
|
|
219
185
|
episodeDigits,
|
|
220
186
|
episodeNumOffset,
|
|
221
187
|
episodeRegex,
|
|
188
|
+
episodeRegexExclude,
|
|
222
189
|
episodeSourceOrder,
|
|
223
190
|
episodeTemplate,
|
|
224
191
|
episodeCustomTemplateOptions,
|
|
@@ -237,8 +204,6 @@ const main = async () => {
|
|
|
237
204
|
|
|
238
205
|
const { numEpisodesDownloaded, hasErrors } = await downloadItemsAsync({
|
|
239
206
|
addMp3MetadataFlag,
|
|
240
|
-
archive,
|
|
241
|
-
archivePrefix,
|
|
242
207
|
attempts,
|
|
243
208
|
basePath,
|
|
244
209
|
bitrate,
|
|
@@ -249,6 +214,7 @@ const main = async () => {
|
|
|
249
214
|
episodeSourceOrder,
|
|
250
215
|
exec,
|
|
251
216
|
feed,
|
|
217
|
+
includeEpisodeImages,
|
|
252
218
|
includeEpisodeMeta,
|
|
253
219
|
mono,
|
|
254
220
|
override,
|
package/bin/commander.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AUDIO_ORDER_TYPES,
|
|
3
|
-
ITEM_LIST_FORMATS,
|
|
4
|
-
TRANSCRIPT_TYPES,
|
|
5
|
-
} from "./util.js";
|
|
1
|
+
import { AUDIO_ORDER_TYPES, TRANSCRIPT_TYPES } from "./util.js";
|
|
6
2
|
import { createParseNumber, hasFfmpeg } from "./validate.js";
|
|
7
3
|
import { logErrorAndExit } from "./logger.js";
|
|
4
|
+
import { ITEM_LIST_FORMATS } from "./items.js";
|
|
8
5
|
|
|
9
6
|
export const setupCommander = (program) => {
|
|
10
7
|
program
|
|
@@ -15,10 +12,6 @@ export const setupCommander = (program) => {
|
|
|
15
12
|
"specify output directory",
|
|
16
13
|
"./{{podcast_title}}"
|
|
17
14
|
)
|
|
18
|
-
.option(
|
|
19
|
-
"--archive [path]",
|
|
20
|
-
"download or write only items not listed in archive file"
|
|
21
|
-
)
|
|
22
15
|
.option(
|
|
23
16
|
"--episode-template <string>",
|
|
24
17
|
"template for generating episode related filenames",
|
|
@@ -111,6 +104,10 @@ export const setupCommander = (program) => {
|
|
|
111
104
|
"--episode-regex <string>",
|
|
112
105
|
"match episode title against regex before downloading"
|
|
113
106
|
)
|
|
107
|
+
.option(
|
|
108
|
+
"--episode-regex-exclude <string>",
|
|
109
|
+
"matched episode titles against regex will be excluded"
|
|
110
|
+
)
|
|
114
111
|
.option(
|
|
115
112
|
"--after <string>",
|
|
116
113
|
"download episodes only after this date (inclusive)"
|
|
@@ -137,7 +134,7 @@ export const setupCommander = (program) => {
|
|
|
137
134
|
.option("--override", "override local files on collision")
|
|
138
135
|
.option(
|
|
139
136
|
"--always-postprocess",
|
|
140
|
-
"always run additional tasks on the file regardless
|
|
137
|
+
"always run additional tasks on the file regardless of whether the file already exists"
|
|
141
138
|
)
|
|
142
139
|
.option("--reverse", "download episodes in reverse order")
|
|
143
140
|
.option("--info", "print retrieved podcast info instead of downloading")
|
|
@@ -158,7 +155,7 @@ export const setupCommander = (program) => {
|
|
|
158
155
|
)
|
|
159
156
|
.option(
|
|
160
157
|
"--exec <string>",
|
|
161
|
-
"
|
|
158
|
+
"execute a command after each episode is downloaded"
|
|
162
159
|
)
|
|
163
160
|
.option(
|
|
164
161
|
"--threads <number>",
|
package/bin/exec.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import util from "util";
|
|
3
|
+
import { escapeArgForShell } from "./util.js";
|
|
4
|
+
|
|
5
|
+
export const execWithPromise = util.promisify(exec);
|
|
6
|
+
|
|
7
|
+
export const runExec = async ({
|
|
8
|
+
exec,
|
|
9
|
+
basePath,
|
|
10
|
+
outputPodcastPath,
|
|
11
|
+
episodeFilename,
|
|
12
|
+
episodeAudioUrl,
|
|
13
|
+
}) => {
|
|
14
|
+
const episodeFilenameBase = episodeFilename.substring(
|
|
15
|
+
0,
|
|
16
|
+
episodeFilename.lastIndexOf(".")
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const execCmd = exec
|
|
20
|
+
.replace(/{{episode_path}}/g, escapeArgForShell(outputPodcastPath))
|
|
21
|
+
.replace(/{{episode_path_base}}/g, escapeArgForShell(basePath))
|
|
22
|
+
.replace(/{{episode_filename}}/g, escapeArgForShell(episodeFilename))
|
|
23
|
+
.replace(
|
|
24
|
+
/{{episode_filename_base}}/g,
|
|
25
|
+
escapeArgForShell(episodeFilenameBase)
|
|
26
|
+
)
|
|
27
|
+
.replace(/{{url}}/g, escapeArgForShell(episodeAudioUrl));
|
|
28
|
+
|
|
29
|
+
await execWithPromise(execCmd, { stdio: "ignore" });
|
|
30
|
+
};
|