podcast-dl 9.2.1 → 9.3.3
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/bin.js +14 -5
- package/bin/commander.js +1 -2
- package/bin/logger.js +2 -0
- package/bin/util.js +72 -16
- package/package.json +2 -3
- package/bin/version.js +0 -2
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/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 || url);
|
|
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
|
@@ -1,12 +1,11 @@
|
|
|
1
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
|
-
import { version } from "./version.js";
|
|
5
4
|
|
|
6
5
|
export const setupCommander = (commander, argv) => {
|
|
7
6
|
commander
|
|
8
|
-
.version(version)
|
|
9
7
|
.option("--url <string>", "url to podcast rss feed")
|
|
8
|
+
.option("--file <path>", "local path to podcast rss feed")
|
|
10
9
|
.option(
|
|
11
10
|
"--out-dir <path>",
|
|
12
11
|
"specify output directory",
|
package/bin/logger.js
CHANGED
package/bin/util.js
CHANGED
|
@@ -9,18 +9,26 @@ 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";
|
|
12
13
|
|
|
13
14
|
/*
|
|
14
15
|
Escape arguments for a shell command used with exec.
|
|
15
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.
|
|
16
19
|
*/
|
|
17
20
|
const escapeArgForShell = (arg) => {
|
|
18
21
|
let result = arg;
|
|
22
|
+
|
|
19
23
|
if (/[^A-Za-z0-9_/:=-]/.test(result)) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.replace(
|
|
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
|
+
}
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
return result;
|
|
@@ -56,6 +64,22 @@ const getPublicObject = (object, exclude = []) => {
|
|
|
56
64
|
return output;
|
|
57
65
|
};
|
|
58
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
|
+
|
|
59
83
|
const getJsonFile = (filePath) => {
|
|
60
84
|
const fullPath = path.resolve(process.cwd(), filePath);
|
|
61
85
|
|
|
@@ -284,10 +308,12 @@ const logItemsList = ({
|
|
|
284
308
|
});
|
|
285
309
|
|
|
286
310
|
if (isJson) {
|
|
311
|
+
// eslint-disable-next-line no-console
|
|
287
312
|
console.log(JSON.stringify(output));
|
|
288
313
|
return;
|
|
289
314
|
}
|
|
290
315
|
|
|
316
|
+
// eslint-disable-next-line no-console
|
|
291
317
|
console.table(output);
|
|
292
318
|
};
|
|
293
319
|
|
|
@@ -369,16 +395,17 @@ const getUrlExt = (url) => {
|
|
|
369
395
|
};
|
|
370
396
|
|
|
371
397
|
const AUDIO_TYPES_TO_EXTS = {
|
|
372
|
-
"audio/
|
|
373
|
-
"audio/mp3": ".mp3",
|
|
398
|
+
"audio/aac": ".aac",
|
|
374
399
|
"audio/flac": ".flac",
|
|
400
|
+
"audio/mp3": ".mp3",
|
|
401
|
+
"audio/mp4": ".m4a",
|
|
402
|
+
"audio/mpeg": ".mp3",
|
|
375
403
|
"audio/ogg": ".ogg",
|
|
404
|
+
"audio/opus": ".opus",
|
|
376
405
|
"audio/vorbis": ".ogg",
|
|
377
|
-
"audio/mp4": ".m4a",
|
|
378
|
-
"audio/x-m4a": ".m4a",
|
|
379
406
|
"audio/wav": ".wav",
|
|
407
|
+
"audio/x-m4a": ".m4a",
|
|
380
408
|
"audio/x-wav": ".wav",
|
|
381
|
-
"audio/aac": ".aac",
|
|
382
409
|
"video/mp4": ".mp4",
|
|
383
410
|
"video/quicktime": ".mov",
|
|
384
411
|
"video/x-m4v": ".m4v",
|
|
@@ -445,7 +472,31 @@ const getImageUrl = ({ image, itunes }) => {
|
|
|
445
472
|
return null;
|
|
446
473
|
};
|
|
447
474
|
|
|
448
|
-
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) => {
|
|
449
500
|
const defaultConfig = {
|
|
450
501
|
defaultRSS: 2.0,
|
|
451
502
|
};
|
|
@@ -521,11 +572,15 @@ const runFfmpeg = async ({
|
|
|
521
572
|
};
|
|
522
573
|
|
|
523
574
|
const metadataString = Object.keys(metaKeysToValues)
|
|
524
|
-
.map((key) =>
|
|
525
|
-
metaKeysToValues[key]
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
+
})
|
|
529
584
|
.filter((segment) => !!segment)
|
|
530
585
|
.join(" ");
|
|
531
586
|
|
|
@@ -577,11 +632,12 @@ export {
|
|
|
577
632
|
getArchiveKey,
|
|
578
633
|
writeToArchive,
|
|
579
634
|
getEpisodeAudioUrlAndExt,
|
|
580
|
-
|
|
635
|
+
getFileFeed,
|
|
581
636
|
getImageUrl,
|
|
582
637
|
getItemsToDownload,
|
|
583
638
|
getTempPath,
|
|
584
639
|
getUrlExt,
|
|
640
|
+
getUrlFeed,
|
|
585
641
|
logFeedInfo,
|
|
586
642
|
ITEM_LIST_FORMATS,
|
|
587
643
|
logItemsList,
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "podcast-dl",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.3",
|
|
4
4
|
"description": "A CLI for downloading podcasts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./bin/bin.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "rimraf ./binaries && rimraf ./dist && node build.cjs",
|
|
9
|
-
"lint": "eslint ./bin"
|
|
10
|
-
"version:generate": "npx genversion --es6 --semi ./bin/version.js"
|
|
9
|
+
"lint": "eslint ./bin"
|
|
11
10
|
},
|
|
12
11
|
"lint-staged": {
|
|
13
12
|
"*.{js,json,md}": [
|
package/bin/version.js
DELETED