podcast-dl 10.4.0 → 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 +36 -42
- package/bin/async.js +90 -98
- package/bin/bin.js +15 -52
- package/bin/commander.js +4 -11
- package/bin/exec.js +30 -0
- package/bin/ffmpeg.js +101 -0
- package/bin/items.js +203 -0
- package/bin/logger.js +8 -18
- package/bin/meta.js +33 -0
- package/bin/naming.js +6 -24
- package/bin/util.js +25 -469
- package/bin/validate.js +2 -5
- package/package.json +3 -3
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
|
-
| --
|
|
35
|
-
| --episode-template
|
|
36
|
-
| --
|
|
37
|
-
| --include-meta
|
|
38
|
-
| --include-episode-
|
|
39
|
-
| --include-episode-
|
|
40
|
-
| --
|
|
41
|
-
| --
|
|
42
|
-
| --
|
|
43
|
-
| --
|
|
44
|
-
| --
|
|
45
|
-
| --episode-regex
|
|
46
|
-
| --episode-
|
|
47
|
-
| --episode-
|
|
48
|
-
| --episode-
|
|
49
|
-
| --episode-
|
|
50
|
-
| --
|
|
51
|
-
| --
|
|
52
|
-
| --
|
|
53
|
-
| --
|
|
54
|
-
| --
|
|
55
|
-
| --
|
|
56
|
-
| --
|
|
57
|
-
| --
|
|
58
|
-
| --
|
|
59
|
-
| --
|
|
60
|
-
| --
|
|
61
|
-
| --
|
|
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`
|
|
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 {
|
|
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 {
|
|
8
|
-
|
|
8
|
+
import { download, downloadItemsAsync } from "./async.js";
|
|
9
9
|
import { setupCommander } from "./commander.js";
|
|
10
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
+
};
|