podcast-dl 9.2.0 → 9.3.2
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 +5 -3
- package/bin/async.js +1 -1
- package/bin/bin.js +14 -5
- package/bin/commander.js +1 -1
- package/bin/logger.js +2 -0
- package/bin/util.js +84 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# podcast-dl
|
|
2
2
|
|
|
3
|
-
A CLI for downloading
|
|
3
|
+
A humble CLI for downloading and archiving podcasts.
|
|
4
4
|
|
|
5
5
|
## How to Use
|
|
6
6
|
|
|
@@ -20,11 +20,14 @@ A CLI for downloading podcasts with a focus on archiving.
|
|
|
20
20
|
|
|
21
21
|
## Options
|
|
22
22
|
|
|
23
|
+
Either `--url` or `--file` must be provided.
|
|
24
|
+
|
|
23
25
|
Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
|
|
24
26
|
|
|
25
27
|
| Option | Type | Required | Description |
|
|
26
28
|
| ------------------------ | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
-
| --url | String | true
|
|
29
|
+
| --url | String | true\* | URL to podcast RSS feed. |
|
|
30
|
+
| --file | String | true\* | Path to local RSS file. |
|
|
28
31
|
| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
|
|
29
32
|
| --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
|
|
30
33
|
| --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. |
|
|
@@ -51,7 +54,6 @@ Type values surrounded in square brackets (`[]`) can be used as used as boolean
|
|
|
51
54
|
| --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
|
|
52
55
|
| --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). |
|
|
53
56
|
| --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
|
|
54
|
-
| --version | | false | Output the version number. |
|
|
55
57
|
| --help | | false | Output usage information. |
|
|
56
58
|
|
|
57
59
|
## Archive
|
package/bin/async.js
CHANGED
package/bin/bin.js
CHANGED
|
@@ -10,10 +10,11 @@ import { setupCommander } from "./commander.js";
|
|
|
10
10
|
import { download } from "./async.js";
|
|
11
11
|
import {
|
|
12
12
|
getArchiveKey,
|
|
13
|
-
|
|
13
|
+
getFileFeed,
|
|
14
14
|
getImageUrl,
|
|
15
15
|
getItemsToDownload,
|
|
16
16
|
getUrlExt,
|
|
17
|
+
getUrlFeed,
|
|
17
18
|
logFeedInfo,
|
|
18
19
|
logItemsList,
|
|
19
20
|
writeFeedMeta,
|
|
@@ -31,6 +32,7 @@ import { downloadItemsAsync } from "./async.js";
|
|
|
31
32
|
setupCommander(commander, process.argv);
|
|
32
33
|
|
|
33
34
|
const {
|
|
35
|
+
file,
|
|
34
36
|
url,
|
|
35
37
|
outDir,
|
|
36
38
|
episodeTemplate,
|
|
@@ -62,17 +64,24 @@ const {
|
|
|
62
64
|
let { archive } = commander;
|
|
63
65
|
|
|
64
66
|
const main = async () => {
|
|
65
|
-
if (!url) {
|
|
66
|
-
logErrorAndExit("No URL provided");
|
|
67
|
+
if (!url && !file) {
|
|
68
|
+
logErrorAndExit("No URL or file location provided");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (url && file) {
|
|
72
|
+
logErrorAndExit("Must not use URL and file location");
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
if (proxy) {
|
|
70
76
|
bootstrapProxy();
|
|
71
77
|
}
|
|
72
78
|
|
|
73
|
-
const
|
|
79
|
+
const feed = url
|
|
80
|
+
? await getUrlFeed(url, parserConfig)
|
|
81
|
+
: await getFileFeed(file, parserConfig);
|
|
82
|
+
|
|
83
|
+
const { hostname, pathname } = new URL(feed.feedUrl);
|
|
74
84
|
const archiveUrl = `${hostname}${pathname}`;
|
|
75
|
-
const feed = await getFeed(url, parserConfig);
|
|
76
85
|
const basePath = _path.resolve(
|
|
77
86
|
process.cwd(),
|
|
78
87
|
getFolderName({ feed, template: outDir })
|
package/bin/commander.js
CHANGED
|
@@ -4,8 +4,8 @@ import { logErrorAndExit } from "./logger.js";
|
|
|
4
4
|
|
|
5
5
|
export const setupCommander = (commander, argv) => {
|
|
6
6
|
commander
|
|
7
|
-
.version("9.2.0")
|
|
8
7
|
.option("--url <string>", "url to podcast rss feed")
|
|
8
|
+
.option("--file <path>", "local path to podcast rss feed")
|
|
9
9
|
.option(
|
|
10
10
|
"--out-dir <path>",
|
|
11
11
|
"specify output directory",
|
package/bin/logger.js
CHANGED
package/bin/util.js
CHANGED
|
@@ -9,6 +9,30 @@ import { logErrorAndExit, logMessage, LOG_LEVELS } from "./logger.js";
|
|
|
9
9
|
import { getArchiveFilename, getItemFilename } from "./naming.js";
|
|
10
10
|
|
|
11
11
|
const execWithPromise = util.promisify(exec);
|
|
12
|
+
const isWin = process.platform === "win32";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
Escape arguments for a shell command used with exec.
|
|
16
|
+
Borrowed from shell-escape: https://github.com/xxorax/node-shell-escape/
|
|
17
|
+
Additionally, @see https://www.robvanderwoude.com/escapechars.php for why
|
|
18
|
+
we avoid trying tp escape complex sequences in Windows.
|
|
19
|
+
*/
|
|
20
|
+
const escapeArgForShell = (arg) => {
|
|
21
|
+
let result = arg;
|
|
22
|
+
|
|
23
|
+
if (/[^A-Za-z0-9_/:=-]/.test(result)) {
|
|
24
|
+
if (isWin) {
|
|
25
|
+
return null;
|
|
26
|
+
} else {
|
|
27
|
+
result = "'" + result.replace(/'/g, "'\\''") + "'";
|
|
28
|
+
result = result
|
|
29
|
+
.replace(/^(?:'')+/g, "") // unduplicate single-quote at the beginning
|
|
30
|
+
.replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
12
36
|
|
|
13
37
|
const getTempPath = (path) => {
|
|
14
38
|
return `${path}.tmp`;
|
|
@@ -40,6 +64,22 @@ const getPublicObject = (object, exclude = []) => {
|
|
|
40
64
|
return output;
|
|
41
65
|
};
|
|
42
66
|
|
|
67
|
+
const getFileString = (filePath) => {
|
|
68
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(fullPath)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data = fs.readFileSync(fullPath, "utf8");
|
|
75
|
+
|
|
76
|
+
if (!data) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return data;
|
|
81
|
+
};
|
|
82
|
+
|
|
43
83
|
const getJsonFile = (filePath) => {
|
|
44
84
|
const fullPath = path.resolve(process.cwd(), filePath);
|
|
45
85
|
|
|
@@ -268,10 +308,12 @@ const logItemsList = ({
|
|
|
268
308
|
});
|
|
269
309
|
|
|
270
310
|
if (isJson) {
|
|
311
|
+
// eslint-disable-next-line no-console
|
|
271
312
|
console.log(JSON.stringify(output));
|
|
272
313
|
return;
|
|
273
314
|
}
|
|
274
315
|
|
|
316
|
+
// eslint-disable-next-line no-console
|
|
275
317
|
console.table(output);
|
|
276
318
|
};
|
|
277
319
|
|
|
@@ -353,16 +395,17 @@ const getUrlExt = (url) => {
|
|
|
353
395
|
};
|
|
354
396
|
|
|
355
397
|
const AUDIO_TYPES_TO_EXTS = {
|
|
356
|
-
"audio/
|
|
357
|
-
"audio/mp3": ".mp3",
|
|
398
|
+
"audio/aac": ".aac",
|
|
358
399
|
"audio/flac": ".flac",
|
|
400
|
+
"audio/mp3": ".mp3",
|
|
401
|
+
"audio/mp4": ".m4a",
|
|
402
|
+
"audio/mpeg": ".mp3",
|
|
359
403
|
"audio/ogg": ".ogg",
|
|
404
|
+
"audio/opus": ".opus",
|
|
360
405
|
"audio/vorbis": ".ogg",
|
|
361
|
-
"audio/mp4": ".m4a",
|
|
362
|
-
"audio/x-m4a": ".m4a",
|
|
363
406
|
"audio/wav": ".wav",
|
|
407
|
+
"audio/x-m4a": ".m4a",
|
|
364
408
|
"audio/x-wav": ".wav",
|
|
365
|
-
"audio/aac": ".aac",
|
|
366
409
|
"video/mp4": ".mp4",
|
|
367
410
|
"video/quicktime": ".mov",
|
|
368
411
|
"video/x-m4v": ".m4v",
|
|
@@ -429,7 +472,31 @@ const getImageUrl = ({ image, itunes }) => {
|
|
|
429
472
|
return null;
|
|
430
473
|
};
|
|
431
474
|
|
|
432
|
-
const
|
|
475
|
+
const getFileFeed = async (filePath, parserConfig) => {
|
|
476
|
+
const defaultConfig = {
|
|
477
|
+
defaultRSS: 2.0,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const config = parserConfig ? getJsonFile(parserConfig) : defaultConfig;
|
|
481
|
+
const rssString = getFileString(filePath);
|
|
482
|
+
|
|
483
|
+
if (parserConfig && !config) {
|
|
484
|
+
logErrorAndExit(`Unable to load parser config: ${parserConfig}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const parser = new rssParser(config);
|
|
488
|
+
|
|
489
|
+
let feed;
|
|
490
|
+
try {
|
|
491
|
+
feed = await parser.parseString(rssString);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
logErrorAndExit("Unable to parse local RSS file", err);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return feed;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const getUrlFeed = async (url, parserConfig) => {
|
|
433
500
|
const defaultConfig = {
|
|
434
501
|
defaultRSS: 2.0,
|
|
435
502
|
};
|
|
@@ -505,11 +572,15 @@ const runFfmpeg = async ({
|
|
|
505
572
|
};
|
|
506
573
|
|
|
507
574
|
const metadataString = Object.keys(metaKeysToValues)
|
|
508
|
-
.map((key) =>
|
|
509
|
-
metaKeysToValues[key]
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
575
|
+
.map((key) => {
|
|
576
|
+
if (!metaKeysToValues[key]) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const argValue = escapeArgForShell(metaKeysToValues[key]);
|
|
581
|
+
|
|
582
|
+
return argValue ? `-metadata ${key}=${argValue}` : null;
|
|
583
|
+
})
|
|
513
584
|
.filter((segment) => !!segment)
|
|
514
585
|
.join(" ");
|
|
515
586
|
|
|
@@ -561,11 +632,12 @@ export {
|
|
|
561
632
|
getArchiveKey,
|
|
562
633
|
writeToArchive,
|
|
563
634
|
getEpisodeAudioUrlAndExt,
|
|
564
|
-
|
|
635
|
+
getFileFeed,
|
|
565
636
|
getImageUrl,
|
|
566
637
|
getItemsToDownload,
|
|
567
638
|
getTempPath,
|
|
568
639
|
getUrlExt,
|
|
640
|
+
getUrlFeed,
|
|
569
641
|
logFeedInfo,
|
|
570
642
|
ITEM_LIST_FORMATS,
|
|
571
643
|
logItemsList,
|