podcast-dl 9.0.2 → 9.0.3

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