podcast-dl 10.4.0 → 11.0.1

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,47 +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-regex-exclude | String | false | Matched episode titles against provided regex will be excluded. |
47
- | --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. |
48
- | --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. |
49
- | --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". |
50
- | --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". |
51
- | --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**) |
52
- | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) |
53
- | --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) |
54
- | --override | | false | Override local files on collision. |
55
- | --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. |
56
- | --reverse | | false | Reverse download direction and start at last RSS item. |
57
- | --info | | false | Print retrieved podcast info instead of downloading. |
58
- | --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
59
- | --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
60
- | --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). |
61
- | --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
62
- | --help | | false | Output usage information. |
63
-
64
- ## Archive
65
-
66
- - If passed the `--archive [path]` option, `podcast-dl` will generate/use a JSON archive at the provided path.
67
- - 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. |
68
62
 
69
63
  ## Template Options
70
64
 
@@ -74,7 +68,7 @@ Options that support templates allow users to specify a template for the generat
74
68
 
75
69
  `--episode-template "{{release_date}}-{{title}}"`
76
70
 
77
- ### `--out-dir` & `--archive`
71
+ ### `--out-dir`
78
72
 
79
73
  - `podcast_title`: Title of the podcast feed.
80
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
 
@@ -66,8 +62,6 @@ const {
66
62
  adjustBitrate: bitrate,
67
63
  } = opts;
68
64
 
69
- let { archive } = opts;
70
-
71
65
  const main = async () => {
72
66
  if (!url && !file) {
73
67
  logErrorAndExit("No URL or file location provided");
@@ -85,15 +79,6 @@ const main = async () => {
85
79
  ? await getUrlFeed(url, parserConfig)
86
80
  : await getFileFeed(file, parserConfig);
87
81
 
88
- const archivePrefix = (() => {
89
- if (feed.feedUrl || url) {
90
- const { hostname, pathname } = new URL(feed.feedUrl || url);
91
- return `${hostname}${pathname}`;
92
- }
93
-
94
- return feed.title || file;
95
- })();
96
-
97
82
  const basePath = _path.resolve(
98
83
  process.cwd(),
99
84
  getFolderName({ feed, template: outDir })
@@ -133,14 +118,6 @@ const main = async () => {
133
118
  fs.mkdirSync(basePath, { recursive: true });
134
119
  }
135
120
 
136
- if (archive) {
137
- archive =
138
- typeof archive === "boolean"
139
- ? "./{{podcast_title}}/archive.json"
140
- : archive;
141
- archive = getFolderName({ feed, template: archive });
142
- }
143
-
144
121
  if (includeMeta) {
145
122
  const podcastImageUrl = getImageUrl(feed);
146
123
 
@@ -157,15 +134,8 @@ const main = async () => {
157
134
  try {
158
135
  logMessage("\nDownloading podcast image...");
159
136
  await download({
160
- archive,
161
137
  override,
162
138
  marker: podcastImageUrl,
163
- key: getArchiveKey({
164
- prefix: archivePrefix,
165
- name: `${
166
- feed.title ? `${feed.title}.image` : "image"
167
- }${podcastImageFileExt}`,
168
- }),
169
139
  outputPath: outputImagePath,
170
140
  url: podcastImageUrl,
171
141
  maxAttempts: attempts,
@@ -186,13 +156,8 @@ const main = async () => {
186
156
  try {
187
157
  logMessage("\nSaving podcast metadata...");
188
158
  writeFeedMeta({
189
- archive,
190
159
  override,
191
160
  feed,
192
- key: getArchiveKey({
193
- prefix: archivePrefix,
194
- name: `${feed.title ? `${feed.title}.meta` : "meta"}.json`,
195
- }),
196
161
  outputPath: outputMetaPath,
197
162
  });
198
163
  } catch (error) {
@@ -209,8 +174,7 @@ const main = async () => {
209
174
  }
210
175
 
211
176
  const targetItems = getItemsToDownload({
212
- archive,
213
- archivePrefix,
177
+ addMp3MetadataFlag,
214
178
  basePath,
215
179
  feed,
216
180
  limit,
@@ -240,8 +204,6 @@ const main = async () => {
240
204
 
241
205
  const { numEpisodesDownloaded, hasErrors } = await downloadItemsAsync({
242
206
  addMp3MetadataFlag,
243
- archive,
244
- archivePrefix,
245
207
  attempts,
246
208
  basePath,
247
209
  bitrate,
@@ -252,6 +214,7 @@ const main = async () => {
252
214
  episodeSourceOrder,
253
215
  exec,
254
216
  feed,
217
+ includeEpisodeImages,
255
218
  includeEpisodeMeta,
256
219
  mono,
257
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",
@@ -141,7 +134,7 @@ export const setupCommander = (program) => {
141
134
  .option("--override", "override local files on collision")
142
135
  .option(
143
136
  "--always-postprocess",
144
- "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"
145
138
  )
146
139
  .option("--reverse", "download episodes in reverse order")
147
140
  .option("--info", "print retrieved podcast info instead of downloading")
@@ -162,7 +155,7 @@ export const setupCommander = (program) => {
162
155
  )
163
156
  .option(
164
157
  "--exec <string>",
165
- "Execute a command after each episode is downloaded"
158
+ "execute a command after each episode is downloaded"
166
159
  )
167
160
  .option(
168
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
+ };