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 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 (error) {
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
- fs.renameSync(tempOutputPath, outputPath);
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 (key && archive) {
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: outputPodcastPath,
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
+ };