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/bin/bin.js CHANGED
@@ -1,42 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs");
4
- const _path = require("path");
5
- const _url = require("url");
6
- const commander = require("commander");
7
-
8
- const { version } = require("../package.json");
9
- const {
10
- download,
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
- runFfmpeg,
25
- } = require("./util");
26
- const { createParseNumber } = require("./validate");
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
- } = require("./logger");
34
- const {
35
- getFilename,
36
- getFolderName,
37
- getArchiveFilename,
38
- getSafeName,
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
- ignoreEpisodeImages,
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 } = _url.parse(url);
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
- if (info) {
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("Saving podcast image");
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 episode image", error);
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
- logMessage("Saving podcast metadata");
229
- writeFeedMeta({
230
- archive,
231
- override,
232
- feed,
233
- key: getArchiveKey({ prefix: archiveUrl, name: outputMetaName }),
234
- outputPath: outputMetaPath,
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
- const nextItem = () => {
261
- counter += 1;
262
- logMessage("");
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
- logMessage("Saving episode metadata");
386
- writeItemMeta({
387
- archive,
388
- override,
389
- item,
390
- key: getArchiveKey({
391
- prefix: archiveUrl,
392
- name: getArchiveFilename({
393
- pubDate: item.pubDate,
394
- name: item.title,
395
- ext: episodeMetaExt,
396
- }),
397
- }),
398
- outputPath: outputEpisodeMetaPath,
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
- nextItem();
296
+ if (numEpisodesDownloaded === 0) {
297
+ process.exit(ERROR_STATUSES.nothingDownloaded);
403
298
  }
404
299
 
405
- if (episodesDownloadedCounter === 0) {
406
- process.exit(ERROR_STATUSES.nothingDownloaded);
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
- module.exports = {
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
- const filenamify = require("filenamify");
2
- const dayjs = require("dayjs");
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
- module.exports = {
71
- getArchiveFilename,
72
- getFilename,
73
- getFolderName,
74
- getSafeName,
75
- };
70
+ export { getArchiveFilename, getFilename, getFolderName, getSafeName };