podcast-dl 11.1.1 → 11.2.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/LICENSE +21 -21
- package/README.md +121 -120
- package/bin/archive.js +39 -39
- package/bin/async.js +370 -370
- package/bin/bin.js +292 -289
- package/bin/commander.js +198 -193
- package/bin/exec.js +30 -30
- package/bin/ffmpeg.js +105 -105
- package/bin/items.js +247 -237
- package/bin/logger.js +84 -84
- package/bin/meta.js +66 -66
- package/bin/naming.js +112 -112
- package/bin/util.js +299 -299
- package/bin/validate.js +39 -39
- package/package.json +62 -62
package/bin/async.js
CHANGED
|
@@ -1,370 +1,370 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import got from "got";
|
|
3
|
-
import pLimit from "p-limit";
|
|
4
|
-
import _path from "path";
|
|
5
|
-
import stream from "stream";
|
|
6
|
-
import { throttle } from "throttle-debounce";
|
|
7
|
-
import { promisify } from "util";
|
|
8
|
-
import {
|
|
9
|
-
getArchiveFilename,
|
|
10
|
-
getArchiveKey,
|
|
11
|
-
getIsInArchive,
|
|
12
|
-
writeToArchive,
|
|
13
|
-
} from "./archive.js";
|
|
14
|
-
import { runExec } from "./exec.js";
|
|
15
|
-
import { runFfmpeg } from "./ffmpeg.js";
|
|
16
|
-
import {
|
|
17
|
-
LOG_LEVELS,
|
|
18
|
-
getLogMessageWithMarker,
|
|
19
|
-
getShouldOutputProgressIndicator,
|
|
20
|
-
logError,
|
|
21
|
-
} from "./logger.js";
|
|
22
|
-
import { writeItemMeta } from "./meta.js";
|
|
23
|
-
import { getItemFilename } from "./naming.js";
|
|
24
|
-
import {
|
|
25
|
-
getEpisodeAudioUrlAndExt,
|
|
26
|
-
getTempPath,
|
|
27
|
-
prepareOutputPath,
|
|
28
|
-
} from "./util.js";
|
|
29
|
-
|
|
30
|
-
const pipeline = promisify(stream.pipeline);
|
|
31
|
-
|
|
32
|
-
const BYTES_IN_MB = 1000000;
|
|
33
|
-
const USER_AGENT =
|
|
34
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
|
|
35
|
-
|
|
36
|
-
export const download = async (options) => {
|
|
37
|
-
const {
|
|
38
|
-
marker,
|
|
39
|
-
url,
|
|
40
|
-
outputPath,
|
|
41
|
-
key,
|
|
42
|
-
archive,
|
|
43
|
-
override,
|
|
44
|
-
alwaysPostprocess,
|
|
45
|
-
onAfterDownload,
|
|
46
|
-
attempt = 1,
|
|
47
|
-
maxAttempts = 3,
|
|
48
|
-
} = options;
|
|
49
|
-
|
|
50
|
-
const logMessage = getLogMessageWithMarker(marker);
|
|
51
|
-
if (!override && fs.existsSync(outputPath)) {
|
|
52
|
-
logMessage("Download exists locally. Skipping...");
|
|
53
|
-
|
|
54
|
-
if (onAfterDownload && alwaysPostprocess) {
|
|
55
|
-
await onAfterDownload();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (key && archive && getIsInArchive({ key, archive })) {
|
|
62
|
-
logMessage("Download exists in archive. Skipping...");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let headResponse = null;
|
|
67
|
-
try {
|
|
68
|
-
headResponse = await got(url, {
|
|
69
|
-
timeout: 30000,
|
|
70
|
-
method: "HEAD",
|
|
71
|
-
responseType: "json",
|
|
72
|
-
headers: {
|
|
73
|
-
accept: "*/*",
|
|
74
|
-
"user-agent": USER_AGENT,
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
} catch (error) {
|
|
78
|
-
// unable to retrieve head response
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const tempOutputPath = getTempPath(outputPath);
|
|
82
|
-
const removeFile = () => {
|
|
83
|
-
if (fs.existsSync(tempOutputPath)) {
|
|
84
|
-
fs.unlinkSync(tempOutputPath);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const expectedSize = headResponse?.headers?.["content-length"]
|
|
89
|
-
? parseInt(headResponse.headers["content-length"])
|
|
90
|
-
: 0;
|
|
91
|
-
|
|
92
|
-
logMessage(
|
|
93
|
-
`Starting download${
|
|
94
|
-
expectedSize
|
|
95
|
-
? ` of ${(expectedSize / BYTES_IN_MB).toFixed(2)} MB...`
|
|
96
|
-
: "..."
|
|
97
|
-
}`
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const onDownloadProgress = throttle(3000, (progress) => {
|
|
102
|
-
if (
|
|
103
|
-
getShouldOutputProgressIndicator() &&
|
|
104
|
-
progress.transferred > 0 &&
|
|
105
|
-
progress.percent < 1
|
|
106
|
-
) {
|
|
107
|
-
logMessage(
|
|
108
|
-
`${(progress.percent * 100).toFixed(0)}% of ${(
|
|
109
|
-
progress.total / BYTES_IN_MB
|
|
110
|
-
).toFixed(2)} MB...`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await pipeline(
|
|
116
|
-
got
|
|
117
|
-
.stream(url, { headers: { "user-agent": USER_AGENT } })
|
|
118
|
-
.on("downloadProgress", onDownloadProgress),
|
|
119
|
-
fs.createWriteStream(tempOutputPath)
|
|
120
|
-
);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
removeFile();
|
|
123
|
-
|
|
124
|
-
if (attempt <= maxAttempts) {
|
|
125
|
-
logMessage(`Download attempt #${attempt} failed. Retrying...`);
|
|
126
|
-
|
|
127
|
-
await download({
|
|
128
|
-
...options,
|
|
129
|
-
attempt: attempt + 1,
|
|
130
|
-
});
|
|
131
|
-
} else {
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const fileSize = fs.statSync(tempOutputPath).size;
|
|
137
|
-
|
|
138
|
-
if (fileSize === 0) {
|
|
139
|
-
removeFile();
|
|
140
|
-
|
|
141
|
-
logMessage(
|
|
142
|
-
"Unable to write to file. Suggestion: verify permissions",
|
|
143
|
-
LOG_LEVELS.important
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
fs.renameSync(tempOutputPath, outputPath);
|
|
150
|
-
|
|
151
|
-
logMessage("Download complete!");
|
|
152
|
-
|
|
153
|
-
if (onAfterDownload) {
|
|
154
|
-
await onAfterDownload();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (key && archive) {
|
|
158
|
-
try {
|
|
159
|
-
writeToArchive({ key, archive });
|
|
160
|
-
} catch (error) {
|
|
161
|
-
throw new Error(`Error writing to archive: ${error.toString()}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
export const downloadItemsAsync = async ({
|
|
167
|
-
addMp3MetadataFlag,
|
|
168
|
-
archive,
|
|
169
|
-
archivePrefix,
|
|
170
|
-
attempts,
|
|
171
|
-
basePath,
|
|
172
|
-
bitrate,
|
|
173
|
-
episodeTemplate,
|
|
174
|
-
episodeCustomTemplateOptions,
|
|
175
|
-
episodeDigits,
|
|
176
|
-
episodeNumOffset,
|
|
177
|
-
episodeSourceOrder,
|
|
178
|
-
exec,
|
|
179
|
-
feed,
|
|
180
|
-
includeEpisodeImages,
|
|
181
|
-
includeEpisodeMeta,
|
|
182
|
-
mono,
|
|
183
|
-
override,
|
|
184
|
-
alwaysPostprocess,
|
|
185
|
-
targetItems,
|
|
186
|
-
threads = 1,
|
|
187
|
-
}) => {
|
|
188
|
-
let numEpisodesDownloaded = 0;
|
|
189
|
-
let hasErrors = false;
|
|
190
|
-
|
|
191
|
-
const limit = pLimit(threads);
|
|
192
|
-
const downloadItem = async (item, index) => {
|
|
193
|
-
const threadIndex = index % threads;
|
|
194
|
-
const marker = threads > 1 ? `[${threadIndex}] ${item.title}` : item.title;
|
|
195
|
-
const logMessage = getLogMessageWithMarker(marker);
|
|
196
|
-
const { url: episodeAudioUrl, ext: audioFileExt } =
|
|
197
|
-
getEpisodeAudioUrlAndExt(item, episodeSourceOrder);
|
|
198
|
-
|
|
199
|
-
if (!episodeAudioUrl) {
|
|
200
|
-
hasErrors = true;
|
|
201
|
-
logError(`${marker} | Unable to find episode download URL`);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const episodeFilename = getItemFilename({
|
|
206
|
-
item,
|
|
207
|
-
feed,
|
|
208
|
-
url: episodeAudioUrl,
|
|
209
|
-
ext: audioFileExt,
|
|
210
|
-
template: episodeTemplate,
|
|
211
|
-
customTemplateOptions: episodeCustomTemplateOptions,
|
|
212
|
-
width: episodeDigits,
|
|
213
|
-
offset: episodeNumOffset,
|
|
214
|
-
});
|
|
215
|
-
const outputPodcastPath = _path.resolve(basePath, episodeFilename);
|
|
216
|
-
|
|
217
|
-
prepareOutputPath(outputPodcastPath);
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
await download({
|
|
221
|
-
archive,
|
|
222
|
-
override,
|
|
223
|
-
alwaysPostprocess,
|
|
224
|
-
marker,
|
|
225
|
-
key: getArchiveKey({
|
|
226
|
-
prefix: archivePrefix,
|
|
227
|
-
name: getArchiveFilename({
|
|
228
|
-
name: item.title,
|
|
229
|
-
pubDate: item.pubDate,
|
|
230
|
-
ext: audioFileExt,
|
|
231
|
-
}),
|
|
232
|
-
}),
|
|
233
|
-
maxAttempts: attempts,
|
|
234
|
-
outputPath: outputPodcastPath,
|
|
235
|
-
url: episodeAudioUrl,
|
|
236
|
-
onAfterDownload: async () => {
|
|
237
|
-
if (item._episodeImage) {
|
|
238
|
-
try {
|
|
239
|
-
await download({
|
|
240
|
-
archive,
|
|
241
|
-
override,
|
|
242
|
-
key: item._episodeImage.key,
|
|
243
|
-
marker: item._episodeImage.url,
|
|
244
|
-
maxAttempts: attempts,
|
|
245
|
-
outputPath: item._episodeImage.outputPath,
|
|
246
|
-
url: item._episodeImage.url,
|
|
247
|
-
});
|
|
248
|
-
} catch (error) {
|
|
249
|
-
hasErrors = true;
|
|
250
|
-
logError(
|
|
251
|
-
`${marker} | Error downloading ${
|
|
252
|
-
item._episodeImage.url
|
|
253
|
-
}: ${error.toString()}`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (item._episodeTranscript) {
|
|
259
|
-
try {
|
|
260
|
-
await download({
|
|
261
|
-
archive,
|
|
262
|
-
override,
|
|
263
|
-
key: item._episodeTranscript.key,
|
|
264
|
-
marker: item._episodeTranscript.url,
|
|
265
|
-
maxAttempts: attempts,
|
|
266
|
-
outputPath: item._episodeTranscript.outputPath,
|
|
267
|
-
url: item._episodeTranscript.url,
|
|
268
|
-
});
|
|
269
|
-
} catch (error) {
|
|
270
|
-
hasErrors = true;
|
|
271
|
-
logError(
|
|
272
|
-
`${marker} | Error downloading ${
|
|
273
|
-
item._episodeTranscript.url
|
|
274
|
-
}: ${error.toString()}`
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const hasEpisodeImage =
|
|
280
|
-
item._episodeImage && fs.existsSync(item._episodeImage.outputPath);
|
|
281
|
-
|
|
282
|
-
if (addMp3MetadataFlag || bitrate || mono) {
|
|
283
|
-
logMessage("Running ffmpeg...");
|
|
284
|
-
await runFfmpeg({
|
|
285
|
-
feed,
|
|
286
|
-
item,
|
|
287
|
-
bitrate,
|
|
288
|
-
mono,
|
|
289
|
-
itemIndex: item._originalIndex,
|
|
290
|
-
outputPath: outputPodcastPath,
|
|
291
|
-
episodeImageOutputPath: hasEpisodeImage
|
|
292
|
-
? item._episodeImage.outputPath
|
|
293
|
-
: undefined,
|
|
294
|
-
addMp3Metadata: addMp3MetadataFlag,
|
|
295
|
-
ext: audioFileExt,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (!includeEpisodeImages && hasEpisodeImage) {
|
|
300
|
-
fs.unlinkSync(item._episodeImage.outputPath);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (exec) {
|
|
304
|
-
logMessage("Running exec...");
|
|
305
|
-
await runExec({
|
|
306
|
-
exec,
|
|
307
|
-
basePath,
|
|
308
|
-
outputPodcastPath,
|
|
309
|
-
episodeFilename,
|
|
310
|
-
episodeAudioUrl,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (includeEpisodeMeta) {
|
|
315
|
-
const episodeMetaExt = ".meta.json";
|
|
316
|
-
const episodeMetaName = getItemFilename({
|
|
317
|
-
item,
|
|
318
|
-
feed,
|
|
319
|
-
url: episodeAudioUrl,
|
|
320
|
-
ext: episodeMetaExt,
|
|
321
|
-
template: episodeTemplate,
|
|
322
|
-
customTemplateOptions: episodeCustomTemplateOptions,
|
|
323
|
-
width: episodeDigits,
|
|
324
|
-
offset: episodeNumOffset,
|
|
325
|
-
});
|
|
326
|
-
const outputEpisodeMetaPath = _path.resolve(
|
|
327
|
-
basePath,
|
|
328
|
-
episodeMetaName
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
logMessage("Saving episode metadata...");
|
|
333
|
-
writeItemMeta({
|
|
334
|
-
marker,
|
|
335
|
-
archive,
|
|
336
|
-
override,
|
|
337
|
-
item,
|
|
338
|
-
key: getArchiveKey({
|
|
339
|
-
prefix: archivePrefix,
|
|
340
|
-
name: getArchiveFilename({
|
|
341
|
-
pubDate: item.pubDate,
|
|
342
|
-
name: item.title,
|
|
343
|
-
ext: episodeMetaExt,
|
|
344
|
-
}),
|
|
345
|
-
}),
|
|
346
|
-
outputPath: outputEpisodeMetaPath,
|
|
347
|
-
});
|
|
348
|
-
} catch (error) {
|
|
349
|
-
hasErrors = true;
|
|
350
|
-
logError(`${marker} | ${error.toString()}`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
numEpisodesDownloaded += 1;
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
} catch (error) {
|
|
358
|
-
hasErrors = true;
|
|
359
|
-
logError(`${marker} | Error downloading episode: ${error.toString()}`);
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
const itemPromises = targetItems.map((item, index) =>
|
|
364
|
-
limit(() => downloadItem(item, index))
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
await Promise.all(itemPromises);
|
|
368
|
-
|
|
369
|
-
return { numEpisodesDownloaded, hasErrors };
|
|
370
|
-
};
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import got from "got";
|
|
3
|
+
import pLimit from "p-limit";
|
|
4
|
+
import _path from "path";
|
|
5
|
+
import stream from "stream";
|
|
6
|
+
import { throttle } from "throttle-debounce";
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
import {
|
|
9
|
+
getArchiveFilename,
|
|
10
|
+
getArchiveKey,
|
|
11
|
+
getIsInArchive,
|
|
12
|
+
writeToArchive,
|
|
13
|
+
} from "./archive.js";
|
|
14
|
+
import { runExec } from "./exec.js";
|
|
15
|
+
import { runFfmpeg } from "./ffmpeg.js";
|
|
16
|
+
import {
|
|
17
|
+
LOG_LEVELS,
|
|
18
|
+
getLogMessageWithMarker,
|
|
19
|
+
getShouldOutputProgressIndicator,
|
|
20
|
+
logError,
|
|
21
|
+
} from "./logger.js";
|
|
22
|
+
import { writeItemMeta } from "./meta.js";
|
|
23
|
+
import { getItemFilename } from "./naming.js";
|
|
24
|
+
import {
|
|
25
|
+
getEpisodeAudioUrlAndExt,
|
|
26
|
+
getTempPath,
|
|
27
|
+
prepareOutputPath,
|
|
28
|
+
} from "./util.js";
|
|
29
|
+
|
|
30
|
+
const pipeline = promisify(stream.pipeline);
|
|
31
|
+
|
|
32
|
+
const BYTES_IN_MB = 1000000;
|
|
33
|
+
const USER_AGENT =
|
|
34
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
|
|
35
|
+
|
|
36
|
+
export const download = async (options) => {
|
|
37
|
+
const {
|
|
38
|
+
marker,
|
|
39
|
+
url,
|
|
40
|
+
outputPath,
|
|
41
|
+
key,
|
|
42
|
+
archive,
|
|
43
|
+
override,
|
|
44
|
+
alwaysPostprocess,
|
|
45
|
+
onAfterDownload,
|
|
46
|
+
attempt = 1,
|
|
47
|
+
maxAttempts = 3,
|
|
48
|
+
} = options;
|
|
49
|
+
|
|
50
|
+
const logMessage = getLogMessageWithMarker(marker);
|
|
51
|
+
if (!override && fs.existsSync(outputPath)) {
|
|
52
|
+
logMessage("Download exists locally. Skipping...");
|
|
53
|
+
|
|
54
|
+
if (onAfterDownload && alwaysPostprocess) {
|
|
55
|
+
await onAfterDownload();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (key && archive && getIsInArchive({ key, archive })) {
|
|
62
|
+
logMessage("Download exists in archive. Skipping...");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let headResponse = null;
|
|
67
|
+
try {
|
|
68
|
+
headResponse = await got(url, {
|
|
69
|
+
timeout: 30000,
|
|
70
|
+
method: "HEAD",
|
|
71
|
+
responseType: "json",
|
|
72
|
+
headers: {
|
|
73
|
+
accept: "*/*",
|
|
74
|
+
"user-agent": USER_AGENT,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// unable to retrieve head response
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tempOutputPath = getTempPath(outputPath);
|
|
82
|
+
const removeFile = () => {
|
|
83
|
+
if (fs.existsSync(tempOutputPath)) {
|
|
84
|
+
fs.unlinkSync(tempOutputPath);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const expectedSize = headResponse?.headers?.["content-length"]
|
|
89
|
+
? parseInt(headResponse.headers["content-length"])
|
|
90
|
+
: 0;
|
|
91
|
+
|
|
92
|
+
logMessage(
|
|
93
|
+
`Starting download${
|
|
94
|
+
expectedSize
|
|
95
|
+
? ` of ${(expectedSize / BYTES_IN_MB).toFixed(2)} MB...`
|
|
96
|
+
: "..."
|
|
97
|
+
}`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const onDownloadProgress = throttle(3000, (progress) => {
|
|
102
|
+
if (
|
|
103
|
+
getShouldOutputProgressIndicator() &&
|
|
104
|
+
progress.transferred > 0 &&
|
|
105
|
+
progress.percent < 1
|
|
106
|
+
) {
|
|
107
|
+
logMessage(
|
|
108
|
+
`${(progress.percent * 100).toFixed(0)}% of ${(
|
|
109
|
+
progress.total / BYTES_IN_MB
|
|
110
|
+
).toFixed(2)} MB...`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await pipeline(
|
|
116
|
+
got
|
|
117
|
+
.stream(url, { headers: { "user-agent": USER_AGENT } })
|
|
118
|
+
.on("downloadProgress", onDownloadProgress),
|
|
119
|
+
fs.createWriteStream(tempOutputPath)
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
removeFile();
|
|
123
|
+
|
|
124
|
+
if (attempt <= maxAttempts) {
|
|
125
|
+
logMessage(`Download attempt #${attempt} failed. Retrying...`);
|
|
126
|
+
|
|
127
|
+
await download({
|
|
128
|
+
...options,
|
|
129
|
+
attempt: attempt + 1,
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fileSize = fs.statSync(tempOutputPath).size;
|
|
137
|
+
|
|
138
|
+
if (fileSize === 0) {
|
|
139
|
+
removeFile();
|
|
140
|
+
|
|
141
|
+
logMessage(
|
|
142
|
+
"Unable to write to file. Suggestion: verify permissions",
|
|
143
|
+
LOG_LEVELS.important
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fs.renameSync(tempOutputPath, outputPath);
|
|
150
|
+
|
|
151
|
+
logMessage("Download complete!");
|
|
152
|
+
|
|
153
|
+
if (onAfterDownload) {
|
|
154
|
+
await onAfterDownload();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (key && archive) {
|
|
158
|
+
try {
|
|
159
|
+
writeToArchive({ key, archive });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error(`Error writing to archive: ${error.toString()}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const downloadItemsAsync = async ({
|
|
167
|
+
addMp3MetadataFlag,
|
|
168
|
+
archive,
|
|
169
|
+
archivePrefix,
|
|
170
|
+
attempts,
|
|
171
|
+
basePath,
|
|
172
|
+
bitrate,
|
|
173
|
+
episodeTemplate,
|
|
174
|
+
episodeCustomTemplateOptions,
|
|
175
|
+
episodeDigits,
|
|
176
|
+
episodeNumOffset,
|
|
177
|
+
episodeSourceOrder,
|
|
178
|
+
exec,
|
|
179
|
+
feed,
|
|
180
|
+
includeEpisodeImages,
|
|
181
|
+
includeEpisodeMeta,
|
|
182
|
+
mono,
|
|
183
|
+
override,
|
|
184
|
+
alwaysPostprocess,
|
|
185
|
+
targetItems,
|
|
186
|
+
threads = 1,
|
|
187
|
+
}) => {
|
|
188
|
+
let numEpisodesDownloaded = 0;
|
|
189
|
+
let hasErrors = false;
|
|
190
|
+
|
|
191
|
+
const limit = pLimit(threads);
|
|
192
|
+
const downloadItem = async (item, index) => {
|
|
193
|
+
const threadIndex = index % threads;
|
|
194
|
+
const marker = threads > 1 ? `[${threadIndex}] ${item.title}` : item.title;
|
|
195
|
+
const logMessage = getLogMessageWithMarker(marker);
|
|
196
|
+
const { url: episodeAudioUrl, ext: audioFileExt } =
|
|
197
|
+
getEpisodeAudioUrlAndExt(item, episodeSourceOrder);
|
|
198
|
+
|
|
199
|
+
if (!episodeAudioUrl) {
|
|
200
|
+
hasErrors = true;
|
|
201
|
+
logError(`${marker} | Unable to find episode download URL`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const episodeFilename = getItemFilename({
|
|
206
|
+
item,
|
|
207
|
+
feed,
|
|
208
|
+
url: episodeAudioUrl,
|
|
209
|
+
ext: audioFileExt,
|
|
210
|
+
template: episodeTemplate,
|
|
211
|
+
customTemplateOptions: episodeCustomTemplateOptions,
|
|
212
|
+
width: episodeDigits,
|
|
213
|
+
offset: episodeNumOffset,
|
|
214
|
+
});
|
|
215
|
+
const outputPodcastPath = _path.resolve(basePath, episodeFilename);
|
|
216
|
+
|
|
217
|
+
prepareOutputPath(outputPodcastPath);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await download({
|
|
221
|
+
archive,
|
|
222
|
+
override,
|
|
223
|
+
alwaysPostprocess,
|
|
224
|
+
marker,
|
|
225
|
+
key: getArchiveKey({
|
|
226
|
+
prefix: archivePrefix,
|
|
227
|
+
name: getArchiveFilename({
|
|
228
|
+
name: item.title,
|
|
229
|
+
pubDate: item.pubDate,
|
|
230
|
+
ext: audioFileExt,
|
|
231
|
+
}),
|
|
232
|
+
}),
|
|
233
|
+
maxAttempts: attempts,
|
|
234
|
+
outputPath: outputPodcastPath,
|
|
235
|
+
url: episodeAudioUrl,
|
|
236
|
+
onAfterDownload: async () => {
|
|
237
|
+
if (item._episodeImage) {
|
|
238
|
+
try {
|
|
239
|
+
await download({
|
|
240
|
+
archive,
|
|
241
|
+
override,
|
|
242
|
+
key: item._episodeImage.key,
|
|
243
|
+
marker: item._episodeImage.url,
|
|
244
|
+
maxAttempts: attempts,
|
|
245
|
+
outputPath: item._episodeImage.outputPath,
|
|
246
|
+
url: item._episodeImage.url,
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
hasErrors = true;
|
|
250
|
+
logError(
|
|
251
|
+
`${marker} | Error downloading ${
|
|
252
|
+
item._episodeImage.url
|
|
253
|
+
}: ${error.toString()}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (item._episodeTranscript) {
|
|
259
|
+
try {
|
|
260
|
+
await download({
|
|
261
|
+
archive,
|
|
262
|
+
override,
|
|
263
|
+
key: item._episodeTranscript.key,
|
|
264
|
+
marker: item._episodeTranscript.url,
|
|
265
|
+
maxAttempts: attempts,
|
|
266
|
+
outputPath: item._episodeTranscript.outputPath,
|
|
267
|
+
url: item._episodeTranscript.url,
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
hasErrors = true;
|
|
271
|
+
logError(
|
|
272
|
+
`${marker} | Error downloading ${
|
|
273
|
+
item._episodeTranscript.url
|
|
274
|
+
}: ${error.toString()}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const hasEpisodeImage =
|
|
280
|
+
item._episodeImage && fs.existsSync(item._episodeImage.outputPath);
|
|
281
|
+
|
|
282
|
+
if (addMp3MetadataFlag || bitrate || mono) {
|
|
283
|
+
logMessage("Running ffmpeg...");
|
|
284
|
+
await runFfmpeg({
|
|
285
|
+
feed,
|
|
286
|
+
item,
|
|
287
|
+
bitrate,
|
|
288
|
+
mono,
|
|
289
|
+
itemIndex: item._originalIndex,
|
|
290
|
+
outputPath: outputPodcastPath,
|
|
291
|
+
episodeImageOutputPath: hasEpisodeImage
|
|
292
|
+
? item._episodeImage.outputPath
|
|
293
|
+
: undefined,
|
|
294
|
+
addMp3Metadata: addMp3MetadataFlag,
|
|
295
|
+
ext: audioFileExt,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!includeEpisodeImages && hasEpisodeImage) {
|
|
300
|
+
fs.unlinkSync(item._episodeImage.outputPath);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (exec) {
|
|
304
|
+
logMessage("Running exec...");
|
|
305
|
+
await runExec({
|
|
306
|
+
exec,
|
|
307
|
+
basePath,
|
|
308
|
+
outputPodcastPath,
|
|
309
|
+
episodeFilename,
|
|
310
|
+
episodeAudioUrl,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (includeEpisodeMeta) {
|
|
315
|
+
const episodeMetaExt = ".meta.json";
|
|
316
|
+
const episodeMetaName = getItemFilename({
|
|
317
|
+
item,
|
|
318
|
+
feed,
|
|
319
|
+
url: episodeAudioUrl,
|
|
320
|
+
ext: episodeMetaExt,
|
|
321
|
+
template: episodeTemplate,
|
|
322
|
+
customTemplateOptions: episodeCustomTemplateOptions,
|
|
323
|
+
width: episodeDigits,
|
|
324
|
+
offset: episodeNumOffset,
|
|
325
|
+
});
|
|
326
|
+
const outputEpisodeMetaPath = _path.resolve(
|
|
327
|
+
basePath,
|
|
328
|
+
episodeMetaName
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
logMessage("Saving episode metadata...");
|
|
333
|
+
writeItemMeta({
|
|
334
|
+
marker,
|
|
335
|
+
archive,
|
|
336
|
+
override,
|
|
337
|
+
item,
|
|
338
|
+
key: getArchiveKey({
|
|
339
|
+
prefix: archivePrefix,
|
|
340
|
+
name: getArchiveFilename({
|
|
341
|
+
pubDate: item.pubDate,
|
|
342
|
+
name: item.title,
|
|
343
|
+
ext: episodeMetaExt,
|
|
344
|
+
}),
|
|
345
|
+
}),
|
|
346
|
+
outputPath: outputEpisodeMetaPath,
|
|
347
|
+
});
|
|
348
|
+
} catch (error) {
|
|
349
|
+
hasErrors = true;
|
|
350
|
+
logError(`${marker} | ${error.toString()}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
numEpisodesDownloaded += 1;
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
hasErrors = true;
|
|
359
|
+
logError(`${marker} | Error downloading episode: ${error.toString()}`);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const itemPromises = targetItems.map((item, index) =>
|
|
364
|
+
limit(() => downloadItem(item, index))
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
await Promise.all(itemPromises);
|
|
368
|
+
|
|
369
|
+
return { numEpisodesDownloaded, hasErrors };
|
|
370
|
+
};
|