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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # podcast-dl
2
2
 
3
- A CLI for downloading podcasts with a focus on archiving.
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 | URL to podcast RSS feed. |
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
@@ -151,7 +151,7 @@ const download = async (options) => {
151
151
  }
152
152
  };
153
153
 
154
- let downloadItemsAsync = async ({
154
+ const downloadItemsAsync = async ({
155
155
  addMp3MetadataFlag,
156
156
  archive,
157
157
  archiveUrl,
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
- getFeed,
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 { hostname, pathname } = new URL(url);
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
@@ -1,3 +1,5 @@
1
+ /* eslint-disable no-console */
2
+
1
3
  const ERROR_STATUSES = {
2
4
  general: 1,
3
5
  nothingDownloaded: 2,
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/mpeg": ".mp3",
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 getFeed = async (url, parserConfig) => {
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
- ? `-metadata ${key}="${metaKeysToValues[key].replace(/"/g, '\\"')}"`
511
- : null
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
- getFeed,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "podcast-dl",
3
- "version": "9.2.0",
3
+ "version": "9.3.2",
4
4
  "description": "A CLI for downloading podcasts.",
5
5
  "type": "module",
6
6
  "bin": "./bin/bin.js",