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/README.md +36 -41
- package/bin/async.js +90 -98
- package/bin/bin.js +18 -52
- package/bin/commander.js +8 -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 -459
- package/bin/validate.js +2 -5
- package/package.json +3 -3
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
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = [
|
|
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": "
|
|
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": ">=
|
|
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"
|