podcast-dl 8.0.6 → 8.0.8

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2020 Joshua Pohl
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Joshua Pohl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,95 +1,95 @@
1
- # podcast-dl
2
-
3
- A CLI for downloading podcasts with a focus on archiving.
4
-
5
- ## How to Use
6
-
7
- ### npx
8
-
9
- **[Node Required](https://nodejs.org/en/)**
10
-
11
- `npx podcast-dl --url <PODCAST_RSS_URL>`
12
-
13
- ### Binaries
14
-
15
- [Visit the releases page](https://github.com/lightpohl/podcast-dl/releases) and download the latest binary for your system.
16
-
17
- `podcast-dl --url <PODCAST_RSS_URL>`
18
-
19
- ### [More Examples](./docs/examples.md)
20
-
21
- ## Options
22
-
23
- Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
24
-
25
- | Option | Type | Required | Description |
26
- | ------------------------ | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
- | --url | String | true | URL to podcast RSS feed. |
28
- | --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
29
- | --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
30
- | --archive | [String] | false | Download or write out items not listed in archive file. Generates archive file at path if not found. Defaults to "./{{podcast_title}}/archive.json" when used as a boolean option. See "Template Options" for more details. |
31
- | --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
32
- | --include-meta | | false | Write out podcast metadata to JSON. |
33
- | --include-episode-meta | | false | Write out individual episode metadata to JSON. |
34
- | --include-episode-images | | false | Download found episode images. |
35
- | --offset | Number | false | Offset starting download position. Default is 0. |
36
- | --limit | Number | false | Max number of episodes to download. Downloads all by default. |
37
- | --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
38
- | --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
39
- | --episode-regex | String | false | Match episode title against provided regex before starting download. |
40
- | --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. |
41
- | --add-mp3-metadata | | false | Attempts to add a base level of MP3 metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
42
- | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of MP3s. (**ffmpeg required**) |
43
- | --mono | | false | Attempts to force MP3s into mono. (**ffmpeg required**) |
44
- | --override | | false | Override local files on collision. |
45
- | --reverse | | false | Reverse download direction and start at last RSS item. |
46
- | --info | | false | Print retrieved podcast info instead of downloading. |
47
- | --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
48
- | --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
49
- | --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). |
50
- | --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
51
- | --version | | false | Output the version number. |
52
- | --help | | false | Output usage information. |
53
-
54
- ## Archive
55
-
56
- - If passed the `--archive [path]` option, `podcast-dl` will generate/use a JSON archive at the provided path.
57
- - Before downloading an episode or writing out metadata, it'll check if the item was saved previously and abort the save if found.
58
-
59
- ## Template Options
60
-
61
- Options that support templates allow users to specify a template for the generated filename(s) or option. The provided template will replace all matched keywords with the related data described below. Each keyword must be wrapped in two braces like so:
62
-
63
- `--out-dir "./{{podcast_title}}"`
64
-
65
- `--episode-template "{{release_date}}-{{title}}"`
66
-
67
- ### `--out-dir` & `--archive`
68
-
69
- - `podcast_title`: Title of the podcast feed.
70
- - `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
71
-
72
- ### `--episode-template`
73
-
74
- - `title`: The title of the episode.
75
- - `release_date`: The release date of the episode in `YYYYMMDD` format.
76
- - `episode_num`: The location number of where the episodes appears in the feed.
77
- - `url`: URL of episode audio file.
78
- - `duration`: Provided `mm:ss` duration (if found).
79
- - `podcast_title`: Title of the podcast feed.
80
- - `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
81
-
82
- ### `--exec`
83
-
84
- - `episode_path`: The path to the downloaded episode.
85
- - `episode_path_base`: The path to the folder of the downloaded episode.
86
- - `episode_filname`: The filename of the episode.
87
- - `episode_filename_base`: The filename of the episode without its extension.
88
-
89
- ## Log Levels
90
-
91
- By default, all logs and errors are outputted to the console. The amount of logs can be controlled using the environment variable `LOG_LEVEL` with the following options:
92
-
93
- - `static`: All logs and errors are outputted to the console, but disables any animations.
94
- - `quiet`: Only important info and non-critical errors will be logged (e.g. episode download started).
95
- - `silent`: Only critical error messages will be be logged.
1
+ # podcast-dl
2
+
3
+ A CLI for downloading podcasts with a focus on archiving.
4
+
5
+ ## How to Use
6
+
7
+ ### npx
8
+
9
+ **[Node Required](https://nodejs.org/en/)**
10
+
11
+ `npx podcast-dl --url <PODCAST_RSS_URL>`
12
+
13
+ ### Binaries
14
+
15
+ [Visit the releases page](https://github.com/lightpohl/podcast-dl/releases) and download the latest binary for your system.
16
+
17
+ `podcast-dl --url <PODCAST_RSS_URL>`
18
+
19
+ ### [More Examples](./docs/examples.md)
20
+
21
+ ## Options
22
+
23
+ Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
24
+
25
+ | Option | Type | Required | Description |
26
+ | ------------------------ | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
+ | --url | String | true | URL to podcast RSS feed. |
28
+ | --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
29
+ | --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
30
+ | --archive | [String] | false | Download or write out items not listed in archive file. Generates archive file at path if not found. Defaults to "./{{podcast_title}}/archive.json" when used as a boolean option. See "Template Options" for more details. |
31
+ | --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
32
+ | --include-meta | | false | Write out podcast metadata to JSON. |
33
+ | --include-episode-meta | | false | Write out individual episode metadata to JSON. |
34
+ | --include-episode-images | | false | Download found episode images. |
35
+ | --offset | Number | false | Offset starting download position. Default is 0. |
36
+ | --limit | Number | false | Max number of episodes to download. Downloads all by default. |
37
+ | --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
38
+ | --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
39
+ | --episode-regex | String | false | Match episode title against provided regex before starting download. |
40
+ | --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. |
41
+ | --add-mp3-metadata | | false | Attempts to add a base level of MP3 metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
42
+ | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of MP3s. (**ffmpeg required**) |
43
+ | --mono | | false | Attempts to force MP3s into mono. (**ffmpeg required**) |
44
+ | --override | | false | Override local files on collision. |
45
+ | --reverse | | false | Reverse download direction and start at last RSS item. |
46
+ | --info | | false | Print retrieved podcast info instead of downloading. |
47
+ | --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
48
+ | --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
49
+ | --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). |
50
+ | --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
51
+ | --version | | false | Output the version number. |
52
+ | --help | | false | Output usage information. |
53
+
54
+ ## Archive
55
+
56
+ - If passed the `--archive [path]` option, `podcast-dl` will generate/use a JSON archive at the provided path.
57
+ - Before downloading an episode or writing out metadata, it'll check if the item was saved previously and abort the save if found.
58
+
59
+ ## Template Options
60
+
61
+ Options that support templates allow users to specify a template for the generated filename(s) or option. The provided template will replace all matched keywords with the related data described below. Each keyword must be wrapped in two braces like so:
62
+
63
+ `--out-dir "./{{podcast_title}}"`
64
+
65
+ `--episode-template "{{release_date}}-{{title}}"`
66
+
67
+ ### `--out-dir` & `--archive`
68
+
69
+ - `podcast_title`: Title of the podcast feed.
70
+ - `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
71
+
72
+ ### `--episode-template`
73
+
74
+ - `title`: The title of the episode.
75
+ - `release_date`: The release date of the episode in `YYYYMMDD` format.
76
+ - `episode_num`: The location number of where the episodes appears in the feed.
77
+ - `url`: URL of episode audio file.
78
+ - `duration`: Provided `mm:ss` duration (if found).
79
+ - `podcast_title`: Title of the podcast feed.
80
+ - `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
81
+
82
+ ### `--exec`
83
+
84
+ - `episode_path`: The path to the downloaded episode.
85
+ - `episode_path_base`: The path to the folder of the downloaded episode.
86
+ - `episode_filname`: The filename of the episode.
87
+ - `episode_filename_base`: The filename of the episode without its extension.
88
+
89
+ ## Log Levels
90
+
91
+ By default, all logs and errors are outputted to the console. The amount of logs can be controlled using the environment variable `LOG_LEVEL` with the following options:
92
+
93
+ - `static`: All logs and errors are outputted to the console, but disables any animations.
94
+ - `quiet`: Only important info and non-critical errors will be logged (e.g. episode download started).
95
+ - `silent`: Only critical error messages will be be logged.
package/bin/async.js CHANGED
@@ -224,7 +224,6 @@ let downloadItemsAsync = async ({
224
224
 
225
225
  for (const extra of item._extra_downloads) {
226
226
  try {
227
- logMessage("Downloading episode image...");
228
227
  await download({
229
228
  archive,
230
229
  override,
@@ -249,6 +248,7 @@ let downloadItemsAsync = async ({
249
248
  url: episodeAudioUrl,
250
249
  ext: episodeMetaExt,
251
250
  template: episodeTemplate,
251
+ width: episodeDigits,
252
252
  });
253
253
  const outputEpisodeMetaPath = _path.resolve(basePath, episodeMetaName);
254
254
 
package/bin/bin.js CHANGED
@@ -179,6 +179,7 @@ const main = async () => {
179
179
  reverse,
180
180
  after,
181
181
  before,
182
+ episodeDigits,
182
183
  episodeRegex,
183
184
  episodeTemplate,
184
185
  includeEpisodeImages,
package/bin/commander.js CHANGED
@@ -4,7 +4,7 @@ import { logErrorAndExit } from "./logger.js";
4
4
 
5
5
  export const setupCommander = (commander, argv) => {
6
6
  commander
7
- .version("8.0.6")
7
+ .version("8.0.8")
8
8
  .option("--url <string>", "url to podcast rss feed")
9
9
  .option(
10
10
  "--out-dir <path>",
package/bin/logger.js CHANGED
@@ -1,92 +1,92 @@
1
- const ERROR_STATUSES = {
2
- general: 1,
3
- nothingDownloaded: 2,
4
- completedWithErrors: 3,
5
- };
6
-
7
- const LOG_LEVEL_TYPES = {
8
- debug: "debug",
9
- quiet: "quiet",
10
- silent: "silent",
11
- static: "static",
12
- };
13
-
14
- const LOG_LEVELS = {
15
- debug: 0,
16
- info: 1,
17
- important: 2,
18
- };
19
-
20
- const getShouldOutputProgressIndicator = () => {
21
- return (
22
- process.stdout.isTTY &&
23
- process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.static &&
24
- process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.quiet &&
25
- process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.silent
26
- );
27
- };
28
-
29
- const logMessage = (message = "", logLevel = 1) => {
30
- if (
31
- !process.env.LOG_LEVEL ||
32
- process.env.LOG_LEVEL === LOG_LEVEL_TYPES.debug ||
33
- process.env.LOG_LEVEL === LOG_LEVEL_TYPES.static
34
- ) {
35
- console.log(message);
36
- return;
37
- }
38
-
39
- if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
40
- return;
41
- }
42
-
43
- if (
44
- process.env.LOG_LEVEL === LOG_LEVEL_TYPES.quiet &&
45
- logLevel > LOG_LEVELS.info
46
- ) {
47
- console.log(message);
48
- return;
49
- }
50
- };
51
-
52
- const getLogMessageWithMarker = (marker) => {
53
- return (message, logLevel) => {
54
- if (marker) {
55
- logMessage(`${marker} | ${message}`, logLevel);
56
- } else {
57
- logMessage(message, logLevel);
58
- }
59
- };
60
- };
61
-
62
- const logError = (msg, error) => {
63
- if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
64
- return;
65
- }
66
-
67
- console.error(msg);
68
-
69
- if (error) {
70
- console.error(error.message);
71
- }
72
- };
73
-
74
- const logErrorAndExit = (msg, error) => {
75
- console.error(msg);
76
-
77
- if (error) {
78
- console.error(error.message);
79
- }
80
-
81
- process.exit(ERROR_STATUSES.general);
82
- };
83
-
84
- export {
85
- ERROR_STATUSES,
86
- getShouldOutputProgressIndicator,
87
- getLogMessageWithMarker,
88
- LOG_LEVELS,
89
- logMessage,
90
- logError,
91
- logErrorAndExit,
92
- };
1
+ const ERROR_STATUSES = {
2
+ general: 1,
3
+ nothingDownloaded: 2,
4
+ completedWithErrors: 3,
5
+ };
6
+
7
+ const LOG_LEVEL_TYPES = {
8
+ debug: "debug",
9
+ quiet: "quiet",
10
+ silent: "silent",
11
+ static: "static",
12
+ };
13
+
14
+ const LOG_LEVELS = {
15
+ debug: 0,
16
+ info: 1,
17
+ important: 2,
18
+ };
19
+
20
+ const getShouldOutputProgressIndicator = () => {
21
+ return (
22
+ process.stdout.isTTY &&
23
+ process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.static &&
24
+ process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.quiet &&
25
+ process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.silent
26
+ );
27
+ };
28
+
29
+ const logMessage = (message = "", logLevel = 1) => {
30
+ if (
31
+ !process.env.LOG_LEVEL ||
32
+ process.env.LOG_LEVEL === LOG_LEVEL_TYPES.debug ||
33
+ process.env.LOG_LEVEL === LOG_LEVEL_TYPES.static
34
+ ) {
35
+ console.log(message);
36
+ return;
37
+ }
38
+
39
+ if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
40
+ return;
41
+ }
42
+
43
+ if (
44
+ process.env.LOG_LEVEL === LOG_LEVEL_TYPES.quiet &&
45
+ logLevel > LOG_LEVELS.info
46
+ ) {
47
+ console.log(message);
48
+ return;
49
+ }
50
+ };
51
+
52
+ const getLogMessageWithMarker = (marker) => {
53
+ return (message, logLevel) => {
54
+ if (marker) {
55
+ logMessage(`${marker} | ${message}`, logLevel);
56
+ } else {
57
+ logMessage(message, logLevel);
58
+ }
59
+ };
60
+ };
61
+
62
+ const logError = (msg, error) => {
63
+ if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
64
+ return;
65
+ }
66
+
67
+ console.error(msg);
68
+
69
+ if (error) {
70
+ console.error(error.message);
71
+ }
72
+ };
73
+
74
+ const logErrorAndExit = (msg, error) => {
75
+ console.error(msg);
76
+
77
+ if (error) {
78
+ console.error(error.message);
79
+ }
80
+
81
+ process.exit(ERROR_STATUSES.general);
82
+ };
83
+
84
+ export {
85
+ ERROR_STATUSES,
86
+ getShouldOutputProgressIndicator,
87
+ getLogMessageWithMarker,
88
+ LOG_LEVELS,
89
+ logMessage,
90
+ logError,
91
+ logErrorAndExit,
92
+ };
package/bin/naming.js CHANGED
@@ -1,72 +1,72 @@
1
- import filenamify from "filenamify";
2
- import dayjs from "dayjs";
3
-
4
- const INVALID_CHAR_REPLACE = "_";
5
- const MAX_LENGTH_FILENAME = 255;
6
-
7
- const getSafeName = (name) => {
8
- return filenamify(name, {
9
- replacement: INVALID_CHAR_REPLACE,
10
- maxLength: MAX_LENGTH_FILENAME,
11
- });
12
- };
13
-
14
- const getFilename = ({ item, ext, url, feed, template, width }) => {
15
- const episodeNum = feed.items.length - item._originalIndex;
16
- const formattedPubDate = item.pubDate
17
- ? dayjs(new Date(item.pubDate)).format("YYYYMMDD")
18
- : null;
19
-
20
- const templateReplacementsTuples = [
21
- ["title", item.title || ""],
22
- ["release_date", formattedPubDate || ""],
23
- ["episode_num", `${episodeNum}`.padStart(width, "0")],
24
- ["url", url],
25
- ["podcast_title", feed.title || ""],
26
- ["podcast_link", feed.link || ""],
27
- ["duration", (item.itunes && item.itunes.duration) || ""],
28
- ];
29
-
30
- let name = template;
31
- templateReplacementsTuples.forEach((replacementTuple) => {
32
- const [matcher, replacement] = replacementTuple;
33
- const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
34
-
35
- name = replacement
36
- ? name.replace(replaceRegex, replacement)
37
- : name.replace(replaceRegex, "");
38
- });
39
-
40
- name = `${name}${ext}`;
41
- return getSafeName(name);
42
- };
43
-
44
- const getFolderName = ({ feed, template }) => {
45
- const templateReplacementsTuples = [
46
- ["podcast_title", feed.title || ""],
47
- ["podcast_link", feed.link || ""],
48
- ];
49
-
50
- let name = template;
51
- templateReplacementsTuples.forEach((replacementTuple) => {
52
- const [matcher, replacement] = replacementTuple;
53
- const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
54
-
55
- name = replacement
56
- ? name.replace(replaceRegex, getSafeName(replacement))
57
- : name.replace(replaceRegex, "");
58
- });
59
-
60
- return name;
61
- };
62
-
63
- const getArchiveFilename = ({ pubDate, name, ext }) => {
64
- const formattedPubDate = pubDate
65
- ? dayjs(new Date(pubDate)).format("YYYYMMDD")
66
- : null;
67
-
68
- const baseName = formattedPubDate ? `${formattedPubDate}-${name}` : name;
69
- return getSafeName(`${baseName}${ext}`);
70
- };
71
-
72
- export { getArchiveFilename, getFilename, getFolderName, getSafeName };
1
+ import filenamify from "filenamify";
2
+ import dayjs from "dayjs";
3
+
4
+ const INVALID_CHAR_REPLACE = "_";
5
+ const MAX_LENGTH_FILENAME = 255;
6
+
7
+ const getSafeName = (name) => {
8
+ return filenamify(name, {
9
+ replacement: INVALID_CHAR_REPLACE,
10
+ maxLength: MAX_LENGTH_FILENAME,
11
+ });
12
+ };
13
+
14
+ const getFilename = ({ item, ext, url, feed, template, width }) => {
15
+ const episodeNum = feed.items.length - item._originalIndex;
16
+ const formattedPubDate = item.pubDate
17
+ ? dayjs(new Date(item.pubDate)).format("YYYYMMDD")
18
+ : null;
19
+
20
+ const templateReplacementsTuples = [
21
+ ["title", item.title || ""],
22
+ ["release_date", formattedPubDate || ""],
23
+ ["episode_num", `${episodeNum}`.padStart(width, "0")],
24
+ ["url", url],
25
+ ["podcast_title", feed.title || ""],
26
+ ["podcast_link", feed.link || ""],
27
+ ["duration", (item.itunes && item.itunes.duration) || ""],
28
+ ];
29
+
30
+ let name = template;
31
+ templateReplacementsTuples.forEach((replacementTuple) => {
32
+ const [matcher, replacement] = replacementTuple;
33
+ const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
34
+
35
+ name = replacement
36
+ ? name.replace(replaceRegex, replacement)
37
+ : name.replace(replaceRegex, "");
38
+ });
39
+
40
+ name = `${name}${ext}`;
41
+ return getSafeName(name);
42
+ };
43
+
44
+ const getFolderName = ({ feed, template }) => {
45
+ const templateReplacementsTuples = [
46
+ ["podcast_title", feed.title || ""],
47
+ ["podcast_link", feed.link || ""],
48
+ ];
49
+
50
+ let name = template;
51
+ templateReplacementsTuples.forEach((replacementTuple) => {
52
+ const [matcher, replacement] = replacementTuple;
53
+ const replaceRegex = new RegExp(`{{${matcher}}}`, "g");
54
+
55
+ name = replacement
56
+ ? name.replace(replaceRegex, getSafeName(replacement))
57
+ : name.replace(replaceRegex, "");
58
+ });
59
+
60
+ return name;
61
+ };
62
+
63
+ const getArchiveFilename = ({ pubDate, name, ext }) => {
64
+ const formattedPubDate = pubDate
65
+ ? dayjs(new Date(pubDate)).format("YYYYMMDD")
66
+ : null;
67
+
68
+ const baseName = formattedPubDate ? `${formattedPubDate}-${name}` : name;
69
+ return getSafeName(`${baseName}${ext}`);
70
+ };
71
+
72
+ export { getArchiveFilename, getFilename, getFolderName, getSafeName };
package/bin/util.js CHANGED
@@ -36,7 +36,13 @@ const getJsonFile = (filePath) => {
36
36
  return null;
37
37
  }
38
38
 
39
- return JSON.parse(fs.readFileSync(fullPath));
39
+ const data = fs.readFileSync(fullPath);
40
+
41
+ if (!data) {
42
+ return null;
43
+ }
44
+
45
+ return JSON.parse(data);
40
46
  };
41
47
 
42
48
  const getArchive = (archive) => {
@@ -96,6 +102,7 @@ const getItemsToDownload = ({
96
102
  reverse,
97
103
  before,
98
104
  after,
105
+ episodeDigits,
99
106
  episodeRegex,
100
107
  episodeTemplate,
101
108
  includeEpisodeImages,
@@ -183,6 +190,7 @@ const getItemsToDownload = ({
183
190
  url: episodeAudioUrl,
184
191
  ext: episodeImageFileExt,
185
192
  template: episodeTemplate,
193
+ width: episodeDigits,
186
194
  });
187
195
 
188
196
  const outputImagePath = path.resolve(basePath, episodeImageName);
@@ -347,7 +355,12 @@ const AUDIO_TYPES_TO_EXTS = {
347
355
  const VALID_AUDIO_EXTS = [...new Set(Object.values(AUDIO_TYPES_TO_EXTS))];
348
356
 
349
357
  const getIsAudioUrl = (url) => {
350
- const ext = getUrlExt(url);
358
+ let ext;
359
+ try {
360
+ ext = getUrlExt(url);
361
+ } catch (err) {
362
+ return false;
363
+ }
351
364
 
352
365
  if (!ext) {
353
366
  return false;
package/bin/validate.js CHANGED
@@ -1,35 +1,35 @@
1
- import { sync as commandExistsSync } from "command-exists";
2
-
3
- import { logErrorAndExit } from "./logger.js";
4
-
5
- const createParseNumber = ({ min, name, required = true }) => {
6
- return (value) => {
7
- if (!value && !required) {
8
- return undefined;
9
- }
10
-
11
- try {
12
- let number = parseInt(value);
13
-
14
- if (isNaN(number)) {
15
- logErrorAndExit(`${name} must be a number`);
16
- }
17
-
18
- if (number < min) {
19
- logErrorAndExit(`${name} must be >= ${min}`);
20
- }
21
-
22
- return number;
23
- } catch (error) {
24
- logErrorAndExit(`Unable to parse ${name}`);
25
- }
26
- };
27
- };
28
-
29
- const hasFfmpeg = () => {
30
- if (!commandExistsSync("ffmpeg")) {
31
- logErrorAndExit('option specified requires "ffmpeg" be available');
32
- }
33
- };
34
-
35
- export { createParseNumber, hasFfmpeg };
1
+ import { sync as commandExistsSync } from "command-exists";
2
+
3
+ import { logErrorAndExit } from "./logger.js";
4
+
5
+ const createParseNumber = ({ min, name, required = true }) => {
6
+ return (value) => {
7
+ if (!value && !required) {
8
+ return undefined;
9
+ }
10
+
11
+ try {
12
+ let number = parseInt(value);
13
+
14
+ if (isNaN(number)) {
15
+ logErrorAndExit(`${name} must be a number`);
16
+ }
17
+
18
+ if (number < min) {
19
+ logErrorAndExit(`${name} must be >= ${min}`);
20
+ }
21
+
22
+ return number;
23
+ } catch (error) {
24
+ logErrorAndExit(`Unable to parse ${name}`);
25
+ }
26
+ };
27
+ };
28
+
29
+ const hasFfmpeg = () => {
30
+ if (!commandExistsSync("ffmpeg")) {
31
+ logErrorAndExit('option specified requires "ffmpeg" be available');
32
+ }
33
+ };
34
+
35
+ export { createParseNumber, hasFfmpeg };
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "podcast-dl",
3
- "version": "8.0.6",
4
- "description": "A CLI for downloading podcasts.",
5
- "type": "module",
6
- "bin": "./bin/bin.js",
7
- "scripts": {
8
- "build": "rimraf ./binaries && rimraf ./dist && node build.cjs",
9
- "lint": "eslint ./bin"
10
- },
11
- "lint-staged": {
12
- "*.{js,json,md}": [
13
- "prettier --write"
14
- ]
15
- },
16
- "husky": {
17
- "hooks": {
18
- "pre-commit": "lint-staged",
19
- "pre-push": "npm run lint"
20
- }
21
- },
22
- "keywords": [
23
- "podcast",
24
- "podcasts",
25
- "downloader",
26
- "cli"
27
- ],
28
- "engines": {
29
- "node": ">=14.17.6"
30
- },
31
- "repository": {
32
- "type": "git",
33
- "url": "https://github.com/lightpohl/podcast-dl.git"
34
- },
35
- "files": [
36
- "bin"
37
- ],
38
- "author": "Joshua Pohl",
39
- "license": "MIT",
40
- "devDependencies": {
41
- "eslint": "^6.8.0",
42
- "eslint-config-prettier": "^6.11.0",
43
- "husky": "^4.2.5",
44
- "lint-staged": "^10.1.7",
45
- "pkg": "^5.8.0",
46
- "prettier": "2.3.2",
47
- "rimraf": "^3.0.2",
48
- "webpack": "^5.75.0"
49
- },
50
- "dependencies": {
51
- "command-exists": "^1.2.9",
52
- "commander": "^5.1.0",
53
- "dayjs": "^1.8.25",
54
- "filenamify": "^4.1.0",
55
- "global-agent": "^3.0.0",
56
- "got": "^11.0.2",
57
- "p-limit": "^4.0.0",
58
- "pluralize": "^8.0.0",
59
- "rss-parser": "^3.12.0",
60
- "throttle-debounce": "^3.0.1"
61
- }
62
- }
1
+ {
2
+ "name": "podcast-dl",
3
+ "version": "8.0.8",
4
+ "description": "A CLI for downloading podcasts.",
5
+ "type": "module",
6
+ "bin": "./bin/bin.js",
7
+ "scripts": {
8
+ "build": "rimraf ./binaries && rimraf ./dist && node build.cjs",
9
+ "lint": "eslint ./bin"
10
+ },
11
+ "lint-staged": {
12
+ "*.{js,json,md}": [
13
+ "prettier --write"
14
+ ]
15
+ },
16
+ "husky": {
17
+ "hooks": {
18
+ "pre-commit": "lint-staged",
19
+ "pre-push": "npm run lint"
20
+ }
21
+ },
22
+ "keywords": [
23
+ "podcast",
24
+ "podcasts",
25
+ "downloader",
26
+ "cli"
27
+ ],
28
+ "engines": {
29
+ "node": ">=14.17.6"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/lightpohl/podcast-dl.git"
34
+ },
35
+ "files": [
36
+ "bin"
37
+ ],
38
+ "author": "Joshua Pohl",
39
+ "license": "MIT",
40
+ "devDependencies": {
41
+ "eslint": "^6.8.0",
42
+ "eslint-config-prettier": "^6.11.0",
43
+ "husky": "^4.2.5",
44
+ "lint-staged": "^10.1.7",
45
+ "pkg": "^5.8.0",
46
+ "prettier": "2.3.2",
47
+ "rimraf": "^3.0.2",
48
+ "webpack": "^5.75.0"
49
+ },
50
+ "dependencies": {
51
+ "command-exists": "^1.2.9",
52
+ "commander": "^5.1.0",
53
+ "dayjs": "^1.8.25",
54
+ "filenamify": "^4.1.0",
55
+ "global-agent": "^3.0.0",
56
+ "got": "^11.0.2",
57
+ "p-limit": "^4.0.0",
58
+ "pluralize": "^8.0.0",
59
+ "rss-parser": "^3.12.0",
60
+ "throttle-debounce": "^3.0.1"
61
+ }
62
+ }