podcast-dl 10.3.3 → 11.0.0

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
@@ -24,46 +24,41 @@ Either `--url` or `--file` must be provided.
24
24
 
25
25
  Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
26
26
 
27
- | Option | Type | Required | Description |
28
- | --------------------------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
- | --url | String | true\* | URL to podcast RSS feed. |
30
- | --file | String | true\* | Path to local RSS file. |
31
- | --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
32
- | --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
33
- | --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. |
34
- | --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. |
35
- | --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
36
- | --episode-custom-template-options | <String...> | false | Provide custom options for the episode template. See "Template Options" for details. |
37
- | --include-meta | | false | Write out podcast metadata to JSON. |
38
- | --include-episode-meta | | false | Write out individual episode metadata **to** JSON. |
39
- | --include-episode-images | | false | Download found episode images. |
40
- | --include-episode-transcripts | | false | Download found episode transcripts. |
41
- | --offset | Number | false | Offset starting download position. Default is 0. |
42
- | --limit | Number | false | Max number of episodes to download. Downloads all by default. |
43
- | --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
44
- | --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
45
- | --episode-regex | String | false | Match episode title against provided regex before starting download. |
46
- | --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. |
47
- | --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. |
48
- | --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". |
49
- | --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain". |
50
- | --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
51
- | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) |
52
- | --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) |
53
- | --override | | false | Override local files on collision. |
54
- | --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec. |
55
- | --reverse | | false | Reverse download direction and start at last RSS item. |
56
- | --info | | false | Print retrieved podcast info instead of downloading. |
57
- | --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
58
- | --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
59
- | --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). |
60
- | --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
61
- | --help | | false | Output usage information. |
62
-
63
- ## Archive
64
-
65
- - If passed the `--archive [path]` option, `podcast-dl` will generate/use a JSON archive at the provided path.
66
- - Before downloading an episode or writing out metadata, it'll check if the item was saved previously and abort the save if found.
27
+ | Option | Type | Required | Description |
28
+ | --------------------------------- | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | --url | String | true\* | URL to podcast RSS feed. |
30
+ | --file | String | true\* | Path to local RSS file. |
31
+ | --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
32
+ | --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
33
+ | --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. |
34
+ | --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
35
+ | --episode-custom-template-options | <String...> | false | Provide custom options for the episode template. See "Template Options" for details. |
36
+ | --include-meta | | false | Write out podcast metadata to JSON. |
37
+ | --include-episode-meta | | false | Write out individual episode metadata **to** JSON. |
38
+ | --include-episode-images | | false | Download found episode images. |
39
+ | --include-episode-transcripts | | false | Download found episode transcripts. |
40
+ | --offset | Number | false | Offset starting download position. Default is 0. |
41
+ | --limit | Number | false | Max number of episodes to download. Downloads all by default. |
42
+ | --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
43
+ | --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
44
+ | --episode-regex | String | false | Match episode title against provided regex before starting download. |
45
+ | --episode-regex-exclude | String | false | Matched episode titles against provided regex will be excluded. |
46
+ | --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. |
47
+ | --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. |
48
+ | --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". |
49
+ | --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain". |
50
+ | --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
51
+ | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) |
52
+ | --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) |
53
+ | --override | | false | Override local files on collision. |
54
+ | --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec. |
55
+ | --reverse | | false | Reverse download direction and start at last RSS item. |
56
+ | --info | | false | Print retrieved podcast info instead of downloading. |
57
+ | --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
58
+ | --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
59
+ | --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). |
60
+ | --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
61
+ | --help | | false | Output usage information. |
67
62
 
68
63
  ## Template Options
69
64
 
@@ -73,7 +68,7 @@ Options that support templates allow users to specify a template for the generat
73
68
 
74
69
  `--episode-template "{{release_date}}-{{title}}"`
75
70
 
76
- ### `--out-dir` & `--archive`
71
+ ### `--out-dir`
77
72
 
78
73
  - `podcast_title`: Title of the podcast feed.
79
74
  - `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
package/bin/async.js CHANGED
@@ -1,27 +1,23 @@
1
+ import fs from "fs";
2
+ import got from "got";
1
3
  import pLimit from "p-limit";
2
4
  import _path from "path";
3
- import { promisify } from "util";
4
5
  import stream from "stream";
5
- import fs from "fs";
6
- import got from "got";
7
6
  import { throttle } from "throttle-debounce";
8
-
7
+ import { promisify } from "util";
8
+ import { runExec } from "./exec.js";
9
+ import { runFfmpeg } from "./ffmpeg.js";
9
10
  import {
10
- logError,
11
11
  LOG_LEVELS,
12
12
  getLogMessageWithMarker,
13
13
  getShouldOutputProgressIndicator,
14
+ logError,
14
15
  } from "./logger.js";
15
- import { getArchiveFilename, getItemFilename } from "./naming.js";
16
+ import { writeItemMeta } from "./meta.js";
17
+ import { getItemFilename } from "./naming.js";
16
18
  import {
17
19
  getEpisodeAudioUrlAndExt,
18
- getArchiveKey,
19
20
  getTempPath,
20
- runFfmpeg,
21
- runExec,
22
- writeItemMeta,
23
- writeToArchive,
24
- getIsInArchive,
25
21
  prepareOutputPath,
26
22
  } from "./util.js";
27
23
 
@@ -31,13 +27,11 @@ const BYTES_IN_MB = 1000000;
31
27
  const USER_AGENT =
32
28
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
33
29
 
34
- const download = async (options) => {
30
+ export const download = async (options) => {
35
31
  const {
36
32
  marker,
37
33
  url,
38
34
  outputPath,
39
- key,
40
- archive,
41
35
  override,
42
36
  alwaysPostprocess,
43
37
  onAfterDownload,
@@ -56,11 +50,6 @@ const download = async (options) => {
56
50
  return;
57
51
  }
58
52
 
59
- if (key && archive && getIsInArchive({ key, archive })) {
60
- logMessage("Download exists in archive. Skipping...");
61
- return;
62
- }
63
-
64
53
  let headResponse = null;
65
54
  try {
66
55
  headResponse = await got(url, {
@@ -151,20 +140,10 @@ const download = async (options) => {
151
140
  if (onAfterDownload) {
152
141
  await onAfterDownload();
153
142
  }
154
-
155
- if (key && archive) {
156
- try {
157
- writeToArchive({ key, archive });
158
- } catch (error) {
159
- throw new Error(`Error writing to archive: ${error.toString()}`);
160
- }
161
- }
162
143
  };
163
144
 
164
- const downloadItemsAsync = async ({
145
+ export const downloadItemsAsync = async ({
165
146
  addMp3MetadataFlag,
166
- archive,
167
- archivePrefix,
168
147
  attempts,
169
148
  basePath,
170
149
  bitrate,
@@ -175,6 +154,7 @@ const downloadItemsAsync = async ({
175
154
  episodeSourceOrder,
176
155
  exec,
177
156
  feed,
157
+ includeEpisodeImages,
178
158
  includeEpisodeMeta,
179
159
  mono,
180
160
  override,
@@ -215,22 +195,54 @@ const downloadItemsAsync = async ({
215
195
 
216
196
  try {
217
197
  await download({
218
- archive,
219
198
  override,
220
199
  alwaysPostprocess,
221
200
  marker,
222
- key: getArchiveKey({
223
- prefix: archivePrefix,
224
- name: getArchiveFilename({
225
- name: item.title,
226
- pubDate: item.pubDate,
227
- ext: audioFileExt,
228
- }),
229
- }),
230
201
  maxAttempts: attempts,
231
202
  outputPath: outputPodcastPath,
232
203
  url: episodeAudioUrl,
233
204
  onAfterDownload: async () => {
205
+ if (item._episodeImage) {
206
+ try {
207
+ await download({
208
+ override,
209
+ marker: item._episodeImage.url,
210
+ maxAttempts: attempts,
211
+ outputPath: item._episodeImage.outputPath,
212
+ url: item._episodeImage.url,
213
+ });
214
+ } catch (error) {
215
+ hasErrors = true;
216
+ logError(
217
+ `${marker} | Error downloading ${
218
+ item._episodeImage.url
219
+ }: ${error.toString()}`
220
+ );
221
+ }
222
+ }
223
+
224
+ if (item._episodeTranscript) {
225
+ try {
226
+ await download({
227
+ override,
228
+ marker: item._episodeTranscript.url,
229
+ maxAttempts: attempts,
230
+ outputPath: item._episodeTranscript.outputPath,
231
+ url: item._episodeTranscript.url,
232
+ });
233
+ } catch (error) {
234
+ hasErrors = true;
235
+ logError(
236
+ `${marker} | Error downloading ${
237
+ item._episodeTranscript.url
238
+ }: ${error.toString()}`
239
+ );
240
+ }
241
+ }
242
+
243
+ const hasEpisodeImage =
244
+ item._episodeImage && fs.existsSync(item._episodeImage.outputPath);
245
+
234
246
  if (addMp3MetadataFlag || bitrate || mono) {
235
247
  logMessage("Running ffmpeg...");
236
248
  await runFfmpeg({
@@ -240,11 +252,18 @@ const downloadItemsAsync = async ({
240
252
  mono,
241
253
  itemIndex: item._originalIndex,
242
254
  outputPath: outputPodcastPath,
255
+ episodeImageOutputPath: hasEpisodeImage
256
+ ? item._episodeImage.outputPath
257
+ : undefined,
243
258
  addMp3Metadata: addMp3MetadataFlag,
244
259
  ext: audioFileExt,
245
260
  });
246
261
  }
247
262
 
263
+ if (!includeEpisodeImages && hasEpisodeImage) {
264
+ fs.unlinkSync(item._episodeImage.outputPath);
265
+ }
266
+
248
267
  if (exec) {
249
268
  logMessage("Running exec...");
250
269
  await runExec({
@@ -256,6 +275,37 @@ const downloadItemsAsync = async ({
256
275
  });
257
276
  }
258
277
 
278
+ if (includeEpisodeMeta) {
279
+ const episodeMetaExt = ".meta.json";
280
+ const episodeMetaName = getItemFilename({
281
+ item,
282
+ feed,
283
+ url: episodeAudioUrl,
284
+ ext: episodeMetaExt,
285
+ template: episodeTemplate,
286
+ customTemplateOptions: episodeCustomTemplateOptions,
287
+ width: episodeDigits,
288
+ offset: episodeNumOffset,
289
+ });
290
+ const outputEpisodeMetaPath = _path.resolve(
291
+ basePath,
292
+ episodeMetaName
293
+ );
294
+
295
+ try {
296
+ logMessage("Saving episode metadata...");
297
+ writeItemMeta({
298
+ marker,
299
+ override,
300
+ item,
301
+ outputPath: outputEpisodeMetaPath,
302
+ });
303
+ } catch (error) {
304
+ hasErrors = true;
305
+ logError(`${marker} | ${error.toString()}`);
306
+ }
307
+ }
308
+
259
309
  numEpisodesDownloaded += 1;
260
310
  },
261
311
  });
@@ -263,62 +313,6 @@ const downloadItemsAsync = async ({
263
313
  hasErrors = true;
264
314
  logError(`${marker} | Error downloading episode: ${error.toString()}`);
265
315
  }
266
-
267
- for (const extra of item._extra_downloads) {
268
- try {
269
- await download({
270
- archive,
271
- override,
272
- marker: extra.url,
273
- maxAttempts: attempts,
274
- key: extra.key,
275
- outputPath: extra.outputPath,
276
- url: extra.url,
277
- });
278
- } catch (error) {
279
- hasErrors = true;
280
- logError(
281
- `${marker} | Error downloading ${extra.url}: ${error.toString()}`
282
- );
283
- }
284
- }
285
-
286
- if (includeEpisodeMeta) {
287
- const episodeMetaExt = ".meta.json";
288
- const episodeMetaName = getItemFilename({
289
- item,
290
- feed,
291
- url: episodeAudioUrl,
292
- ext: episodeMetaExt,
293
- template: episodeTemplate,
294
- customTemplateOptions: episodeCustomTemplateOptions,
295
- width: episodeDigits,
296
- offset: episodeNumOffset,
297
- });
298
- const outputEpisodeMetaPath = _path.resolve(basePath, episodeMetaName);
299
-
300
- try {
301
- logMessage("Saving episode metadata...");
302
- writeItemMeta({
303
- marker,
304
- archive,
305
- override,
306
- item,
307
- key: getArchiveKey({
308
- prefix: archivePrefix,
309
- name: getArchiveFilename({
310
- pubDate: item.pubDate,
311
- name: item.title,
312
- ext: episodeMetaExt,
313
- }),
314
- }),
315
- outputPath: outputEpisodeMetaPath,
316
- });
317
- } catch (error) {
318
- hasErrors = true;
319
- logError(`${marker} | ${error.toString()}`);
320
- }
321
- }
322
316
  };
323
317
 
324
318
  const itemPromises = targetItems.map((item, index) =>
@@ -329,5 +323,3 @@ const downloadItemsAsync = async ({
329
323
 
330
324
  return { numEpisodesDownloaded, hasErrors };
331
325
  };
332
-
333
- export { download, downloadItemsAsync };
package/bin/bin.js CHANGED
@@ -1,33 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { program } from "commander";
3
4
  import fs from "fs";
5
+ import { bootstrap as bootstrapProxy } from "global-agent";
4
6
  import _path from "path";
5
- import { program } from "commander";
6
7
  import pluralize from "pluralize";
7
- import { bootstrap as bootstrapProxy } from "global-agent";
8
-
8
+ import { download, downloadItemsAsync } from "./async.js";
9
9
  import { setupCommander } from "./commander.js";
10
- import { download } from "./async.js";
11
- import {
12
- getArchiveKey,
13
- getFileFeed,
14
- getImageUrl,
15
- getItemsToDownload,
16
- getUrlExt,
17
- getUrlFeed,
18
- logFeedInfo,
19
- logItemsList,
20
- writeFeedMeta,
21
- } from "./util.js";
10
+ import { getItemsToDownload, logItemsList } from "./items.js";
22
11
  import {
23
12
  ERROR_STATUSES,
24
13
  LOG_LEVELS,
25
- logMessage,
26
14
  logError,
27
15
  logErrorAndExit,
16
+ logMessage,
28
17
  } from "./logger.js";
18
+ import { writeFeedMeta } from "./meta.js";
29
19
  import { getFolderName, getSimpleFilename } from "./naming.js";
30
- import { downloadItemsAsync } from "./async.js";
20
+ import {
21
+ getFileFeed,
22
+ getImageUrl,
23
+ getUrlExt,
24
+ getUrlFeed,
25
+ logFeedInfo,
26
+ } from "./util.js";
31
27
 
32
28
  const opts = setupCommander(program);
33
29
 
@@ -39,6 +35,7 @@ const {
39
35
  episodeDigits,
40
36
  episodeNumOffset,
41
37
  episodeRegex,
38
+ episodeRegexExclude,
42
39
  episodeSourceOrder,
43
40
  episodeTemplate,
44
41
  episodeCustomTemplateOptions,
@@ -65,8 +62,6 @@ const {
65
62
  adjustBitrate: bitrate,
66
63
  } = opts;
67
64
 
68
- let { archive } = opts;
69
-
70
65
  const main = async () => {
71
66
  if (!url && !file) {
72
67
  logErrorAndExit("No URL or file location provided");
@@ -84,15 +79,6 @@ const main = async () => {
84
79
  ? await getUrlFeed(url, parserConfig)
85
80
  : await getFileFeed(file, parserConfig);
86
81
 
87
- const archivePrefix = (() => {
88
- if (feed.feedUrl || url) {
89
- const { hostname, pathname } = new URL(feed.feedUrl || url);
90
- return `${hostname}${pathname}`;
91
- }
92
-
93
- return feed.title || file;
94
- })();
95
-
96
82
  const basePath = _path.resolve(
97
83
  process.cwd(),
98
84
  getFolderName({ feed, template: outDir })
@@ -114,6 +100,7 @@ const main = async () => {
114
100
  after,
115
101
  before,
116
102
  episodeRegex,
103
+ episodeRegexExclude,
117
104
  });
118
105
  } else {
119
106
  logErrorAndExit("No episodes found to list");
@@ -131,14 +118,6 @@ const main = async () => {
131
118
  fs.mkdirSync(basePath, { recursive: true });
132
119
  }
133
120
 
134
- if (archive) {
135
- archive =
136
- typeof archive === "boolean"
137
- ? "./{{podcast_title}}/archive.json"
138
- : archive;
139
- archive = getFolderName({ feed, template: archive });
140
- }
141
-
142
121
  if (includeMeta) {
143
122
  const podcastImageUrl = getImageUrl(feed);
144
123
 
@@ -155,15 +134,8 @@ const main = async () => {
155
134
  try {
156
135
  logMessage("\nDownloading podcast image...");
157
136
  await download({
158
- archive,
159
137
  override,
160
138
  marker: podcastImageUrl,
161
- key: getArchiveKey({
162
- prefix: archivePrefix,
163
- name: `${
164
- feed.title ? `${feed.title}.image` : "image"
165
- }${podcastImageFileExt}`,
166
- }),
167
139
  outputPath: outputImagePath,
168
140
  url: podcastImageUrl,
169
141
  maxAttempts: attempts,
@@ -184,13 +156,8 @@ const main = async () => {
184
156
  try {
185
157
  logMessage("\nSaving podcast metadata...");
186
158
  writeFeedMeta({
187
- archive,
188
159
  override,
189
160
  feed,
190
- key: getArchiveKey({
191
- prefix: archivePrefix,
192
- name: `${feed.title ? `${feed.title}.meta` : "meta"}.json`,
193
- }),
194
161
  outputPath: outputMetaPath,
195
162
  });
196
163
  } catch (error) {
@@ -207,8 +174,7 @@ const main = async () => {
207
174
  }
208
175
 
209
176
  const targetItems = getItemsToDownload({
210
- archive,
211
- archivePrefix,
177
+ addMp3MetadataFlag,
212
178
  basePath,
213
179
  feed,
214
180
  limit,
@@ -219,6 +185,7 @@ const main = async () => {
219
185
  episodeDigits,
220
186
  episodeNumOffset,
221
187
  episodeRegex,
188
+ episodeRegexExclude,
222
189
  episodeSourceOrder,
223
190
  episodeTemplate,
224
191
  episodeCustomTemplateOptions,
@@ -237,8 +204,6 @@ const main = async () => {
237
204
 
238
205
  const { numEpisodesDownloaded, hasErrors } = await downloadItemsAsync({
239
206
  addMp3MetadataFlag,
240
- archive,
241
- archivePrefix,
242
207
  attempts,
243
208
  basePath,
244
209
  bitrate,
@@ -249,6 +214,7 @@ const main = async () => {
249
214
  episodeSourceOrder,
250
215
  exec,
251
216
  feed,
217
+ includeEpisodeImages,
252
218
  includeEpisodeMeta,
253
219
  mono,
254
220
  override,
package/bin/commander.js CHANGED
@@ -1,10 +1,7 @@
1
- import {
2
- AUDIO_ORDER_TYPES,
3
- ITEM_LIST_FORMATS,
4
- TRANSCRIPT_TYPES,
5
- } from "./util.js";
1
+ import { AUDIO_ORDER_TYPES, TRANSCRIPT_TYPES } from "./util.js";
6
2
  import { createParseNumber, hasFfmpeg } from "./validate.js";
7
3
  import { logErrorAndExit } from "./logger.js";
4
+ import { ITEM_LIST_FORMATS } from "./items.js";
8
5
 
9
6
  export const setupCommander = (program) => {
10
7
  program
@@ -15,10 +12,6 @@ export const setupCommander = (program) => {
15
12
  "specify output directory",
16
13
  "./{{podcast_title}}"
17
14
  )
18
- .option(
19
- "--archive [path]",
20
- "download or write only items not listed in archive file"
21
- )
22
15
  .option(
23
16
  "--episode-template <string>",
24
17
  "template for generating episode related filenames",
@@ -111,6 +104,10 @@ export const setupCommander = (program) => {
111
104
  "--episode-regex <string>",
112
105
  "match episode title against regex before downloading"
113
106
  )
107
+ .option(
108
+ "--episode-regex-exclude <string>",
109
+ "matched episode titles against regex will be excluded"
110
+ )
114
111
  .option(
115
112
  "--after <string>",
116
113
  "download episodes only after this date (inclusive)"
@@ -137,7 +134,7 @@ export const setupCommander = (program) => {
137
134
  .option("--override", "override local files on collision")
138
135
  .option(
139
136
  "--always-postprocess",
140
- "always run additional tasks on the file regardless if the file already exists"
137
+ "always run additional tasks on the file regardless of whether the file already exists"
141
138
  )
142
139
  .option("--reverse", "download episodes in reverse order")
143
140
  .option("--info", "print retrieved podcast info instead of downloading")
@@ -158,7 +155,7 @@ export const setupCommander = (program) => {
158
155
  )
159
156
  .option(
160
157
  "--exec <string>",
161
- "Execute a command after each episode is downloaded"
158
+ "execute a command after each episode is downloaded"
162
159
  )
163
160
  .option(
164
161
  "--threads <number>",
package/bin/exec.js ADDED
@@ -0,0 +1,30 @@
1
+ import { exec } from "child_process";
2
+ import util from "util";
3
+ import { escapeArgForShell } from "./util.js";
4
+
5
+ export const execWithPromise = util.promisify(exec);
6
+
7
+ export const runExec = async ({
8
+ exec,
9
+ basePath,
10
+ outputPodcastPath,
11
+ episodeFilename,
12
+ episodeAudioUrl,
13
+ }) => {
14
+ const episodeFilenameBase = episodeFilename.substring(
15
+ 0,
16
+ episodeFilename.lastIndexOf(".")
17
+ );
18
+
19
+ const execCmd = exec
20
+ .replace(/{{episode_path}}/g, escapeArgForShell(outputPodcastPath))
21
+ .replace(/{{episode_path_base}}/g, escapeArgForShell(basePath))
22
+ .replace(/{{episode_filename}}/g, escapeArgForShell(episodeFilename))
23
+ .replace(
24
+ /{{episode_filename_base}}/g,
25
+ escapeArgForShell(episodeFilenameBase)
26
+ )
27
+ .replace(/{{url}}/g, escapeArgForShell(episodeAudioUrl));
28
+
29
+ await execWithPromise(execCmd, { stdio: "ignore" });
30
+ };