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/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,299 +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
|
-
episodeRegexExclude,
|
|
170
|
-
episodeSourceOrder,
|
|
171
|
-
episodeTemplate,
|
|
172
|
-
episodeCustomTemplateOptions,
|
|
173
|
-
includeEpisodeImages,
|
|
174
|
-
includeEpisodeTranscripts,
|
|
175
|
-
episodeTranscriptTypes,
|
|
176
|
-
}) => {
|
|
177
|
-
const { startIndex, shouldGo, next } = getLoopControls({
|
|
178
|
-
offset,
|
|
179
|
-
reverse,
|
|
180
|
-
length: feed.items.length,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
let i = startIndex;
|
|
184
|
-
const items = [];
|
|
185
|
-
|
|
186
|
-
const savedArchive = archive ? getArchive(archive) : [];
|
|
187
|
-
|
|
188
|
-
while (shouldGo(i)) {
|
|
189
|
-
const { title, pubDate } = feed.items[i];
|
|
190
|
-
const pubDateDay = dayjs(new Date(pubDate));
|
|
191
|
-
let isValid = true;
|
|
192
|
-
|
|
193
|
-
if (episodeRegex) {
|
|
194
|
-
const generatedEpisodeRegex = new RegExp(episodeRegex);
|
|
195
|
-
if (title && !generatedEpisodeRegex.test(title)) {
|
|
196
|
-
isValid = false;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (episodeRegexExclude) {
|
|
201
|
-
const generatedEpisodeRegexExclude = new RegExp(episodeRegexExclude);
|
|
202
|
-
if (title && generatedEpisodeRegexExclude.test(title)) {
|
|
203
|
-
isValid = false;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (before) {
|
|
208
|
-
const beforeDateDay = dayjs(new Date(before));
|
|
209
|
-
if (
|
|
210
|
-
!pubDateDay.isSame(beforeDateDay, "day") &&
|
|
211
|
-
!pubDateDay.isBefore(beforeDateDay, "day")
|
|
212
|
-
) {
|
|
213
|
-
isValid = false;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (after) {
|
|
218
|
-
const afterDateDay = dayjs(new Date(after));
|
|
219
|
-
if (
|
|
220
|
-
!pubDateDay.isSame(afterDateDay, "day") &&
|
|
221
|
-
!pubDateDay.isAfter(afterDateDay, "day")
|
|
222
|
-
) {
|
|
223
|
-
isValid = false;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const { url: episodeAudioUrl, ext: audioFileExt } =
|
|
228
|
-
getEpisodeAudioUrlAndExt(feed.items[i], episodeSourceOrder);
|
|
229
|
-
const key = getArchiveKey({
|
|
230
|
-
prefix: archivePrefix,
|
|
231
|
-
name: getArchiveFilename({
|
|
232
|
-
pubDate,
|
|
233
|
-
name: title,
|
|
234
|
-
ext: audioFileExt,
|
|
235
|
-
}),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
if (key && savedArchive.includes(key)) {
|
|
239
|
-
isValid = false;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (isValid) {
|
|
243
|
-
const item = feed.items[i];
|
|
244
|
-
item._originalIndex = i;
|
|
245
|
-
item._extra_downloads = [];
|
|
246
|
-
|
|
247
|
-
if (includeEpisodeImages) {
|
|
248
|
-
const episodeImageUrl = getImageUrl(item);
|
|
249
|
-
|
|
250
|
-
if (episodeImageUrl) {
|
|
251
|
-
const episodeImageFileExt = getUrlExt(episodeImageUrl);
|
|
252
|
-
const episodeImageArchiveKey = getArchiveKey({
|
|
253
|
-
prefix: archivePrefix,
|
|
254
|
-
name: getArchiveFilename({
|
|
255
|
-
pubDate,
|
|
256
|
-
name: title,
|
|
257
|
-
ext: episodeImageFileExt,
|
|
258
|
-
}),
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const episodeImageName = getItemFilename({
|
|
262
|
-
item,
|
|
263
|
-
feed,
|
|
264
|
-
url: episodeAudioUrl,
|
|
265
|
-
ext: episodeImageFileExt,
|
|
266
|
-
template: episodeTemplate,
|
|
267
|
-
customTemplateOptions: episodeCustomTemplateOptions,
|
|
268
|
-
width: episodeDigits,
|
|
269
|
-
offset: episodeNumOffset,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const outputImagePath = path.resolve(basePath, episodeImageName);
|
|
273
|
-
item._extra_downloads.push({
|
|
274
|
-
url: episodeImageUrl,
|
|
275
|
-
outputPath: outputImagePath,
|
|
276
|
-
key: episodeImageArchiveKey,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (includeEpisodeTranscripts) {
|
|
282
|
-
const episodeTranscriptUrl = getTranscriptUrl(
|
|
283
|
-
item,
|
|
284
|
-
episodeTranscriptTypes
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
if (episodeTranscriptUrl) {
|
|
288
|
-
const episodeTranscriptFileExt = getUrlExt(episodeTranscriptUrl);
|
|
289
|
-
const episodeTranscriptArchiveKey = getArchiveKey({
|
|
290
|
-
prefix: archivePrefix,
|
|
291
|
-
name: getArchiveFilename({
|
|
292
|
-
pubDate,
|
|
293
|
-
name: title,
|
|
294
|
-
ext: episodeTranscriptFileExt,
|
|
295
|
-
}),
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const episodeTranscriptName = getItemFilename({
|
|
299
|
-
item,
|
|
300
|
-
feed,
|
|
301
|
-
url: episodeAudioUrl,
|
|
302
|
-
ext: episodeTranscriptFileExt,
|
|
303
|
-
template: episodeTemplate,
|
|
304
|
-
width: episodeDigits,
|
|
305
|
-
offset: episodeNumOffset,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const outputTranscriptPath = path.resolve(
|
|
309
|
-
basePath,
|
|
310
|
-
episodeTranscriptName
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
item._extra_downloads.push({
|
|
314
|
-
url: episodeTranscriptUrl,
|
|
315
|
-
outputPath: outputTranscriptPath,
|
|
316
|
-
key: episodeTranscriptArchiveKey,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
items.push(item);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
i = next(i);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return limit ? items.slice(0, limit) : items;
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const logFeedInfo = (feed) => {
|
|
125
|
+
export const logFeedInfo = (feed) => {
|
|
331
126
|
logMessage(feed.title);
|
|
332
127
|
logMessage(feed.description);
|
|
333
128
|
logMessage();
|
|
334
129
|
};
|
|
335
130
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
const logItemsList = ({
|
|
339
|
-
type,
|
|
340
|
-
feed,
|
|
341
|
-
limit,
|
|
342
|
-
offset,
|
|
343
|
-
reverse,
|
|
344
|
-
before,
|
|
345
|
-
after,
|
|
346
|
-
episodeRegex,
|
|
347
|
-
episodeRegexExclude,
|
|
348
|
-
}) => {
|
|
349
|
-
const items = getItemsToDownload({
|
|
350
|
-
feed,
|
|
351
|
-
limit,
|
|
352
|
-
offset,
|
|
353
|
-
reverse,
|
|
354
|
-
before,
|
|
355
|
-
after,
|
|
356
|
-
episodeRegex,
|
|
357
|
-
episodeRegexExclude,
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
if (!items.length) {
|
|
361
|
-
logErrorAndExit("No episodes found with provided criteria to list");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const isJson = type === "json";
|
|
365
|
-
|
|
366
|
-
const output = items.map((item) => {
|
|
367
|
-
const data = {
|
|
368
|
-
episodeNum: feed.items.length - item._originalIndex,
|
|
369
|
-
title: item.title,
|
|
370
|
-
pubDate: item.pubDate,
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
return data;
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
if (isJson) {
|
|
377
|
-
// eslint-disable-next-line no-console
|
|
378
|
-
console.log(JSON.stringify(output));
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// eslint-disable-next-line no-console
|
|
383
|
-
console.table(output);
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const writeFeedMeta = ({ outputPath, feed, key, archive, override }) => {
|
|
387
|
-
if (key && archive && getIsInArchive({ key, archive })) {
|
|
388
|
-
logMessage("Feed metadata exists in archive. Skipping...");
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const output = getPublicObject(feed, ["items"]);
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
if (override || !fs.existsSync(outputPath)) {
|
|
395
|
-
fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
|
|
396
|
-
} else {
|
|
397
|
-
logMessage("Feed metadata exists locally. Skipping...");
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (key && archive && !getIsInArchive({ key, archive })) {
|
|
401
|
-
try {
|
|
402
|
-
writeToArchive({ key, archive });
|
|
403
|
-
} catch (error) {
|
|
404
|
-
throw new Error(`Error writing to archive: ${error.toString()}`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
} catch (error) {
|
|
408
|
-
throw new Error(
|
|
409
|
-
`Unable to save metadata file for feed: ${error.toString()}`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const writeItemMeta = ({
|
|
415
|
-
marker,
|
|
416
|
-
outputPath,
|
|
417
|
-
item,
|
|
418
|
-
key,
|
|
419
|
-
archive,
|
|
420
|
-
override,
|
|
421
|
-
}) => {
|
|
422
|
-
if (key && archive && getIsInArchive({ key, archive })) {
|
|
423
|
-
logMessage(`${marker} | Episode metadata exists in archive. Skipping...`);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const output = getPublicObject(item);
|
|
428
|
-
|
|
429
|
-
try {
|
|
430
|
-
if (override || !fs.existsSync(outputPath)) {
|
|
431
|
-
fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
|
|
432
|
-
} else {
|
|
433
|
-
logMessage(`${marker} | Episode metadata exists locally. Skipping...`);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (key && archive && !getIsInArchive({ key, archive })) {
|
|
437
|
-
try {
|
|
438
|
-
writeToArchive({ key, archive });
|
|
439
|
-
} catch (error) {
|
|
440
|
-
throw new Error("Error writing to archive", error);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
} catch (error) {
|
|
444
|
-
throw new Error("Unable to save meta file for episode", error);
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
const getUrlExt = (url) => {
|
|
131
|
+
export const getUrlExt = (url) => {
|
|
449
132
|
if (!url) {
|
|
450
133
|
return "";
|
|
451
134
|
}
|
|
@@ -460,7 +143,7 @@ const getUrlExt = (url) => {
|
|
|
460
143
|
return ext;
|
|
461
144
|
};
|
|
462
145
|
|
|
463
|
-
const AUDIO_TYPES_TO_EXTS = {
|
|
146
|
+
export const AUDIO_TYPES_TO_EXTS = {
|
|
464
147
|
"audio/aac": ".aac",
|
|
465
148
|
"audio/flac": ".flac",
|
|
466
149
|
"audio/mp3": ".mp3",
|
|
@@ -477,9 +160,11 @@ const AUDIO_TYPES_TO_EXTS = {
|
|
|
477
160
|
"video/x-m4v": ".m4v",
|
|
478
161
|
};
|
|
479
162
|
|
|
480
|
-
const VALID_AUDIO_EXTS = [
|
|
163
|
+
export const VALID_AUDIO_EXTS = [
|
|
164
|
+
...new Set(Object.values(AUDIO_TYPES_TO_EXTS)),
|
|
165
|
+
];
|
|
481
166
|
|
|
482
|
-
const getIsAudioUrl = (url) => {
|
|
167
|
+
export const getIsAudioUrl = (url) => {
|
|
483
168
|
let ext;
|
|
484
169
|
try {
|
|
485
170
|
ext = getUrlExt(url);
|
|
@@ -494,12 +179,12 @@ const getIsAudioUrl = (url) => {
|
|
|
494
179
|
return VALID_AUDIO_EXTS.includes(ext);
|
|
495
180
|
};
|
|
496
181
|
|
|
497
|
-
const AUDIO_ORDER_TYPES = {
|
|
182
|
+
export const AUDIO_ORDER_TYPES = {
|
|
498
183
|
enclosure: "enclosure",
|
|
499
184
|
link: "link",
|
|
500
185
|
};
|
|
501
186
|
|
|
502
|
-
const getEpisodeAudioUrlAndExt = (
|
|
187
|
+
export const getEpisodeAudioUrlAndExt = (
|
|
503
188
|
{ enclosure, link },
|
|
504
189
|
order = [AUDIO_ORDER_TYPES.enclosure, AUDIO_ORDER_TYPES.link]
|
|
505
190
|
) => {
|
|
@@ -522,7 +207,7 @@ const getEpisodeAudioUrlAndExt = (
|
|
|
522
207
|
return { url: null, ext: null };
|
|
523
208
|
};
|
|
524
209
|
|
|
525
|
-
const getImageUrl = ({ image, itunes }) => {
|
|
210
|
+
export const getImageUrl = ({ image, itunes }) => {
|
|
526
211
|
if (image?.url) {
|
|
527
212
|
return image.url;
|
|
528
213
|
}
|
|
@@ -549,7 +234,7 @@ export const TRANSCRIPT_TYPES = {
|
|
|
549
234
|
};
|
|
550
235
|
|
|
551
236
|
// @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#transcript
|
|
552
|
-
const getTranscriptUrl = (item, transcriptTypes = []) => {
|
|
237
|
+
export const getTranscriptUrl = (item, transcriptTypes = []) => {
|
|
553
238
|
if (!item.podcastTranscripts?.length) {
|
|
554
239
|
return null;
|
|
555
240
|
}
|
|
@@ -568,7 +253,7 @@ const getTranscriptUrl = (item, transcriptTypes = []) => {
|
|
|
568
253
|
return null;
|
|
569
254
|
};
|
|
570
255
|
|
|
571
|
-
const getFileFeed = async (filePath, parserConfig) => {
|
|
256
|
+
export const getFileFeed = async (filePath, parserConfig) => {
|
|
572
257
|
const config = parserConfig
|
|
573
258
|
? getJsonFile(parserConfig)
|
|
574
259
|
: defaultRssParserConfig;
|
|
@@ -590,7 +275,7 @@ const getFileFeed = async (filePath, parserConfig) => {
|
|
|
590
275
|
return feed;
|
|
591
276
|
};
|
|
592
277
|
|
|
593
|
-
const getUrlFeed = async (url, parserConfig) => {
|
|
278
|
+
export const getUrlFeed = async (url, parserConfig) => {
|
|
594
279
|
const config = parserConfig
|
|
595
280
|
? getJsonFile(parserConfig)
|
|
596
281
|
: defaultRssParserConfig;
|
|
@@ -612,132 +297,3 @@ const getUrlFeed = async (url, parserConfig) => {
|
|
|
612
297
|
|
|
613
298
|
return feed;
|
|
614
299
|
};
|
|
615
|
-
|
|
616
|
-
const runFfmpeg = async ({
|
|
617
|
-
feed,
|
|
618
|
-
item,
|
|
619
|
-
itemIndex,
|
|
620
|
-
outputPath,
|
|
621
|
-
bitrate,
|
|
622
|
-
mono,
|
|
623
|
-
addMp3Metadata,
|
|
624
|
-
ext,
|
|
625
|
-
}) => {
|
|
626
|
-
if (!fs.existsSync(outputPath)) {
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
let command = `ffmpeg -loglevel quiet -i "${outputPath}"`;
|
|
631
|
-
|
|
632
|
-
if (bitrate) {
|
|
633
|
-
command += ` -b:a ${bitrate}`;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (mono) {
|
|
637
|
-
command += " -ac 1";
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (addMp3Metadata) {
|
|
641
|
-
const album = feed.title || "";
|
|
642
|
-
const artist = item.itunes?.author || item.author || "";
|
|
643
|
-
const title = item.title || "";
|
|
644
|
-
const subtitle = item.itunes?.subtitle || "";
|
|
645
|
-
const comment = item.content || "";
|
|
646
|
-
const disc = item.itunes?.season || "";
|
|
647
|
-
const track = item.itunes?.episode || `${feed.items.length - itemIndex}`;
|
|
648
|
-
const episodeType = item.itunes?.episodeType || "";
|
|
649
|
-
const date = item.pubDate
|
|
650
|
-
? dayjs(new Date(item.pubDate)).format("YYYY-MM-DD")
|
|
651
|
-
: "";
|
|
652
|
-
|
|
653
|
-
const metaKeysToValues = {
|
|
654
|
-
album,
|
|
655
|
-
artist,
|
|
656
|
-
album_artist: artist,
|
|
657
|
-
title,
|
|
658
|
-
subtitle,
|
|
659
|
-
comment,
|
|
660
|
-
disc,
|
|
661
|
-
track,
|
|
662
|
-
"episode-type": episodeType,
|
|
663
|
-
date,
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
const metadataString = Object.keys(metaKeysToValues)
|
|
667
|
-
.map((key) => {
|
|
668
|
-
if (!metaKeysToValues[key]) {
|
|
669
|
-
return null;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const argValue = escapeArgForShell(metaKeysToValues[key]);
|
|
673
|
-
|
|
674
|
-
return argValue ? `-metadata ${key}=${argValue}` : null;
|
|
675
|
-
})
|
|
676
|
-
.filter((segment) => !!segment)
|
|
677
|
-
.join(" ");
|
|
678
|
-
|
|
679
|
-
command += ` -map_metadata 0 ${metadataString} -codec copy`;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const tmpMp3Path = `${outputPath}.tmp${ext}`;
|
|
683
|
-
command += ` "${tmpMp3Path}"`;
|
|
684
|
-
logMessage("Running command: " + command, LOG_LEVELS.debug);
|
|
685
|
-
|
|
686
|
-
try {
|
|
687
|
-
await execWithPromise(command, { stdio: "ignore" });
|
|
688
|
-
} catch (error) {
|
|
689
|
-
if (fs.existsSync(tmpMp3Path)) {
|
|
690
|
-
fs.unlinkSync(tmpMp3Path);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
throw error;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
fs.unlinkSync(outputPath);
|
|
697
|
-
fs.renameSync(tmpMp3Path, outputPath);
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
const runExec = async ({
|
|
701
|
-
exec,
|
|
702
|
-
basePath,
|
|
703
|
-
outputPodcastPath,
|
|
704
|
-
episodeFilename,
|
|
705
|
-
episodeAudioUrl,
|
|
706
|
-
}) => {
|
|
707
|
-
const episodeFilenameBase = episodeFilename.substring(
|
|
708
|
-
0,
|
|
709
|
-
episodeFilename.lastIndexOf(".")
|
|
710
|
-
);
|
|
711
|
-
|
|
712
|
-
const execCmd = exec
|
|
713
|
-
.replace(/{{episode_path}}/g, `"${outputPodcastPath}"`)
|
|
714
|
-
.replace(/{{episode_path_base}}/g, `"${basePath}"`)
|
|
715
|
-
.replace(/{{episode_filename}}/g, `"${episodeFilename}"`)
|
|
716
|
-
.replace(/{{episode_filename_base}}/g, `"${episodeFilenameBase}"`)
|
|
717
|
-
.replace(/{{url}}/g, `"${episodeAudioUrl}"`);
|
|
718
|
-
|
|
719
|
-
await execWithPromise(execCmd, { stdio: "ignore" });
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
export {
|
|
723
|
-
AUDIO_ORDER_TYPES,
|
|
724
|
-
getArchive,
|
|
725
|
-
getIsInArchive,
|
|
726
|
-
getArchiveKey,
|
|
727
|
-
writeToArchive,
|
|
728
|
-
getEpisodeAudioUrlAndExt,
|
|
729
|
-
getFileFeed,
|
|
730
|
-
getImageUrl,
|
|
731
|
-
getItemsToDownload,
|
|
732
|
-
getTempPath,
|
|
733
|
-
getUrlExt,
|
|
734
|
-
getUrlFeed,
|
|
735
|
-
logFeedInfo,
|
|
736
|
-
ITEM_LIST_FORMATS,
|
|
737
|
-
logItemsList,
|
|
738
|
-
prepareOutputPath,
|
|
739
|
-
writeFeedMeta,
|
|
740
|
-
writeItemMeta,
|
|
741
|
-
runFfmpeg,
|
|
742
|
-
runExec,
|
|
743
|
-
};
|
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"
|