podcast-dl 11.4.0 → 11.5.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 +1 -0
- package/bin/archive.js +39 -39
- package/bin/async.js +43 -14
- package/bin/bin.js +5 -7
- package/bin/commander.js +2 -1
- package/bin/exec.js +30 -30
- package/bin/ffmpeg.js +105 -105
- package/bin/items.js +247 -247
- package/bin/logger.js +84 -84
- package/bin/meta.js +66 -66
- package/bin/naming.js +136 -136
- package/bin/util.js +112 -1
- package/bin/validate.js +3 -3
- package/package.json +63 -62
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020 Joshua Pohl
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Joshua Pohl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ Type values surrounded in square brackets (`[]`) can be used as boolean options
|
|
|
61
61
|
| --parser-config | String | false | Path to JSON file that will be parsed and used to override the default config passed to [rss-parser](https://github.com/rbren/rss-parser#xml-options). |
|
|
62
62
|
| --user-agent | String | false | Specify custom user agent string for HTTP requests. Defaults to a Chrome user agent if not specified. |
|
|
63
63
|
| --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
|
|
64
|
+
| --trust-ext | | false | Trust file extension from URL, skip MIME-based correction. |
|
|
64
65
|
| --help | | false | Output usage information. |
|
|
65
66
|
|
|
66
67
|
## Archive
|
package/bin/archive.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import dayjs from "dayjs";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { getJsonFile } from "./util.js";
|
|
5
|
-
|
|
6
|
-
export const getArchiveKey = ({ prefix, name }) => {
|
|
7
|
-
return `${prefix}-${name}`;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const getArchive = (archive) => {
|
|
11
|
-
const archiveContent = getJsonFile(archive);
|
|
12
|
-
return archiveContent === null ? [] : archiveContent;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const writeToArchive = ({ key, archive }) => {
|
|
16
|
-
const archivePath = path.resolve(process.cwd(), archive);
|
|
17
|
-
const archiveResult = getArchive(archive);
|
|
18
|
-
|
|
19
|
-
if (!archiveResult.includes(key)) {
|
|
20
|
-
archiveResult.push(key);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
fs.writeFileSync(archivePath, JSON.stringify(archiveResult, null, 4));
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const getIsInArchive = ({ key, archive }) => {
|
|
27
|
-
const archiveResult = getArchive(archive);
|
|
28
|
-
return archiveResult.includes(key);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const getArchiveFilename = ({ pubDate, name, ext }) => {
|
|
32
|
-
const formattedPubDate = pubDate
|
|
33
|
-
? dayjs(new Date(pubDate)).format("YYYYMMDD")
|
|
34
|
-
: null;
|
|
35
|
-
|
|
36
|
-
const baseName = formattedPubDate ? `${formattedPubDate}-${name}` : name;
|
|
37
|
-
|
|
38
|
-
return `${baseName}${ext}`;
|
|
39
|
-
};
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getJsonFile } from "./util.js";
|
|
5
|
+
|
|
6
|
+
export const getArchiveKey = ({ prefix, name }) => {
|
|
7
|
+
return `${prefix}-${name}`;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const getArchive = (archive) => {
|
|
11
|
+
const archiveContent = getJsonFile(archive);
|
|
12
|
+
return archiveContent === null ? [] : archiveContent;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const writeToArchive = ({ key, archive }) => {
|
|
16
|
+
const archivePath = path.resolve(process.cwd(), archive);
|
|
17
|
+
const archiveResult = getArchive(archive);
|
|
18
|
+
|
|
19
|
+
if (!archiveResult.includes(key)) {
|
|
20
|
+
archiveResult.push(key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(archivePath, JSON.stringify(archiveResult, null, 4));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const getIsInArchive = ({ key, archive }) => {
|
|
27
|
+
const archiveResult = getArchive(archive);
|
|
28
|
+
return archiveResult.includes(key);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getArchiveFilename = ({ pubDate, name, ext }) => {
|
|
32
|
+
const formattedPubDate = pubDate
|
|
33
|
+
? dayjs(new Date(pubDate)).format("YYYYMMDD")
|
|
34
|
+
: null;
|
|
35
|
+
|
|
36
|
+
const baseName = formattedPubDate ? `${formattedPubDate}-${name}` : name;
|
|
37
|
+
|
|
38
|
+
return `${baseName}${ext}`;
|
|
39
|
+
};
|
package/bin/async.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { writeItemMeta } from "./meta.js";
|
|
23
23
|
import { getItemFilename } from "./naming.js";
|
|
24
24
|
import {
|
|
25
|
+
correctExtensionFromMime,
|
|
25
26
|
getEpisodeAudioUrlAndExt,
|
|
26
27
|
getTempPath,
|
|
27
28
|
prepareOutputPath,
|
|
@@ -45,6 +46,7 @@ export const download = async (options) => {
|
|
|
45
46
|
onAfterDownload,
|
|
46
47
|
attempt = 1,
|
|
47
48
|
maxAttempts = 3,
|
|
49
|
+
trustExt,
|
|
48
50
|
userAgent = USER_AGENT,
|
|
49
51
|
} = options;
|
|
50
52
|
|
|
@@ -56,12 +58,12 @@ export const download = async (options) => {
|
|
|
56
58
|
await onAfterDownload();
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
return;
|
|
61
|
+
return outputPath;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
if (key && archive && getIsInArchive({ key, archive })) {
|
|
63
65
|
logMessage("Download exists in archive. Skipping...");
|
|
64
|
-
return;
|
|
66
|
+
return null;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
let headResponse = null;
|
|
@@ -75,7 +77,7 @@ export const download = async (options) => {
|
|
|
75
77
|
"user-agent": userAgent,
|
|
76
78
|
},
|
|
77
79
|
});
|
|
78
|
-
} catch
|
|
80
|
+
} catch {
|
|
79
81
|
// unable to retrieve head response
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -125,7 +127,7 @@ export const download = async (options) => {
|
|
|
125
127
|
if (attempt <= maxAttempts) {
|
|
126
128
|
logMessage(`Download attempt #${attempt} failed. Retrying...`);
|
|
127
129
|
|
|
128
|
-
await download({
|
|
130
|
+
return await download({
|
|
129
131
|
...options,
|
|
130
132
|
attempt: attempt + 1,
|
|
131
133
|
});
|
|
@@ -144,24 +146,39 @@ export const download = async (options) => {
|
|
|
144
146
|
LOG_LEVELS.important
|
|
145
147
|
);
|
|
146
148
|
|
|
147
|
-
return;
|
|
149
|
+
return null;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
|
|
152
|
+
const { outputPath: finalOutputPath, key: finalKey } = trustExt
|
|
153
|
+
? { outputPath, key }
|
|
154
|
+
: correctExtensionFromMime({
|
|
155
|
+
outputPath,
|
|
156
|
+
key,
|
|
157
|
+
contentType: headResponse?.headers?.["content-type"],
|
|
158
|
+
onCorrect: (from, to) =>
|
|
159
|
+
logMessage(
|
|
160
|
+
`Correcting extension: ${from} --> ${to}`,
|
|
161
|
+
LOG_LEVELS.important
|
|
162
|
+
),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
fs.renameSync(tempOutputPath, finalOutputPath);
|
|
151
166
|
|
|
152
167
|
logMessage("Download complete!");
|
|
153
168
|
|
|
154
169
|
if (onAfterDownload) {
|
|
155
|
-
await onAfterDownload();
|
|
170
|
+
await onAfterDownload(finalOutputPath);
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
if (
|
|
173
|
+
if (finalKey && archive) {
|
|
159
174
|
try {
|
|
160
|
-
writeToArchive({ key, archive });
|
|
175
|
+
writeToArchive({ key: finalKey, archive });
|
|
161
176
|
} catch (error) {
|
|
162
177
|
throw new Error(`Error writing to archive: ${error.toString()}`);
|
|
163
178
|
}
|
|
164
179
|
}
|
|
180
|
+
|
|
181
|
+
return finalOutputPath;
|
|
165
182
|
};
|
|
166
183
|
|
|
167
184
|
export const downloadItemsAsync = async ({
|
|
@@ -185,6 +202,7 @@ export const downloadItemsAsync = async ({
|
|
|
185
202
|
alwaysPostprocess,
|
|
186
203
|
targetItems,
|
|
187
204
|
threads = 1,
|
|
205
|
+
trustExt,
|
|
188
206
|
userAgent = USER_AGENT,
|
|
189
207
|
}) => {
|
|
190
208
|
let numEpisodesDownloaded = 0;
|
|
@@ -224,6 +242,7 @@ export const downloadItemsAsync = async ({
|
|
|
224
242
|
override,
|
|
225
243
|
alwaysPostprocess,
|
|
226
244
|
marker,
|
|
245
|
+
trustExt,
|
|
227
246
|
userAgent,
|
|
228
247
|
key: getArchiveKey({
|
|
229
248
|
prefix: archivePrefix,
|
|
@@ -236,12 +255,13 @@ export const downloadItemsAsync = async ({
|
|
|
236
255
|
maxAttempts: attempts,
|
|
237
256
|
outputPath: outputPodcastPath,
|
|
238
257
|
url: episodeAudioUrl,
|
|
239
|
-
onAfterDownload: async () => {
|
|
258
|
+
onAfterDownload: async (finalEpisodePath) => {
|
|
240
259
|
if (item._episodeImage) {
|
|
241
260
|
try {
|
|
242
|
-
await download({
|
|
261
|
+
const finalImagePath = await download({
|
|
243
262
|
archive,
|
|
244
263
|
override,
|
|
264
|
+
trustExt,
|
|
245
265
|
userAgent,
|
|
246
266
|
key: item._episodeImage.key,
|
|
247
267
|
marker: item._episodeImage.url,
|
|
@@ -249,6 +269,10 @@ export const downloadItemsAsync = async ({
|
|
|
249
269
|
outputPath: item._episodeImage.outputPath,
|
|
250
270
|
url: item._episodeImage.url,
|
|
251
271
|
});
|
|
272
|
+
|
|
273
|
+
if (finalImagePath) {
|
|
274
|
+
item._episodeImage.outputPath = finalImagePath;
|
|
275
|
+
}
|
|
252
276
|
} catch (error) {
|
|
253
277
|
hasErrors = true;
|
|
254
278
|
logError(
|
|
@@ -261,16 +285,21 @@ export const downloadItemsAsync = async ({
|
|
|
261
285
|
|
|
262
286
|
if (item._episodeTranscript) {
|
|
263
287
|
try {
|
|
264
|
-
await download({
|
|
288
|
+
const finalTranscriptPath = await download({
|
|
265
289
|
archive,
|
|
266
290
|
override,
|
|
267
291
|
key: item._episodeTranscript.key,
|
|
268
292
|
marker: item._episodeTranscript.url,
|
|
269
293
|
maxAttempts: attempts,
|
|
270
294
|
outputPath: item._episodeTranscript.outputPath,
|
|
295
|
+
trustExt,
|
|
271
296
|
url: item._episodeTranscript.url,
|
|
272
297
|
userAgent,
|
|
273
298
|
});
|
|
299
|
+
|
|
300
|
+
if (finalTranscriptPath) {
|
|
301
|
+
item._episodeTranscript.outputPath = finalTranscriptPath;
|
|
302
|
+
}
|
|
274
303
|
} catch (error) {
|
|
275
304
|
hasErrors = true;
|
|
276
305
|
logError(
|
|
@@ -292,7 +321,7 @@ export const downloadItemsAsync = async ({
|
|
|
292
321
|
bitrate,
|
|
293
322
|
mono,
|
|
294
323
|
itemIndex: item._originalIndex,
|
|
295
|
-
outputPath:
|
|
324
|
+
outputPath: finalEpisodePath,
|
|
296
325
|
episodeImageOutputPath: hasEpisodeImage
|
|
297
326
|
? item._episodeImage.outputPath
|
|
298
327
|
: undefined,
|
|
@@ -310,7 +339,7 @@ export const downloadItemsAsync = async ({
|
|
|
310
339
|
await runExec({
|
|
311
340
|
exec,
|
|
312
341
|
basePath,
|
|
313
|
-
outputPodcastPath,
|
|
342
|
+
outputPodcastPath: finalEpisodePath,
|
|
314
343
|
episodeFilename,
|
|
315
344
|
episodeAudioUrl,
|
|
316
345
|
});
|
package/bin/bin.js
CHANGED
|
@@ -63,6 +63,7 @@ const {
|
|
|
63
63
|
addMp3Metadata: addMp3MetadataFlag,
|
|
64
64
|
adjustBitrate: bitrate,
|
|
65
65
|
season,
|
|
66
|
+
trustExt,
|
|
66
67
|
} = opts;
|
|
67
68
|
|
|
68
69
|
let { archive } = opts;
|
|
@@ -148,10 +149,7 @@ const main = async () => {
|
|
|
148
149
|
const podcastImageFileExt = getUrlExt(podcastImageUrl);
|
|
149
150
|
const outputImagePath = _path.resolve(
|
|
150
151
|
basePath,
|
|
151
|
-
getSimpleFilename(
|
|
152
|
-
feed.title ? feed.title : "image",
|
|
153
|
-
feed.title ? `.image${podcastImageFileExt}` : podcastImageFileExt
|
|
154
|
-
)
|
|
152
|
+
getSimpleFilename(feed.title || "image", podcastImageFileExt)
|
|
155
153
|
);
|
|
156
154
|
|
|
157
155
|
try {
|
|
@@ -159,13 +157,12 @@ const main = async () => {
|
|
|
159
157
|
await download({
|
|
160
158
|
archive,
|
|
161
159
|
override,
|
|
160
|
+
trustExt,
|
|
162
161
|
userAgent,
|
|
163
162
|
marker: podcastImageUrl,
|
|
164
163
|
key: getArchiveKey({
|
|
165
164
|
prefix: archivePrefix,
|
|
166
|
-
name: `${
|
|
167
|
-
feed.title ? `${feed.title}.image` : "image"
|
|
168
|
-
}${podcastImageFileExt}`,
|
|
165
|
+
name: `${feed.title || "image"}${podcastImageFileExt}`,
|
|
169
166
|
}),
|
|
170
167
|
outputPath: outputImagePath,
|
|
171
168
|
url: podcastImageUrl,
|
|
@@ -262,6 +259,7 @@ const main = async () => {
|
|
|
262
259
|
alwaysPostprocess,
|
|
263
260
|
targetItems,
|
|
264
261
|
threads,
|
|
262
|
+
trustExt,
|
|
265
263
|
userAgent,
|
|
266
264
|
});
|
|
267
265
|
|
package/bin/commander.js
CHANGED
|
@@ -191,7 +191,8 @@ export const setupCommander = (program) => {
|
|
|
191
191
|
"path to JSON config to override RSS parser"
|
|
192
192
|
)
|
|
193
193
|
.option("--proxy", "enable proxy support via global-agent")
|
|
194
|
-
.option("--user-agent <string>", "specify custom user agent string")
|
|
194
|
+
.option("--user-agent <string>", "specify custom user agent string")
|
|
195
|
+
.option("--trust-ext", "trust file extension, skip MIME-based correction");
|
|
195
196
|
|
|
196
197
|
program.parse();
|
|
197
198
|
|
package/bin/exec.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { exec } from "child_process";
|
|
2
|
-
import util from "util";
|
|
3
|
-
import { escapeArgForShell } from "./util.js";
|
|
4
|
-
|
|
5
|
-
export const execWithPromise = util.promisify(exec);
|
|
6
|
-
|
|
7
|
-
export const runExec = async ({
|
|
8
|
-
exec,
|
|
9
|
-
basePath,
|
|
10
|
-
outputPodcastPath,
|
|
11
|
-
episodeFilename,
|
|
12
|
-
episodeAudioUrl,
|
|
13
|
-
}) => {
|
|
14
|
-
const episodeFilenameBase = episodeFilename.substring(
|
|
15
|
-
0,
|
|
16
|
-
episodeFilename.lastIndexOf(".")
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const execCmd = exec
|
|
20
|
-
.replace(/{{episode_path}}/g, escapeArgForShell(outputPodcastPath))
|
|
21
|
-
.replace(/{{episode_path_base}}/g, escapeArgForShell(basePath))
|
|
22
|
-
.replace(/{{episode_filename}}/g, escapeArgForShell(episodeFilename))
|
|
23
|
-
.replace(
|
|
24
|
-
/{{episode_filename_base}}/g,
|
|
25
|
-
escapeArgForShell(episodeFilenameBase)
|
|
26
|
-
)
|
|
27
|
-
.replace(/{{url}}/g, escapeArgForShell(episodeAudioUrl));
|
|
28
|
-
|
|
29
|
-
await execWithPromise(execCmd, { stdio: "ignore" });
|
|
30
|
-
};
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import util from "util";
|
|
3
|
+
import { escapeArgForShell } from "./util.js";
|
|
4
|
+
|
|
5
|
+
export const execWithPromise = util.promisify(exec);
|
|
6
|
+
|
|
7
|
+
export const runExec = async ({
|
|
8
|
+
exec,
|
|
9
|
+
basePath,
|
|
10
|
+
outputPodcastPath,
|
|
11
|
+
episodeFilename,
|
|
12
|
+
episodeAudioUrl,
|
|
13
|
+
}) => {
|
|
14
|
+
const episodeFilenameBase = episodeFilename.substring(
|
|
15
|
+
0,
|
|
16
|
+
episodeFilename.lastIndexOf(".")
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const execCmd = exec
|
|
20
|
+
.replace(/{{episode_path}}/g, escapeArgForShell(outputPodcastPath))
|
|
21
|
+
.replace(/{{episode_path_base}}/g, escapeArgForShell(basePath))
|
|
22
|
+
.replace(/{{episode_filename}}/g, escapeArgForShell(episodeFilename))
|
|
23
|
+
.replace(
|
|
24
|
+
/{{episode_filename_base}}/g,
|
|
25
|
+
escapeArgForShell(episodeFilenameBase)
|
|
26
|
+
)
|
|
27
|
+
.replace(/{{url}}/g, escapeArgForShell(episodeAudioUrl));
|
|
28
|
+
|
|
29
|
+
await execWithPromise(execCmd, { stdio: "ignore" });
|
|
30
|
+
};
|
package/bin/ffmpeg.js
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import dayjs from "dayjs";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import { execWithPromise } from "./exec.js";
|
|
4
|
-
import { LOG_LEVELS, logMessage } from "./logger.js";
|
|
5
|
-
import { escapeArgForShell, isWin } from "./util.js";
|
|
6
|
-
|
|
7
|
-
export const runFfmpeg = async ({
|
|
8
|
-
feed,
|
|
9
|
-
item,
|
|
10
|
-
itemIndex,
|
|
11
|
-
outputPath,
|
|
12
|
-
episodeImageOutputPath,
|
|
13
|
-
bitrate,
|
|
14
|
-
mono,
|
|
15
|
-
addMp3Metadata,
|
|
16
|
-
ext,
|
|
17
|
-
}) => {
|
|
18
|
-
if (!fs.existsSync(outputPath)) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const shouldEmbedImage = addMp3Metadata && episodeImageOutputPath;
|
|
23
|
-
let command = `ffmpeg -loglevel quiet -i ${escapeArgForShell(outputPath)}`;
|
|
24
|
-
|
|
25
|
-
if (shouldEmbedImage) {
|
|
26
|
-
command += ` -i ${escapeArgForShell(episodeImageOutputPath)}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (bitrate) {
|
|
30
|
-
command += ` -b:a ${bitrate}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (mono) {
|
|
34
|
-
command += " -ac 1";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (addMp3Metadata) {
|
|
38
|
-
const album = feed.title || "";
|
|
39
|
-
const artist = item.itunes?.author || item.author || "";
|
|
40
|
-
const title = item.title || "";
|
|
41
|
-
const subtitle = item.itunes?.subtitle || "";
|
|
42
|
-
const comment = item.contentSnippet || item.content || "";
|
|
43
|
-
const disc = item.itunes?.season || "";
|
|
44
|
-
const track = item.itunes?.episode || `${feed.items.length - itemIndex}`;
|
|
45
|
-
const episodeType = item.itunes?.episodeType || "";
|
|
46
|
-
const date = item.pubDate
|
|
47
|
-
? dayjs(new Date(item.pubDate)).format("YYYY-MM-DD")
|
|
48
|
-
: "";
|
|
49
|
-
|
|
50
|
-
const metaKeysToValues = {
|
|
51
|
-
album,
|
|
52
|
-
artist,
|
|
53
|
-
album_artist: artist,
|
|
54
|
-
title,
|
|
55
|
-
disc,
|
|
56
|
-
track,
|
|
57
|
-
"episode-type": episodeType,
|
|
58
|
-
date,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
if (!isWin) {
|
|
62
|
-
// Due to limited escape options, these metadata fields often break in Windows
|
|
63
|
-
metaKeysToValues.comment = comment;
|
|
64
|
-
metaKeysToValues.subtitle = subtitle
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const metadataString = Object.keys(metaKeysToValues)
|
|
68
|
-
.map((key) => {
|
|
69
|
-
if (!metaKeysToValues[key]) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const argValue = escapeArgForShell(metaKeysToValues[key]);
|
|
74
|
-
|
|
75
|
-
return argValue ? `-metadata ${key}=${argValue}` : null;
|
|
76
|
-
})
|
|
77
|
-
.filter((segment) => !!segment)
|
|
78
|
-
.join(" ");
|
|
79
|
-
|
|
80
|
-
command += ` -map_metadata 0 ${metadataString} -codec copy`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (shouldEmbedImage) {
|
|
84
|
-
command += ` -map 0 -map 1`;
|
|
85
|
-
} else {
|
|
86
|
-
command += ` -map 0`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const tmpMp3Path = `${outputPath}.tmp${ext}`;
|
|
90
|
-
command += ` ${escapeArgForShell(tmpMp3Path)}`;
|
|
91
|
-
logMessage("Running command: " + command, LOG_LEVELS.debug);
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
await execWithPromise(command, { stdio: "ignore" });
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (fs.existsSync(tmpMp3Path)) {
|
|
97
|
-
fs.unlinkSync(tmpMp3Path);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
fs.unlinkSync(outputPath);
|
|
104
|
-
fs.renameSync(tmpMp3Path, outputPath);
|
|
105
|
-
};
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { execWithPromise } from "./exec.js";
|
|
4
|
+
import { LOG_LEVELS, logMessage } from "./logger.js";
|
|
5
|
+
import { escapeArgForShell, isWin } from "./util.js";
|
|
6
|
+
|
|
7
|
+
export const runFfmpeg = async ({
|
|
8
|
+
feed,
|
|
9
|
+
item,
|
|
10
|
+
itemIndex,
|
|
11
|
+
outputPath,
|
|
12
|
+
episodeImageOutputPath,
|
|
13
|
+
bitrate,
|
|
14
|
+
mono,
|
|
15
|
+
addMp3Metadata,
|
|
16
|
+
ext,
|
|
17
|
+
}) => {
|
|
18
|
+
if (!fs.existsSync(outputPath)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const shouldEmbedImage = addMp3Metadata && episodeImageOutputPath;
|
|
23
|
+
let command = `ffmpeg -loglevel quiet -i ${escapeArgForShell(outputPath)}`;
|
|
24
|
+
|
|
25
|
+
if (shouldEmbedImage) {
|
|
26
|
+
command += ` -i ${escapeArgForShell(episodeImageOutputPath)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (bitrate) {
|
|
30
|
+
command += ` -b:a ${bitrate}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (mono) {
|
|
34
|
+
command += " -ac 1";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (addMp3Metadata) {
|
|
38
|
+
const album = feed.title || "";
|
|
39
|
+
const artist = item.itunes?.author || item.author || "";
|
|
40
|
+
const title = item.title || "";
|
|
41
|
+
const subtitle = item.itunes?.subtitle || "";
|
|
42
|
+
const comment = item.contentSnippet || item.content || "";
|
|
43
|
+
const disc = item.itunes?.season || "";
|
|
44
|
+
const track = item.itunes?.episode || `${feed.items.length - itemIndex}`;
|
|
45
|
+
const episodeType = item.itunes?.episodeType || "";
|
|
46
|
+
const date = item.pubDate
|
|
47
|
+
? dayjs(new Date(item.pubDate)).format("YYYY-MM-DD")
|
|
48
|
+
: "";
|
|
49
|
+
|
|
50
|
+
const metaKeysToValues = {
|
|
51
|
+
album,
|
|
52
|
+
artist,
|
|
53
|
+
album_artist: artist,
|
|
54
|
+
title,
|
|
55
|
+
disc,
|
|
56
|
+
track,
|
|
57
|
+
"episode-type": episodeType,
|
|
58
|
+
date,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!isWin) {
|
|
62
|
+
// Due to limited escape options, these metadata fields often break in Windows
|
|
63
|
+
metaKeysToValues.comment = comment;
|
|
64
|
+
metaKeysToValues.subtitle = subtitle
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const metadataString = Object.keys(metaKeysToValues)
|
|
68
|
+
.map((key) => {
|
|
69
|
+
if (!metaKeysToValues[key]) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const argValue = escapeArgForShell(metaKeysToValues[key]);
|
|
74
|
+
|
|
75
|
+
return argValue ? `-metadata ${key}=${argValue}` : null;
|
|
76
|
+
})
|
|
77
|
+
.filter((segment) => !!segment)
|
|
78
|
+
.join(" ");
|
|
79
|
+
|
|
80
|
+
command += ` -map_metadata 0 ${metadataString} -codec copy`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (shouldEmbedImage) {
|
|
84
|
+
command += ` -map 0 -map 1`;
|
|
85
|
+
} else {
|
|
86
|
+
command += ` -map 0`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tmpMp3Path = `${outputPath}.tmp${ext}`;
|
|
90
|
+
command += ` ${escapeArgForShell(tmpMp3Path)}`;
|
|
91
|
+
logMessage("Running command: " + command, LOG_LEVELS.debug);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await execWithPromise(command, { stdio: "ignore" });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (fs.existsSync(tmpMp3Path)) {
|
|
97
|
+
fs.unlinkSync(tmpMp3Path);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fs.unlinkSync(outputPath);
|
|
104
|
+
fs.renameSync(tmpMp3Path, outputPath);
|
|
105
|
+
};
|