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/bin/util.js CHANGED
@@ -1,17 +1,11 @@
1
- import rssParser from "rss-parser";
2
- import path from "path";
3
1
  import fs from "fs";
4
- import dayjs from "dayjs";
5
- import util from "util";
6
- import { exec } from "child_process";
7
-
8
- import { logErrorAndExit, logMessage, LOG_LEVELS } from "./logger.js";
9
- import { getArchiveFilename, getItemFilename } from "./naming.js";
2
+ import path from "path";
3
+ import rssParser from "rss-parser";
4
+ import { logErrorAndExit, logMessage } from "./logger.js";
10
5
 
11
- const execWithPromise = util.promisify(exec);
12
6
  const isWin = process.platform === "win32";
13
7
 
14
- const defaultRssParserConfig = {
8
+ export const defaultRssParserConfig = {
15
9
  defaultRSS: 2.0,
16
10
  headers: {
17
11
  Accept: "*/*",
@@ -27,12 +21,12 @@ const defaultRssParserConfig = {
27
21
  Additionally, @see https://www.robvanderwoude.com/escapechars.php for why
28
22
  we avoid trying to escape complex sequences in Windows.
29
23
  */
30
- const escapeArgForShell = (arg) => {
24
+ export const escapeArgForShell = (arg) => {
31
25
  let result = arg;
32
26
 
33
27
  if (/[^A-Za-z0-9_/:=-]/.test(result)) {
34
28
  if (isWin) {
35
- return null;
29
+ return `"${result}"`;
36
30
  } else {
37
31
  result = "'" + result.replace(/'/g, "'\\''") + "'";
38
32
  result = result
@@ -44,11 +38,11 @@ const escapeArgForShell = (arg) => {
44
38
  return result;
45
39
  };
46
40
 
47
- const getTempPath = (path) => {
41
+ export const getTempPath = (path) => {
48
42
  return `${path}.tmp`;
49
43
  };
50
44
 
51
- const prepareOutputPath = (outputPath) => {
45
+ export const prepareOutputPath = (outputPath) => {
52
46
  const outputPathSegments = outputPath.split(path.sep);
53
47
  outputPathSegments.pop();
54
48
 
@@ -59,11 +53,7 @@ const prepareOutputPath = (outputPath) => {
59
53
  }
60
54
  };
61
55
 
62
- const getArchiveKey = ({ prefix, name }) => {
63
- return `${prefix}-${name}`;
64
- };
65
-
66
- const getPublicObject = (object, exclude = []) => {
56
+ export const getPublicObject = (object, exclude = []) => {
67
57
  const output = {};
68
58
  Object.keys(object).forEach((key) => {
69
59
  if (!key.startsWith("_") && !exclude.includes(key) && object[key]) {
@@ -74,7 +64,7 @@ const getPublicObject = (object, exclude = []) => {
74
64
  return output;
75
65
  };
76
66
 
77
- const getFileString = (filePath) => {
67
+ export const getFileString = (filePath) => {
78
68
  const fullPath = path.resolve(process.cwd(), filePath);
79
69
 
80
70
  if (!fs.existsSync(fullPath)) {
@@ -90,7 +80,7 @@ const getFileString = (filePath) => {
90
80
  return data;
91
81
  };
92
82
 
93
- const getJsonFile = (filePath) => {
83
+ export const getJsonFile = (filePath) => {
94
84
  const fullPath = path.resolve(process.cwd(), filePath);
95
85
 
96
86
  if (!fs.existsSync(fullPath)) {
@@ -106,28 +96,7 @@ const getJsonFile = (filePath) => {
106
96
  return JSON.parse(data);
107
97
  };
108
98
 
109
- const getArchive = (archive) => {
110
- const archiveContent = getJsonFile(archive);
111
- return archiveContent === null ? [] : archiveContent;
112
- };
113
-
114
- const writeToArchive = ({ key, archive }) => {
115
- const archivePath = path.resolve(process.cwd(), archive);
116
- const archiveResult = getArchive(archive);
117
-
118
- if (!archiveResult.includes(key)) {
119
- archiveResult.push(key);
120
- }
121
-
122
- fs.writeFileSync(archivePath, JSON.stringify(archiveResult, null, 4));
123
- };
124
-
125
- const getIsInArchive = ({ key, archive }) => {
126
- const archiveResult = getArchive(archive);
127
- return archiveResult.includes(key);
128
- };
129
-
130
- const getLoopControls = ({ offset, length, reverse }) => {
99
+ export const getLoopControls = ({ offset, length, reverse }) => {
131
100
  if (reverse) {
132
101
  const startIndex = length - 1 - offset;
133
102
  const min = -1;
@@ -153,289 +122,13 @@ const getLoopControls = ({ offset, length, reverse }) => {
153
122
  };
154
123
  };
155
124
 
156
- const getItemsToDownload = ({
157
- archive,
158
- archivePrefix,
159
- basePath,
160
- feed,
161
- limit,
162
- offset,
163
- reverse,
164
- before,
165
- after,
166
- episodeDigits,
167
- episodeNumOffset,
168
- episodeRegex,
169
- episodeSourceOrder,
170
- episodeTemplate,
171
- episodeCustomTemplateOptions,
172
- includeEpisodeImages,
173
- includeEpisodeTranscripts,
174
- episodeTranscriptTypes,
175
- }) => {
176
- const { startIndex, shouldGo, next } = getLoopControls({
177
- offset,
178
- reverse,
179
- length: feed.items.length,
180
- });
181
-
182
- let i = startIndex;
183
- const items = [];
184
-
185
- const savedArchive = archive ? getArchive(archive) : [];
186
-
187
- while (shouldGo(i)) {
188
- const { title, pubDate } = feed.items[i];
189
- const pubDateDay = dayjs(new Date(pubDate));
190
- let isValid = true;
191
-
192
- if (episodeRegex) {
193
- const generatedEpisodeRegex = new RegExp(episodeRegex);
194
- if (title && !generatedEpisodeRegex.test(title)) {
195
- isValid = false;
196
- }
197
- }
198
-
199
- if (before) {
200
- const beforeDateDay = dayjs(new Date(before));
201
- if (
202
- !pubDateDay.isSame(beforeDateDay, "day") &&
203
- !pubDateDay.isBefore(beforeDateDay, "day")
204
- ) {
205
- isValid = false;
206
- }
207
- }
208
-
209
- if (after) {
210
- const afterDateDay = dayjs(new Date(after));
211
- if (
212
- !pubDateDay.isSame(afterDateDay, "day") &&
213
- !pubDateDay.isAfter(afterDateDay, "day")
214
- ) {
215
- isValid = false;
216
- }
217
- }
218
-
219
- const { url: episodeAudioUrl, ext: audioFileExt } =
220
- getEpisodeAudioUrlAndExt(feed.items[i], episodeSourceOrder);
221
- const key = getArchiveKey({
222
- prefix: archivePrefix,
223
- name: getArchiveFilename({
224
- pubDate,
225
- name: title,
226
- ext: audioFileExt,
227
- }),
228
- });
229
-
230
- if (key && savedArchive.includes(key)) {
231
- isValid = false;
232
- }
233
-
234
- if (isValid) {
235
- const item = feed.items[i];
236
- item._originalIndex = i;
237
- item._extra_downloads = [];
238
-
239
- if (includeEpisodeImages) {
240
- const episodeImageUrl = getImageUrl(item);
241
-
242
- if (episodeImageUrl) {
243
- const episodeImageFileExt = getUrlExt(episodeImageUrl);
244
- const episodeImageArchiveKey = getArchiveKey({
245
- prefix: archivePrefix,
246
- name: getArchiveFilename({
247
- pubDate,
248
- name: title,
249
- ext: episodeImageFileExt,
250
- }),
251
- });
252
-
253
- const episodeImageName = getItemFilename({
254
- item,
255
- feed,
256
- url: episodeAudioUrl,
257
- ext: episodeImageFileExt,
258
- template: episodeTemplate,
259
- customTemplateOptions: episodeCustomTemplateOptions,
260
- width: episodeDigits,
261
- offset: episodeNumOffset,
262
- });
263
-
264
- const outputImagePath = path.resolve(basePath, episodeImageName);
265
- item._extra_downloads.push({
266
- url: episodeImageUrl,
267
- outputPath: outputImagePath,
268
- key: episodeImageArchiveKey,
269
- });
270
- }
271
- }
272
-
273
- if (includeEpisodeTranscripts) {
274
- const episodeTranscriptUrl = getTranscriptUrl(
275
- item,
276
- episodeTranscriptTypes
277
- );
278
-
279
- if (episodeTranscriptUrl) {
280
- const episodeTranscriptFileExt = getUrlExt(episodeTranscriptUrl);
281
- const episodeTranscriptArchiveKey = getArchiveKey({
282
- prefix: archivePrefix,
283
- name: getArchiveFilename({
284
- pubDate,
285
- name: title,
286
- ext: episodeTranscriptFileExt,
287
- }),
288
- });
289
-
290
- const episodeTranscriptName = getItemFilename({
291
- item,
292
- feed,
293
- url: episodeAudioUrl,
294
- ext: episodeTranscriptFileExt,
295
- template: episodeTemplate,
296
- width: episodeDigits,
297
- offset: episodeNumOffset,
298
- });
299
-
300
- const outputTranscriptPath = path.resolve(
301
- basePath,
302
- episodeTranscriptName
303
- );
304
-
305
- item._extra_downloads.push({
306
- url: episodeTranscriptUrl,
307
- outputPath: outputTranscriptPath,
308
- key: episodeTranscriptArchiveKey,
309
- });
310
- }
311
- }
312
-
313
- items.push(item);
314
- }
315
-
316
- i = next(i);
317
- }
318
-
319
- return limit ? items.slice(0, limit) : items;
320
- };
321
-
322
- const logFeedInfo = (feed) => {
125
+ export const logFeedInfo = (feed) => {
323
126
  logMessage(feed.title);
324
127
  logMessage(feed.description);
325
128
  logMessage();
326
129
  };
327
130
 
328
- const ITEM_LIST_FORMATS = ["table", "json"];
329
-
330
- const logItemsList = ({
331
- type,
332
- feed,
333
- limit,
334
- offset,
335
- reverse,
336
- before,
337
- after,
338
- episodeRegex,
339
- }) => {
340
- const items = getItemsToDownload({
341
- feed,
342
- limit,
343
- offset,
344
- reverse,
345
- before,
346
- after,
347
- episodeRegex,
348
- });
349
-
350
- if (!items.length) {
351
- logErrorAndExit("No episodes found with provided criteria to list");
352
- }
353
-
354
- const isJson = type === "json";
355
-
356
- const output = items.map((item) => {
357
- const data = {
358
- episodeNum: feed.items.length - item._originalIndex,
359
- title: item.title,
360
- pubDate: item.pubDate,
361
- };
362
-
363
- return data;
364
- });
365
-
366
- if (isJson) {
367
- // eslint-disable-next-line no-console
368
- console.log(JSON.stringify(output));
369
- return;
370
- }
371
-
372
- // eslint-disable-next-line no-console
373
- console.table(output);
374
- };
375
-
376
- const writeFeedMeta = ({ outputPath, feed, key, archive, override }) => {
377
- if (key && archive && getIsInArchive({ key, archive })) {
378
- logMessage("Feed metadata exists in archive. Skipping...");
379
- return;
380
- }
381
- const output = getPublicObject(feed, ["items"]);
382
-
383
- try {
384
- if (override || !fs.existsSync(outputPath)) {
385
- fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
386
- } else {
387
- logMessage("Feed metadata exists locally. Skipping...");
388
- }
389
-
390
- if (key && archive && !getIsInArchive({ key, archive })) {
391
- try {
392
- writeToArchive({ key, archive });
393
- } catch (error) {
394
- throw new Error(`Error writing to archive: ${error.toString()}`);
395
- }
396
- }
397
- } catch (error) {
398
- throw new Error(
399
- `Unable to save metadata file for feed: ${error.toString()}`
400
- );
401
- }
402
- };
403
-
404
- const writeItemMeta = ({
405
- marker,
406
- outputPath,
407
- item,
408
- key,
409
- archive,
410
- override,
411
- }) => {
412
- if (key && archive && getIsInArchive({ key, archive })) {
413
- logMessage(`${marker} | Episode metadata exists in archive. Skipping...`);
414
- return;
415
- }
416
-
417
- const output = getPublicObject(item);
418
-
419
- try {
420
- if (override || !fs.existsSync(outputPath)) {
421
- fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
422
- } else {
423
- logMessage(`${marker} | Episode metadata exists locally. Skipping...`);
424
- }
425
-
426
- if (key && archive && !getIsInArchive({ key, archive })) {
427
- try {
428
- writeToArchive({ key, archive });
429
- } catch (error) {
430
- throw new Error("Error writing to archive", error);
431
- }
432
- }
433
- } catch (error) {
434
- throw new Error("Unable to save meta file for episode", error);
435
- }
436
- };
437
-
438
- const getUrlExt = (url) => {
131
+ export const getUrlExt = (url) => {
439
132
  if (!url) {
440
133
  return "";
441
134
  }
@@ -450,7 +143,7 @@ const getUrlExt = (url) => {
450
143
  return ext;
451
144
  };
452
145
 
453
- const AUDIO_TYPES_TO_EXTS = {
146
+ export const AUDIO_TYPES_TO_EXTS = {
454
147
  "audio/aac": ".aac",
455
148
  "audio/flac": ".flac",
456
149
  "audio/mp3": ".mp3",
@@ -467,9 +160,11 @@ const AUDIO_TYPES_TO_EXTS = {
467
160
  "video/x-m4v": ".m4v",
468
161
  };
469
162
 
470
- const VALID_AUDIO_EXTS = [...new Set(Object.values(AUDIO_TYPES_TO_EXTS))];
163
+ export const VALID_AUDIO_EXTS = [
164
+ ...new Set(Object.values(AUDIO_TYPES_TO_EXTS)),
165
+ ];
471
166
 
472
- const getIsAudioUrl = (url) => {
167
+ export const getIsAudioUrl = (url) => {
473
168
  let ext;
474
169
  try {
475
170
  ext = getUrlExt(url);
@@ -484,12 +179,12 @@ const getIsAudioUrl = (url) => {
484
179
  return VALID_AUDIO_EXTS.includes(ext);
485
180
  };
486
181
 
487
- const AUDIO_ORDER_TYPES = {
182
+ export const AUDIO_ORDER_TYPES = {
488
183
  enclosure: "enclosure",
489
184
  link: "link",
490
185
  };
491
186
 
492
- const getEpisodeAudioUrlAndExt = (
187
+ export const getEpisodeAudioUrlAndExt = (
493
188
  { enclosure, link },
494
189
  order = [AUDIO_ORDER_TYPES.enclosure, AUDIO_ORDER_TYPES.link]
495
190
  ) => {
@@ -512,7 +207,7 @@ const getEpisodeAudioUrlAndExt = (
512
207
  return { url: null, ext: null };
513
208
  };
514
209
 
515
- const getImageUrl = ({ image, itunes }) => {
210
+ export const getImageUrl = ({ image, itunes }) => {
516
211
  if (image?.url) {
517
212
  return image.url;
518
213
  }
@@ -539,7 +234,7 @@ export const TRANSCRIPT_TYPES = {
539
234
  };
540
235
 
541
236
  // @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#transcript
542
- const getTranscriptUrl = (item, transcriptTypes = []) => {
237
+ export const getTranscriptUrl = (item, transcriptTypes = []) => {
543
238
  if (!item.podcastTranscripts?.length) {
544
239
  return null;
545
240
  }
@@ -558,7 +253,7 @@ const getTranscriptUrl = (item, transcriptTypes = []) => {
558
253
  return null;
559
254
  };
560
255
 
561
- const getFileFeed = async (filePath, parserConfig) => {
256
+ export const getFileFeed = async (filePath, parserConfig) => {
562
257
  const config = parserConfig
563
258
  ? getJsonFile(parserConfig)
564
259
  : defaultRssParserConfig;
@@ -580,7 +275,7 @@ const getFileFeed = async (filePath, parserConfig) => {
580
275
  return feed;
581
276
  };
582
277
 
583
- const getUrlFeed = async (url, parserConfig) => {
278
+ export const getUrlFeed = async (url, parserConfig) => {
584
279
  const config = parserConfig
585
280
  ? getJsonFile(parserConfig)
586
281
  : defaultRssParserConfig;
@@ -602,132 +297,3 @@ const getUrlFeed = async (url, parserConfig) => {
602
297
 
603
298
  return feed;
604
299
  };
605
-
606
- const runFfmpeg = async ({
607
- feed,
608
- item,
609
- itemIndex,
610
- outputPath,
611
- bitrate,
612
- mono,
613
- addMp3Metadata,
614
- ext,
615
- }) => {
616
- if (!fs.existsSync(outputPath)) {
617
- return;
618
- }
619
-
620
- let command = `ffmpeg -loglevel quiet -i "${outputPath}"`;
621
-
622
- if (bitrate) {
623
- command += ` -b:a ${bitrate}`;
624
- }
625
-
626
- if (mono) {
627
- command += " -ac 1";
628
- }
629
-
630
- if (addMp3Metadata) {
631
- const album = feed.title || "";
632
- const artist = item.itunes?.author || item.author || "";
633
- const title = item.title || "";
634
- const subtitle = item.itunes?.subtitle || "";
635
- const comment = item.content || "";
636
- const disc = item.itunes?.season || "";
637
- const track = item.itunes?.episode || `${feed.items.length - itemIndex}`;
638
- const episodeType = item.itunes?.episodeType || "";
639
- const date = item.pubDate
640
- ? dayjs(new Date(item.pubDate)).format("YYYY-MM-DD")
641
- : "";
642
-
643
- const metaKeysToValues = {
644
- album,
645
- artist,
646
- album_artist: artist,
647
- title,
648
- subtitle,
649
- comment,
650
- disc,
651
- track,
652
- "episode-type": episodeType,
653
- date,
654
- };
655
-
656
- const metadataString = Object.keys(metaKeysToValues)
657
- .map((key) => {
658
- if (!metaKeysToValues[key]) {
659
- return null;
660
- }
661
-
662
- const argValue = escapeArgForShell(metaKeysToValues[key]);
663
-
664
- return argValue ? `-metadata ${key}=${argValue}` : null;
665
- })
666
- .filter((segment) => !!segment)
667
- .join(" ");
668
-
669
- command += ` -map_metadata 0 ${metadataString} -codec copy`;
670
- }
671
-
672
- const tmpMp3Path = `${outputPath}.tmp${ext}`;
673
- command += ` "${tmpMp3Path}"`;
674
- logMessage("Running command: " + command, LOG_LEVELS.debug);
675
-
676
- try {
677
- await execWithPromise(command, { stdio: "ignore" });
678
- } catch (error) {
679
- if (fs.existsSync(tmpMp3Path)) {
680
- fs.unlinkSync(tmpMp3Path);
681
- }
682
-
683
- throw error;
684
- }
685
-
686
- fs.unlinkSync(outputPath);
687
- fs.renameSync(tmpMp3Path, outputPath);
688
- };
689
-
690
- const runExec = async ({
691
- exec,
692
- basePath,
693
- outputPodcastPath,
694
- episodeFilename,
695
- episodeAudioUrl,
696
- }) => {
697
- const episodeFilenameBase = episodeFilename.substring(
698
- 0,
699
- episodeFilename.lastIndexOf(".")
700
- );
701
-
702
- const execCmd = exec
703
- .replace(/{{episode_path}}/g, `"${outputPodcastPath}"`)
704
- .replace(/{{episode_path_base}}/g, `"${basePath}"`)
705
- .replace(/{{episode_filename}}/g, `"${episodeFilename}"`)
706
- .replace(/{{episode_filename_base}}/g, `"${episodeFilenameBase}"`)
707
- .replace(/{{url}}/g, `"${episodeAudioUrl}"`);
708
-
709
- await execWithPromise(execCmd, { stdio: "ignore" });
710
- };
711
-
712
- export {
713
- AUDIO_ORDER_TYPES,
714
- getArchive,
715
- getIsInArchive,
716
- getArchiveKey,
717
- writeToArchive,
718
- getEpisodeAudioUrlAndExt,
719
- getFileFeed,
720
- getImageUrl,
721
- getItemsToDownload,
722
- getTempPath,
723
- getUrlExt,
724
- getUrlFeed,
725
- logFeedInfo,
726
- ITEM_LIST_FORMATS,
727
- logItemsList,
728
- prepareOutputPath,
729
- writeFeedMeta,
730
- writeItemMeta,
731
- runFfmpeg,
732
- runExec,
733
- };
package/bin/validate.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { sync as commandExistsSync } from "command-exists";
2
-
3
2
  import { logErrorAndExit } from "./logger.js";
4
3
 
5
- const createParseNumber = ({ min, max, name, required = true }) => {
4
+ export const createParseNumber = ({ min, max, name, required = true }) => {
6
5
  return (value) => {
7
6
  if (!value && !required) {
8
7
  return undefined;
@@ -33,10 +32,8 @@ const createParseNumber = ({ min, max, name, required = true }) => {
33
32
  };
34
33
  };
35
34
 
36
- const hasFfmpeg = () => {
35
+ export const hasFfmpeg = () => {
37
36
  if (!commandExistsSync("ffmpeg")) {
38
37
  logErrorAndExit('option specified requires "ffmpeg" be available');
39
38
  }
40
39
  };
41
-
42
- export { createParseNumber, hasFfmpeg };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "podcast-dl",
3
- "version": "10.3.3",
3
+ "version": "11.0.0",
4
4
  "description": "A CLI for downloading podcasts.",
5
5
  "type": "module",
6
6
  "bin": "./bin/bin.js",
@@ -26,7 +26,7 @@
26
26
  "cli"
27
27
  ],
28
28
  "engines": {
29
- "node": ">=18.17.0"
29
+ "node": ">=22.16.0"
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",
@@ -38,11 +38,11 @@
38
38
  "author": "Joshua Pohl",
39
39
  "license": "MIT",
40
40
  "devDependencies": {
41
+ "@yao-pkg/pkg": "^5.8.0",
41
42
  "eslint": "^6.8.0",
42
43
  "eslint-config-prettier": "^6.11.0",
43
44
  "husky": "^4.2.5",
44
45
  "lint-staged": "^10.1.7",
45
- "pkg": "^5.8.0",
46
46
  "prettier": "2.3.2",
47
47
  "rimraf": "^3.0.2",
48
48
  "webpack": "^5.75.0"