podcast-dl 9.0.3 → 9.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 +103 -102
- package/bin/async.js +316 -304
- package/bin/bin.js +261 -259
- package/bin/commander.js +8 -4
- package/bin/logger.js +92 -92
- package/bin/naming.js +96 -85
- package/bin/util.js +577 -564
- package/bin/validate.js +43 -43
- package/package.json +62 -62
package/bin/bin.js
CHANGED
|
@@ -1,259 +1,261 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import _path from "path";
|
|
5
|
-
import commander from "commander";
|
|
6
|
-
import pluralize from "pluralize";
|
|
7
|
-
import { bootstrap as bootstrapProxy } from "global-agent";
|
|
8
|
-
|
|
9
|
-
import { setupCommander } from "./commander.js";
|
|
10
|
-
import { download } from "./async.js";
|
|
11
|
-
import {
|
|
12
|
-
getArchiveKey,
|
|
13
|
-
getFeed,
|
|
14
|
-
getImageUrl,
|
|
15
|
-
getItemsToDownload,
|
|
16
|
-
getUrlExt,
|
|
17
|
-
logFeedInfo,
|
|
18
|
-
logItemsList,
|
|
19
|
-
writeFeedMeta,
|
|
20
|
-
} from "./util.js";
|
|
21
|
-
import {
|
|
22
|
-
ERROR_STATUSES,
|
|
23
|
-
LOG_LEVELS,
|
|
24
|
-
logMessage,
|
|
25
|
-
logError,
|
|
26
|
-
logErrorAndExit,
|
|
27
|
-
} from "./logger.js";
|
|
28
|
-
import { getFolderName, getSimpleFilename } from "./naming.js";
|
|
29
|
-
import { downloadItemsAsync } from "./async.js";
|
|
30
|
-
|
|
31
|
-
setupCommander(commander, process.argv);
|
|
32
|
-
|
|
33
|
-
const {
|
|
34
|
-
url,
|
|
35
|
-
outDir,
|
|
36
|
-
episodeTemplate,
|
|
37
|
-
episodeDigits,
|
|
38
|
-
episodeSourceOrder,
|
|
39
|
-
includeMeta,
|
|
40
|
-
includeEpisodeMeta,
|
|
41
|
-
includeEpisodeImages,
|
|
42
|
-
offset,
|
|
43
|
-
limit,
|
|
44
|
-
episodeRegex,
|
|
45
|
-
after,
|
|
46
|
-
before,
|
|
47
|
-
override,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
feed.title ?
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
feed.title ?
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import _path from "path";
|
|
5
|
+
import commander from "commander";
|
|
6
|
+
import pluralize from "pluralize";
|
|
7
|
+
import { bootstrap as bootstrapProxy } from "global-agent";
|
|
8
|
+
|
|
9
|
+
import { setupCommander } from "./commander.js";
|
|
10
|
+
import { download } from "./async.js";
|
|
11
|
+
import {
|
|
12
|
+
getArchiveKey,
|
|
13
|
+
getFeed,
|
|
14
|
+
getImageUrl,
|
|
15
|
+
getItemsToDownload,
|
|
16
|
+
getUrlExt,
|
|
17
|
+
logFeedInfo,
|
|
18
|
+
logItemsList,
|
|
19
|
+
writeFeedMeta,
|
|
20
|
+
} from "./util.js";
|
|
21
|
+
import {
|
|
22
|
+
ERROR_STATUSES,
|
|
23
|
+
LOG_LEVELS,
|
|
24
|
+
logMessage,
|
|
25
|
+
logError,
|
|
26
|
+
logErrorAndExit,
|
|
27
|
+
} from "./logger.js";
|
|
28
|
+
import { getFolderName, getSimpleFilename } from "./naming.js";
|
|
29
|
+
import { downloadItemsAsync } from "./async.js";
|
|
30
|
+
|
|
31
|
+
setupCommander(commander, process.argv);
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
url,
|
|
35
|
+
outDir,
|
|
36
|
+
episodeTemplate,
|
|
37
|
+
episodeDigits,
|
|
38
|
+
episodeSourceOrder,
|
|
39
|
+
includeMeta,
|
|
40
|
+
includeEpisodeMeta,
|
|
41
|
+
includeEpisodeImages,
|
|
42
|
+
offset,
|
|
43
|
+
limit,
|
|
44
|
+
episodeRegex,
|
|
45
|
+
after,
|
|
46
|
+
before,
|
|
47
|
+
override,
|
|
48
|
+
alwaysPostprocess,
|
|
49
|
+
reverse,
|
|
50
|
+
info,
|
|
51
|
+
list,
|
|
52
|
+
exec,
|
|
53
|
+
mono,
|
|
54
|
+
threads,
|
|
55
|
+
attempts,
|
|
56
|
+
parserConfig,
|
|
57
|
+
proxy,
|
|
58
|
+
addMp3Metadata: addMp3MetadataFlag,
|
|
59
|
+
adjustBitrate: bitrate,
|
|
60
|
+
} = commander;
|
|
61
|
+
|
|
62
|
+
let { archive } = commander;
|
|
63
|
+
|
|
64
|
+
const main = async () => {
|
|
65
|
+
if (!url) {
|
|
66
|
+
logErrorAndExit("No URL provided");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (proxy) {
|
|
70
|
+
bootstrapProxy();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { hostname, pathname } = new URL(url);
|
|
74
|
+
const archiveUrl = `${hostname}${pathname}`;
|
|
75
|
+
const feed = await getFeed(url, parserConfig);
|
|
76
|
+
const basePath = _path.resolve(
|
|
77
|
+
process.cwd(),
|
|
78
|
+
getFolderName({ feed, template: outDir })
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (info) {
|
|
82
|
+
logFeedInfo(feed);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (list) {
|
|
86
|
+
if (feed?.items?.length) {
|
|
87
|
+
const listFormat = typeof list === "boolean" ? "table" : list;
|
|
88
|
+
logItemsList({
|
|
89
|
+
type: listFormat,
|
|
90
|
+
feed,
|
|
91
|
+
limit,
|
|
92
|
+
offset,
|
|
93
|
+
reverse,
|
|
94
|
+
after,
|
|
95
|
+
before,
|
|
96
|
+
episodeRegex,
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
logErrorAndExit("No episodes found to list");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (info || list) {
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logFeedInfo(feed);
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(basePath)) {
|
|
110
|
+
logMessage(`${basePath} does not exist. Creating...`, LOG_LEVELS.important);
|
|
111
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (archive) {
|
|
115
|
+
archive =
|
|
116
|
+
typeof archive === "boolean"
|
|
117
|
+
? "./{{podcast_title}}/archive.json"
|
|
118
|
+
: archive;
|
|
119
|
+
archive = getFolderName({ feed, template: archive });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (includeMeta) {
|
|
123
|
+
const podcastImageUrl = getImageUrl(feed);
|
|
124
|
+
|
|
125
|
+
if (podcastImageUrl) {
|
|
126
|
+
const podcastImageFileExt = getUrlExt(podcastImageUrl);
|
|
127
|
+
const outputImagePath = _path.resolve(
|
|
128
|
+
basePath,
|
|
129
|
+
getSimpleFilename(
|
|
130
|
+
feed.title ? feed.title : "image",
|
|
131
|
+
feed.title ? `.image${podcastImageFileExt}` : podcastImageFileExt
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
logMessage("\nDownloading podcast image...");
|
|
137
|
+
await download({
|
|
138
|
+
archive,
|
|
139
|
+
override,
|
|
140
|
+
marker: podcastImageUrl,
|
|
141
|
+
key: getArchiveKey({
|
|
142
|
+
prefix: archiveUrl,
|
|
143
|
+
name: `${
|
|
144
|
+
feed.title ? `${feed.title}.image` : "image"
|
|
145
|
+
}${podcastImageFileExt}`,
|
|
146
|
+
}),
|
|
147
|
+
outputPath: outputImagePath,
|
|
148
|
+
url: podcastImageUrl,
|
|
149
|
+
maxAttempts: attempts,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logError("Unable to download podcast image", error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const outputMetaPath = _path.resolve(
|
|
157
|
+
basePath,
|
|
158
|
+
getSimpleFilename(
|
|
159
|
+
feed.title ? feed.title : "meta",
|
|
160
|
+
feed.title ? ".meta.json" : ".json"
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
logMessage("\nSaving podcast metadata...");
|
|
166
|
+
writeFeedMeta({
|
|
167
|
+
archive,
|
|
168
|
+
override,
|
|
169
|
+
feed,
|
|
170
|
+
key: getArchiveKey({
|
|
171
|
+
prefix: archiveUrl,
|
|
172
|
+
name: `${feed.title ? `${feed.title}.meta` : "meta"}.json`,
|
|
173
|
+
}),
|
|
174
|
+
outputPath: outputMetaPath,
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logError("Unable to save podcast metadata", error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!feed.items || feed.items.length === 0) {
|
|
182
|
+
logErrorAndExit("No episodes found to download");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (offset >= feed.items.length) {
|
|
186
|
+
logErrorAndExit("--offset too large. No episodes to download.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const targetItems = getItemsToDownload({
|
|
190
|
+
archive,
|
|
191
|
+
archiveUrl,
|
|
192
|
+
basePath,
|
|
193
|
+
feed,
|
|
194
|
+
limit,
|
|
195
|
+
offset,
|
|
196
|
+
reverse,
|
|
197
|
+
after,
|
|
198
|
+
before,
|
|
199
|
+
episodeDigits,
|
|
200
|
+
episodeRegex,
|
|
201
|
+
episodeSourceOrder,
|
|
202
|
+
episodeTemplate,
|
|
203
|
+
includeEpisodeImages,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!targetItems.length) {
|
|
207
|
+
logErrorAndExit("No episodes found with provided criteria to download");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
logMessage(
|
|
211
|
+
`\nStarting download of ${pluralize("episode", targetItems.length, true)}\n`
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const { numEpisodesDownloaded, hasErrors } = await downloadItemsAsync({
|
|
215
|
+
addMp3MetadataFlag,
|
|
216
|
+
archive,
|
|
217
|
+
archiveUrl,
|
|
218
|
+
attempts,
|
|
219
|
+
basePath,
|
|
220
|
+
bitrate,
|
|
221
|
+
episodeTemplate,
|
|
222
|
+
episodeDigits,
|
|
223
|
+
episodeSourceOrder,
|
|
224
|
+
exec,
|
|
225
|
+
feed,
|
|
226
|
+
includeEpisodeMeta,
|
|
227
|
+
mono,
|
|
228
|
+
override,
|
|
229
|
+
alwaysPostprocess,
|
|
230
|
+
targetItems,
|
|
231
|
+
threads,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (hasErrors && numEpisodesDownloaded !== targetItems.length) {
|
|
235
|
+
logMessage(
|
|
236
|
+
`\n${numEpisodesDownloaded} of ${pluralize(
|
|
237
|
+
"episode",
|
|
238
|
+
targetItems.length,
|
|
239
|
+
true
|
|
240
|
+
)} downloaded\n`
|
|
241
|
+
);
|
|
242
|
+
} else if (numEpisodesDownloaded > 0) {
|
|
243
|
+
logMessage(
|
|
244
|
+
`\nSuccessfully downloaded ${pluralize(
|
|
245
|
+
"episode",
|
|
246
|
+
numEpisodesDownloaded,
|
|
247
|
+
true
|
|
248
|
+
)}\n`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (numEpisodesDownloaded === 0) {
|
|
253
|
+
process.exit(ERROR_STATUSES.nothingDownloaded);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (hasErrors) {
|
|
257
|
+
process.exit(ERROR_STATUSES.completedWithErrors);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
main();
|
package/bin/commander.js
CHANGED
|
@@ -4,7 +4,7 @@ import { logErrorAndExit } from "./logger.js";
|
|
|
4
4
|
|
|
5
5
|
export const setupCommander = (commander, argv) => {
|
|
6
6
|
commander
|
|
7
|
-
.version("9.0
|
|
7
|
+
.version("9.2.0")
|
|
8
8
|
.option("--url <string>", "url to podcast rss feed")
|
|
9
9
|
.option(
|
|
10
10
|
"--out-dir <path>",
|
|
@@ -74,20 +74,24 @@ export const setupCommander = (commander, argv) => {
|
|
|
74
74
|
)
|
|
75
75
|
.option(
|
|
76
76
|
"--add-mp3-metadata",
|
|
77
|
-
"attempts to add a base level of metadata to
|
|
77
|
+
"attempts to add a base level of metadata to episode files using ffmpeg",
|
|
78
78
|
hasFfmpeg
|
|
79
79
|
)
|
|
80
80
|
.option(
|
|
81
81
|
"--adjust-bitrate <string>",
|
|
82
|
-
"attempts to adjust bitrate of
|
|
82
|
+
"attempts to adjust bitrate of episode files using ffmpeg",
|
|
83
83
|
hasFfmpeg
|
|
84
84
|
)
|
|
85
85
|
.option(
|
|
86
86
|
"--mono",
|
|
87
|
-
"attempts to force
|
|
87
|
+
"attempts to force episode files into mono using ffmpeg",
|
|
88
88
|
hasFfmpeg
|
|
89
89
|
)
|
|
90
90
|
.option("--override", "override local files on collision")
|
|
91
|
+
.option(
|
|
92
|
+
"--always-postprocess",
|
|
93
|
+
"always run additional tasks on the file regardless if the file already exists"
|
|
94
|
+
)
|
|
91
95
|
.option("--reverse", "download episodes in reverse order")
|
|
92
96
|
.option("--info", "print retrieved podcast info instead of downloading")
|
|
93
97
|
.option(
|