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 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/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 || 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
@@ -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,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
- result = "'" + result.replace(/'/g, "'\\''") + "'";
21
- result = result
22
- .replace(/^(?:'')+/g, "") // unduplicate single-quote at the beginning
23
- .replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
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/mpeg": ".mp3",
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 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) => {
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
- ? `-metadata ${key}=${escapeArgForShell(metaKeysToValues[key])}`
527
- : null
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
- getFeed,
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.2.1",
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
@@ -1,2 +0,0 @@
1
- // Generated by genversion.
2
- export const version = "9.2.1";