podcast-dl 6.1.0 → 7.1.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 +26 -34
- package/bin/async.js +294 -0
- package/bin/bin.js +85 -190
- package/bin/logger.js +14 -2
- package/bin/naming.js +3 -8
- package/bin/util.js +188 -230
- package/bin/validate.js +9 -3
- package/package.json +8 -5
package/bin/bin.js
CHANGED
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import _path from "path";
|
|
5
|
+
import commander from "commander";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import pluralize from "pluralize";
|
|
8
|
+
|
|
9
|
+
import { download } from "./async.js";
|
|
10
|
+
import {
|
|
11
11
|
getArchiveKey,
|
|
12
|
-
getEpisodeAudioUrlAndExt,
|
|
13
12
|
getFeed,
|
|
14
13
|
getImageUrl,
|
|
15
14
|
getItemsToDownload,
|
|
16
15
|
getUrlExt,
|
|
17
16
|
logFeedInfo,
|
|
18
|
-
logItemInfo,
|
|
19
17
|
logItemsList,
|
|
20
18
|
writeFeedMeta,
|
|
21
|
-
writeItemMeta,
|
|
22
|
-
runExec,
|
|
23
19
|
ITEM_LIST_FORMATS,
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const {
|
|
20
|
+
} from "./util.js";
|
|
21
|
+
import { createParseNumber, hasFfmpeg } from "./validate.js";
|
|
22
|
+
import {
|
|
28
23
|
ERROR_STATUSES,
|
|
29
24
|
LOG_LEVELS,
|
|
30
25
|
logMessage,
|
|
31
26
|
logError,
|
|
32
27
|
logErrorAndExit,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} = require("./naming");
|
|
28
|
+
} from "./logger.js";
|
|
29
|
+
import { getFolderName, getSafeName } from "./naming.js";
|
|
30
|
+
import { downloadItemsAsync } from "./async.js";
|
|
31
|
+
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const { version } = require("../package.json");
|
|
40
34
|
|
|
41
35
|
commander
|
|
42
36
|
.version(version)
|
|
@@ -56,10 +50,7 @@ commander
|
|
|
56
50
|
"--include-episode-meta",
|
|
57
51
|
"write out individual episode metadata to json"
|
|
58
52
|
)
|
|
59
|
-
.option(
|
|
60
|
-
"--ignore-episode-images",
|
|
61
|
-
"ignore downloading found images from --include-episode-meta"
|
|
62
|
-
)
|
|
53
|
+
.option("--include-episode-images", "download found episode images")
|
|
63
54
|
.option(
|
|
64
55
|
"--offset <number>",
|
|
65
56
|
"offset episode to start downloading from (most recent = 0)",
|
|
@@ -85,13 +76,19 @@ commander
|
|
|
85
76
|
)
|
|
86
77
|
.option(
|
|
87
78
|
"--add-mp3-metadata",
|
|
88
|
-
"attempts to add a base level of metadata to .mp3 files using ffmpeg"
|
|
79
|
+
"attempts to add a base level of metadata to .mp3 files using ffmpeg",
|
|
80
|
+
hasFfmpeg
|
|
89
81
|
)
|
|
90
82
|
.option(
|
|
91
83
|
"--adjust-bitrate <string>",
|
|
92
|
-
"attempts to adjust bitrate of .mp3 files using ffmpeg"
|
|
84
|
+
"attempts to adjust bitrate of .mp3 files using ffmpeg",
|
|
85
|
+
hasFfmpeg
|
|
86
|
+
)
|
|
87
|
+
.option(
|
|
88
|
+
"--mono",
|
|
89
|
+
"attempts to force .mp3 files into mono using ffmpeg",
|
|
90
|
+
hasFfmpeg
|
|
93
91
|
)
|
|
94
|
-
.option("--mono", "attempts to force .mp3 files into mono using ffmpeg")
|
|
95
92
|
.option("--override", "override local files on collision")
|
|
96
93
|
.option("--reverse", "download episodes in reverse order")
|
|
97
94
|
.option("--info", "print retrieved podcast info instead of downloading")
|
|
@@ -115,6 +112,16 @@ commander
|
|
|
115
112
|
"--exec <string>",
|
|
116
113
|
"Execute a command after each episode is downloaded"
|
|
117
114
|
)
|
|
115
|
+
.option(
|
|
116
|
+
"--threads <number>",
|
|
117
|
+
"the number of downloads that can happen concurrently",
|
|
118
|
+
createParseNumber({ min: 1, max: 32, name: "threads" }),
|
|
119
|
+
1
|
|
120
|
+
)
|
|
121
|
+
.option(
|
|
122
|
+
"--filter-url-tracking",
|
|
123
|
+
"attempts to extract the direct download link of an episode if detected (experimental)"
|
|
124
|
+
)
|
|
118
125
|
.parse(process.argv);
|
|
119
126
|
|
|
120
127
|
const {
|
|
@@ -123,7 +130,7 @@ const {
|
|
|
123
130
|
episodeTemplate,
|
|
124
131
|
includeMeta,
|
|
125
132
|
includeEpisodeMeta,
|
|
126
|
-
|
|
133
|
+
includeEpisodeImages,
|
|
127
134
|
offset,
|
|
128
135
|
limit,
|
|
129
136
|
episodeRegex,
|
|
@@ -135,6 +142,8 @@ const {
|
|
|
135
142
|
list,
|
|
136
143
|
exec,
|
|
137
144
|
mono,
|
|
145
|
+
threads,
|
|
146
|
+
filterUrlTracking,
|
|
138
147
|
addMp3Metadata: addMp3MetadataFlag,
|
|
139
148
|
adjustBitrate: bitrate,
|
|
140
149
|
} = commander;
|
|
@@ -146,7 +155,7 @@ const main = async () => {
|
|
|
146
155
|
logErrorAndExit("No URL provided");
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
const { hostname, pathname } =
|
|
158
|
+
const { hostname, pathname } = new URL(url);
|
|
150
159
|
const archiveUrl = `${hostname}${pathname}`;
|
|
151
160
|
const feed = await getFeed(url);
|
|
152
161
|
const basePath = _path.resolve(
|
|
@@ -154,9 +163,7 @@ const main = async () => {
|
|
|
154
163
|
getFolderName({ feed, template: outDir })
|
|
155
164
|
);
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
logFeedInfo(feed);
|
|
159
|
-
}
|
|
166
|
+
logFeedInfo(feed);
|
|
160
167
|
|
|
161
168
|
if (list) {
|
|
162
169
|
if (feed.items && feed.items.length) {
|
|
@@ -207,32 +214,35 @@ const main = async () => {
|
|
|
207
214
|
);
|
|
208
215
|
|
|
209
216
|
try {
|
|
210
|
-
logMessage("
|
|
217
|
+
logMessage("\nDownloading podcast image...");
|
|
211
218
|
await download({
|
|
212
219
|
archive,
|
|
213
220
|
override,
|
|
221
|
+
marker: podcastImageUrl,
|
|
214
222
|
key: getArchiveKey({ prefix: archiveUrl, name: podcastImageName }),
|
|
215
223
|
outputPath: outputImagePath,
|
|
216
224
|
url: podcastImageUrl,
|
|
217
225
|
});
|
|
218
226
|
} catch (error) {
|
|
219
|
-
logError("Unable to download
|
|
227
|
+
logError("Unable to download podcast image", error);
|
|
220
228
|
}
|
|
221
|
-
} else {
|
|
222
|
-
logMessage("Unable to find podcast image");
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
const outputMetaName = `${feed.title ? `${feed.title}.meta` : "meta"}.json`;
|
|
226
232
|
const outputMetaPath = _path.resolve(basePath, getSafeName(outputMetaName));
|
|
227
233
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
try {
|
|
235
|
+
logMessage("\nSaving podcast metadata...");
|
|
236
|
+
writeFeedMeta({
|
|
237
|
+
archive,
|
|
238
|
+
override,
|
|
239
|
+
feed,
|
|
240
|
+
key: getArchiveKey({ prefix: archiveUrl, name: outputMetaName }),
|
|
241
|
+
outputPath: outputMetaPath,
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logError("Unable to save podcast metadata", error);
|
|
245
|
+
}
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
if (!feed.items || feed.items.length === 0) {
|
|
@@ -244,6 +254,9 @@ const main = async () => {
|
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
const targetItems = getItemsToDownload({
|
|
257
|
+
archive,
|
|
258
|
+
archiveUrl,
|
|
259
|
+
basePath,
|
|
247
260
|
feed,
|
|
248
261
|
limit,
|
|
249
262
|
offset,
|
|
@@ -251,159 +264,41 @@ const main = async () => {
|
|
|
251
264
|
after,
|
|
252
265
|
before,
|
|
253
266
|
episodeRegex,
|
|
267
|
+
episodeTemplate,
|
|
268
|
+
includeEpisodeImages,
|
|
254
269
|
});
|
|
255
270
|
|
|
256
271
|
if (!targetItems.length) {
|
|
257
272
|
logErrorAndExit("No episodes found with provided criteria to download");
|
|
258
273
|
}
|
|
259
274
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const episodeText = targetItems.length === 1 ? "episode" : "episodes";
|
|
266
|
-
logMessage(`Starting download of ${targetItems.length} ${episodeText}\n`);
|
|
267
|
-
|
|
268
|
-
let counter = 1;
|
|
269
|
-
let episodesDownloadedCounter = 0;
|
|
270
|
-
|
|
271
|
-
for (const item of targetItems) {
|
|
272
|
-
logMessage(`${counter} of ${targetItems.length}`);
|
|
273
|
-
|
|
274
|
-
const { url: episodeAudioUrl, ext: audioFileExt } =
|
|
275
|
-
getEpisodeAudioUrlAndExt(item);
|
|
276
|
-
|
|
277
|
-
if (!episodeAudioUrl) {
|
|
278
|
-
logItemInfo(item, LOG_LEVELS.critical);
|
|
279
|
-
logError("Unable to find episode download URL. Skipping");
|
|
280
|
-
nextItem();
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const episodeFilename = getFilename({
|
|
285
|
-
item,
|
|
286
|
-
feed,
|
|
287
|
-
url: episodeAudioUrl,
|
|
288
|
-
ext: audioFileExt,
|
|
289
|
-
template: episodeTemplate,
|
|
290
|
-
});
|
|
291
|
-
const outputPodcastPath = _path.resolve(basePath, episodeFilename);
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
await download({
|
|
295
|
-
archive,
|
|
296
|
-
override,
|
|
297
|
-
key: getArchiveKey({
|
|
298
|
-
prefix: archiveUrl,
|
|
299
|
-
name: getArchiveFilename({
|
|
300
|
-
name: item.title,
|
|
301
|
-
pubDate: item.pubDate,
|
|
302
|
-
ext: audioFileExt,
|
|
303
|
-
}),
|
|
304
|
-
}),
|
|
305
|
-
outputPath: outputPodcastPath,
|
|
306
|
-
url: episodeAudioUrl,
|
|
307
|
-
onSkip: () => {
|
|
308
|
-
logItemInfo(item);
|
|
309
|
-
},
|
|
310
|
-
onBeforeDownload: () => {
|
|
311
|
-
logItemInfo(item, LOG_LEVELS.important);
|
|
312
|
-
},
|
|
313
|
-
onAfterDownload: () => {
|
|
314
|
-
if (addMp3MetadataFlag || bitrate || mono) {
|
|
315
|
-
runFfmpeg({
|
|
316
|
-
feed,
|
|
317
|
-
item,
|
|
318
|
-
bitrate,
|
|
319
|
-
mono,
|
|
320
|
-
itemIndex: item._originalIndex,
|
|
321
|
-
outputPath: outputPodcastPath,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (exec) {
|
|
326
|
-
runExec({ exec, outputPodcastPath, episodeFilename });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
episodesDownloadedCounter += 1;
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
} catch (error) {
|
|
333
|
-
logError("Unable to download episode", error);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (includeEpisodeMeta) {
|
|
337
|
-
if (!ignoreEpisodeImages) {
|
|
338
|
-
const episodeImageUrl = getImageUrl(item);
|
|
339
|
-
|
|
340
|
-
if (episodeImageUrl) {
|
|
341
|
-
const episodeImageFileExt = getUrlExt(episodeImageUrl);
|
|
342
|
-
const episodeImageName = getFilename({
|
|
343
|
-
item,
|
|
344
|
-
feed,
|
|
345
|
-
url: episodeAudioUrl,
|
|
346
|
-
ext: episodeImageFileExt,
|
|
347
|
-
template: episodeTemplate,
|
|
348
|
-
});
|
|
349
|
-
const outputImagePath = _path.resolve(basePath, episodeImageName);
|
|
350
|
-
|
|
351
|
-
logMessage("Saving episode image");
|
|
352
|
-
try {
|
|
353
|
-
await download({
|
|
354
|
-
archive,
|
|
355
|
-
override,
|
|
356
|
-
key: getArchiveKey({
|
|
357
|
-
prefix: archiveUrl,
|
|
358
|
-
name: getArchiveFilename({
|
|
359
|
-
pubDate: item.pubDate,
|
|
360
|
-
name: item.title,
|
|
361
|
-
ext: episodeImageFileExt,
|
|
362
|
-
}),
|
|
363
|
-
}),
|
|
364
|
-
outputPath: outputImagePath,
|
|
365
|
-
url: episodeImageUrl,
|
|
366
|
-
});
|
|
367
|
-
} catch (error) {
|
|
368
|
-
logError("Unable to download episode image", error);
|
|
369
|
-
}
|
|
370
|
-
} else {
|
|
371
|
-
logMessage("Unable to find episode image URL");
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const episodeMetaExt = ".meta.json";
|
|
376
|
-
const episodeMetaName = getFilename({
|
|
377
|
-
item,
|
|
378
|
-
feed,
|
|
379
|
-
url: episodeAudioUrl,
|
|
380
|
-
ext: episodeMetaExt,
|
|
381
|
-
template: episodeTemplate,
|
|
382
|
-
});
|
|
383
|
-
const outputEpisodeMetaPath = _path.resolve(basePath, episodeMetaName);
|
|
275
|
+
logMessage(
|
|
276
|
+
`\nStarting download of ${pluralize("episode", targetItems.length, true)}\n`
|
|
277
|
+
);
|
|
384
278
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
279
|
+
const { numEpisodesDownloaded, hasErrors } = await downloadItemsAsync({
|
|
280
|
+
addMp3MetadataFlag,
|
|
281
|
+
archive,
|
|
282
|
+
archiveUrl,
|
|
283
|
+
basePath,
|
|
284
|
+
bitrate,
|
|
285
|
+
episodeTemplate,
|
|
286
|
+
exec,
|
|
287
|
+
feed,
|
|
288
|
+
includeEpisodeMeta,
|
|
289
|
+
mono,
|
|
290
|
+
override,
|
|
291
|
+
targetItems,
|
|
292
|
+
threads,
|
|
293
|
+
filterUrlTracking,
|
|
294
|
+
});
|
|
401
295
|
|
|
402
|
-
|
|
296
|
+
if (numEpisodesDownloaded === 0) {
|
|
297
|
+
process.exit(ERROR_STATUSES.nothingDownloaded);
|
|
403
298
|
}
|
|
404
299
|
|
|
405
|
-
if (
|
|
406
|
-
process.exit(ERROR_STATUSES.
|
|
300
|
+
if (hasErrors) {
|
|
301
|
+
process.exit(ERROR_STATUSES.completedWithErrors);
|
|
407
302
|
}
|
|
408
303
|
};
|
|
409
304
|
|
package/bin/logger.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const ERROR_STATUSES = {
|
|
2
2
|
general: 1,
|
|
3
3
|
nothingDownloaded: 2,
|
|
4
|
+
completedWithErrors: 3,
|
|
4
5
|
};
|
|
5
6
|
|
|
6
7
|
const LOG_LEVEL_TYPES = {
|
|
@@ -25,7 +26,7 @@ const getShouldOutputProgressIndicator = () => {
|
|
|
25
26
|
);
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
const logMessage = (message, logLevel = 1) => {
|
|
29
|
+
const logMessage = (message = "", logLevel = 1) => {
|
|
29
30
|
if (
|
|
30
31
|
!process.env.LOG_LEVEL ||
|
|
31
32
|
process.env.LOG_LEVEL === LOG_LEVEL_TYPES.debug ||
|
|
@@ -48,6 +49,16 @@ const logMessage = (message, logLevel = 1) => {
|
|
|
48
49
|
}
|
|
49
50
|
};
|
|
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
|
+
|
|
51
62
|
const logError = (msg, error) => {
|
|
52
63
|
if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
|
|
53
64
|
return;
|
|
@@ -70,9 +81,10 @@ const logErrorAndExit = (msg, error) => {
|
|
|
70
81
|
process.exit(ERROR_STATUSES.general);
|
|
71
82
|
};
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
export {
|
|
74
85
|
ERROR_STATUSES,
|
|
75
86
|
getShouldOutputProgressIndicator,
|
|
87
|
+
getLogMessageWithMarker,
|
|
76
88
|
LOG_LEVELS,
|
|
77
89
|
logMessage,
|
|
78
90
|
logError,
|
package/bin/naming.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import filenamify from "filenamify";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
3
|
|
|
4
4
|
const INVALID_CHAR_REPLACE = "_";
|
|
5
5
|
const MAX_LENGTH_FILENAME = 255;
|
|
@@ -67,9 +67,4 @@ const getArchiveFilename = ({ pubDate, name, ext }) => {
|
|
|
67
67
|
return getSafeName(`${baseName}${ext}`);
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
getArchiveFilename,
|
|
72
|
-
getFilename,
|
|
73
|
-
getFolderName,
|
|
74
|
-
getSafeName,
|
|
75
|
-
};
|
|
70
|
+
export { getArchiveFilename, getFilename, getFolderName, getSafeName };
|