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/bin/meta.js
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import { getIsInArchive, writeToArchive } from "./archive.js";
|
|
3
|
-
import { logMessage } from "./logger.js";
|
|
4
|
-
import { getPublicObject } from "./util.js";
|
|
5
|
-
|
|
6
|
-
export const writeFeedMeta = ({ outputPath, feed, key, archive, override }) => {
|
|
7
|
-
if (key && archive && getIsInArchive({ key, archive })) {
|
|
8
|
-
logMessage("Feed metadata exists in archive. Skipping...");
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
const output = getPublicObject(feed, ["items"]);
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
if (override || !fs.existsSync(outputPath)) {
|
|
15
|
-
fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
|
|
16
|
-
} else {
|
|
17
|
-
logMessage("Feed metadata exists locally. Skipping...");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (key && archive && !getIsInArchive({ key, archive })) {
|
|
21
|
-
try {
|
|
22
|
-
writeToArchive({ key, archive });
|
|
23
|
-
} catch (error) {
|
|
24
|
-
throw new Error(`Error writing to archive: ${error.toString()}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
} catch (error) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`Unable to save metadata file for feed: ${error.toString()}`
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export const writeItemMeta = ({
|
|
35
|
-
marker,
|
|
36
|
-
outputPath,
|
|
37
|
-
item,
|
|
38
|
-
key,
|
|
39
|
-
archive,
|
|
40
|
-
override,
|
|
41
|
-
}) => {
|
|
42
|
-
if (key && archive && getIsInArchive({ key, archive })) {
|
|
43
|
-
logMessage(`${marker} | Episode metadata exists in archive. Skipping...`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const output = getPublicObject(item);
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
if (override || !fs.existsSync(outputPath)) {
|
|
51
|
-
fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
|
|
52
|
-
} else {
|
|
53
|
-
logMessage(`${marker} | Episode metadata exists locally. Skipping...`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (key && archive && !getIsInArchive({ key, archive })) {
|
|
57
|
-
try {
|
|
58
|
-
writeToArchive({ key, archive });
|
|
59
|
-
} catch (error) {
|
|
60
|
-
throw new Error("Error writing to archive", error);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
throw new Error("Unable to save meta file for episode", error);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { getIsInArchive, writeToArchive } from "./archive.js";
|
|
3
|
+
import { logMessage } from "./logger.js";
|
|
4
|
+
import { getPublicObject } from "./util.js";
|
|
5
|
+
|
|
6
|
+
export const writeFeedMeta = ({ outputPath, feed, key, archive, override }) => {
|
|
7
|
+
if (key && archive && getIsInArchive({ key, archive })) {
|
|
8
|
+
logMessage("Feed metadata exists in archive. Skipping...");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const output = getPublicObject(feed, ["items"]);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
if (override || !fs.existsSync(outputPath)) {
|
|
15
|
+
fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
|
|
16
|
+
} else {
|
|
17
|
+
logMessage("Feed metadata exists locally. Skipping...");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (key && archive && !getIsInArchive({ key, archive })) {
|
|
21
|
+
try {
|
|
22
|
+
writeToArchive({ key, archive });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(`Error writing to archive: ${error.toString()}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Unable to save metadata file for feed: ${error.toString()}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const writeItemMeta = ({
|
|
35
|
+
marker,
|
|
36
|
+
outputPath,
|
|
37
|
+
item,
|
|
38
|
+
key,
|
|
39
|
+
archive,
|
|
40
|
+
override,
|
|
41
|
+
}) => {
|
|
42
|
+
if (key && archive && getIsInArchive({ key, archive })) {
|
|
43
|
+
logMessage(`${marker} | Episode metadata exists in archive. Skipping...`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const output = getPublicObject(item);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (override || !fs.existsSync(outputPath)) {
|
|
51
|
+
fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
|
|
52
|
+
} else {
|
|
53
|
+
logMessage(`${marker} | Episode metadata exists locally. Skipping...`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (key && archive && !getIsInArchive({ key, archive })) {
|
|
57
|
+
try {
|
|
58
|
+
writeToArchive({ key, archive });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error("Error writing to archive", error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error("Unable to save meta file for episode", error);
|
|
65
|
+
}
|
|
66
|
+
};
|
package/bin/naming.js
CHANGED
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
import dayjs from "dayjs";
|
|
2
|
-
import filenamify from "filenamify";
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
const INVALID_CHAR_REPLACE = "_";
|
|
6
|
-
|
|
7
|
-
const FILTER_FUNCTIONS = {
|
|
8
|
-
strip: (val) => val.replace(/\s+/g, ""),
|
|
9
|
-
strip_special: (val) => val.replace(/[^a-zA-Z0-9\s]/g, ""),
|
|
10
|
-
underscore: (val) => val.replace(/\s+/g, "_"),
|
|
11
|
-
dash: (val) => val.replace(/\s+/g, "-"),
|
|
12
|
-
camelcase: (val) =>
|
|
13
|
-
val
|
|
14
|
-
.split(/\s+/)
|
|
15
|
-
.map((w) =>
|
|
16
|
-
w ? w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() : ""
|
|
17
|
-
)
|
|
18
|
-
.join(""),
|
|
19
|
-
lowercase: (val) => val.toLowerCase(),
|
|
20
|
-
uppercase: (val) => val.toUpperCase(),
|
|
21
|
-
trim: (val) => val.trim(),
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const applyFilters = (value, filterStr) => {
|
|
25
|
-
if (!filterStr) {
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const filters = filterStr.slice(1).split("|");
|
|
30
|
-
return filters.reduce((val, filter) => {
|
|
31
|
-
const filterFn = FILTER_FUNCTIONS[filter];
|
|
32
|
-
return filterFn ? filterFn(val) : val;
|
|
33
|
-
}, value);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const MAX_LENGTH_FILENAME = process.env.MAX_LENGTH_FILENAME
|
|
37
|
-
? parseInt(process.env.MAX_LENGTH_FILENAME)
|
|
38
|
-
: 255;
|
|
39
|
-
|
|
40
|
-
export const getSafeName = (name, maxLength = MAX_LENGTH_FILENAME) => {
|
|
41
|
-
return filenamify(name, {
|
|
42
|
-
replacement: INVALID_CHAR_REPLACE,
|
|
43
|
-
maxLength,
|
|
44
|
-
});
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const getSimpleFilename = (name, ext = "") => {
|
|
48
|
-
return `${getSafeName(name, MAX_LENGTH_FILENAME - (ext?.length ?? 0))}${ext}`;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const getItemFilename = ({
|
|
52
|
-
item,
|
|
53
|
-
ext,
|
|
54
|
-
url,
|
|
55
|
-
feed,
|
|
56
|
-
template,
|
|
57
|
-
width,
|
|
58
|
-
customTemplateOptions = [],
|
|
59
|
-
offset = 0,
|
|
60
|
-
}) => {
|
|
61
|
-
const episodeNum = feed.items.length - item._originalIndex + offset;
|
|
62
|
-
const title = item.title || "";
|
|
63
|
-
|
|
64
|
-
const releaseYear = item.pubDate
|
|
65
|
-
? dayjs(new Date(item.pubDate)).format("YYYY")
|
|
66
|
-
: null;
|
|
67
|
-
|
|
68
|
-
const releaseMonth = item.pubDate
|
|
69
|
-
? dayjs(new Date(item.pubDate)).format("MM")
|
|
70
|
-
: null;
|
|
71
|
-
|
|
72
|
-
const releaseDay = item.pubDate
|
|
73
|
-
? dayjs(new Date(item.pubDate)).format("DD")
|
|
74
|
-
: null;
|
|
75
|
-
|
|
76
|
-
const releaseDate = item.pubDate
|
|
77
|
-
? dayjs(new Date(item.pubDate)).format("YYYYMMDD")
|
|
78
|
-
: null;
|
|
79
|
-
|
|
80
|
-
const customReplacementTuples = customTemplateOptions.map((option, i) => {
|
|
81
|
-
const matchRegex = new RegExp(option);
|
|
82
|
-
const match = title.match(matchRegex);
|
|
83
|
-
|
|
84
|
-
return match && match[0] ? [`custom_${i}`, match[0]] : [`custom_${i}`, ""];
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const templateReplacementsTuples = [
|
|
88
|
-
["title", title],
|
|
89
|
-
["release_date", releaseDate || ""],
|
|
90
|
-
["release_year", releaseYear || ""],
|
|
91
|
-
["release_month", releaseMonth || ""],
|
|
92
|
-
["release_day", releaseDay || ""],
|
|
93
|
-
["episode_num", `${episodeNum}`.padStart(width, "0")],
|
|
94
|
-
["url", url],
|
|
95
|
-
["podcast_title", feed.title || ""],
|
|
96
|
-
["podcast_link", feed.link || ""],
|
|
97
|
-
["duration", item.itunes?.duration || ""],
|
|
98
|
-
["guid", item.guid],
|
|
99
|
-
...customReplacementTuples,
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
const replacementsMap = Object.fromEntries(templateReplacementsTuples);
|
|
103
|
-
const templateSegments = template.trim().split(path.sep);
|
|
104
|
-
const nameSegments = templateSegments.map((segment) => {
|
|
105
|
-
const replaceRegex = /{{(\w+)(\|[^}]+)?}}/g;
|
|
106
|
-
const name = segment.replace(replaceRegex, (match, varName, filterStr) => {
|
|
107
|
-
const replacement = replacementsMap[varName] || "";
|
|
108
|
-
return applyFilters(replacement, filterStr);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return getSimpleFilename(name);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
nameSegments[nameSegments.length - 1] = getSimpleFilename(
|
|
115
|
-
nameSegments[nameSegments.length - 1],
|
|
116
|
-
ext
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
return nameSegments.join(path.sep);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export const getFolderName = ({ feed, template }) => {
|
|
123
|
-
const replacementsMap = {
|
|
124
|
-
podcast_title: feed.title || "",
|
|
125
|
-
podcast_link: feed.link || "",
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const replaceRegex = /{{(\w+)(\|[^}]+)?}}/g;
|
|
129
|
-
const name = template.replace(replaceRegex, (_, varName, filterStr) => {
|
|
130
|
-
const replacement = replacementsMap[varName] || "";
|
|
131
|
-
const filtered = applyFilters(replacement, filterStr);
|
|
132
|
-
return getSafeName(filtered);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
return name;
|
|
136
|
-
};
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import filenamify from "filenamify";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const INVALID_CHAR_REPLACE = "_";
|
|
6
|
+
|
|
7
|
+
const FILTER_FUNCTIONS = {
|
|
8
|
+
strip: (val) => val.replace(/\s+/g, ""),
|
|
9
|
+
strip_special: (val) => val.replace(/[^a-zA-Z0-9\s]/g, ""),
|
|
10
|
+
underscore: (val) => val.replace(/\s+/g, "_"),
|
|
11
|
+
dash: (val) => val.replace(/\s+/g, "-"),
|
|
12
|
+
camelcase: (val) =>
|
|
13
|
+
val
|
|
14
|
+
.split(/\s+/)
|
|
15
|
+
.map((w) =>
|
|
16
|
+
w ? w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() : ""
|
|
17
|
+
)
|
|
18
|
+
.join(""),
|
|
19
|
+
lowercase: (val) => val.toLowerCase(),
|
|
20
|
+
uppercase: (val) => val.toUpperCase(),
|
|
21
|
+
trim: (val) => val.trim(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const applyFilters = (value, filterStr) => {
|
|
25
|
+
if (!filterStr) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const filters = filterStr.slice(1).split("|");
|
|
30
|
+
return filters.reduce((val, filter) => {
|
|
31
|
+
const filterFn = FILTER_FUNCTIONS[filter];
|
|
32
|
+
return filterFn ? filterFn(val) : val;
|
|
33
|
+
}, value);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const MAX_LENGTH_FILENAME = process.env.MAX_LENGTH_FILENAME
|
|
37
|
+
? parseInt(process.env.MAX_LENGTH_FILENAME)
|
|
38
|
+
: 255;
|
|
39
|
+
|
|
40
|
+
export const getSafeName = (name, maxLength = MAX_LENGTH_FILENAME) => {
|
|
41
|
+
return filenamify(name, {
|
|
42
|
+
replacement: INVALID_CHAR_REPLACE,
|
|
43
|
+
maxLength,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const getSimpleFilename = (name, ext = "") => {
|
|
48
|
+
return `${getSafeName(name, MAX_LENGTH_FILENAME - (ext?.length ?? 0))}${ext}`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const getItemFilename = ({
|
|
52
|
+
item,
|
|
53
|
+
ext,
|
|
54
|
+
url,
|
|
55
|
+
feed,
|
|
56
|
+
template,
|
|
57
|
+
width,
|
|
58
|
+
customTemplateOptions = [],
|
|
59
|
+
offset = 0,
|
|
60
|
+
}) => {
|
|
61
|
+
const episodeNum = feed.items.length - item._originalIndex + offset;
|
|
62
|
+
const title = item.title || "";
|
|
63
|
+
|
|
64
|
+
const releaseYear = item.pubDate
|
|
65
|
+
? dayjs(new Date(item.pubDate)).format("YYYY")
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
const releaseMonth = item.pubDate
|
|
69
|
+
? dayjs(new Date(item.pubDate)).format("MM")
|
|
70
|
+
: null;
|
|
71
|
+
|
|
72
|
+
const releaseDay = item.pubDate
|
|
73
|
+
? dayjs(new Date(item.pubDate)).format("DD")
|
|
74
|
+
: null;
|
|
75
|
+
|
|
76
|
+
const releaseDate = item.pubDate
|
|
77
|
+
? dayjs(new Date(item.pubDate)).format("YYYYMMDD")
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
const customReplacementTuples = customTemplateOptions.map((option, i) => {
|
|
81
|
+
const matchRegex = new RegExp(option);
|
|
82
|
+
const match = title.match(matchRegex);
|
|
83
|
+
|
|
84
|
+
return match && match[0] ? [`custom_${i}`, match[0]] : [`custom_${i}`, ""];
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const templateReplacementsTuples = [
|
|
88
|
+
["title", title],
|
|
89
|
+
["release_date", releaseDate || ""],
|
|
90
|
+
["release_year", releaseYear || ""],
|
|
91
|
+
["release_month", releaseMonth || ""],
|
|
92
|
+
["release_day", releaseDay || ""],
|
|
93
|
+
["episode_num", `${episodeNum}`.padStart(width, "0")],
|
|
94
|
+
["url", url],
|
|
95
|
+
["podcast_title", feed.title || ""],
|
|
96
|
+
["podcast_link", feed.link || ""],
|
|
97
|
+
["duration", item.itunes?.duration || ""],
|
|
98
|
+
["guid", item.guid],
|
|
99
|
+
...customReplacementTuples,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const replacementsMap = Object.fromEntries(templateReplacementsTuples);
|
|
103
|
+
const templateSegments = template.trim().split(path.sep);
|
|
104
|
+
const nameSegments = templateSegments.map((segment) => {
|
|
105
|
+
const replaceRegex = /{{(\w+)(\|[^}]+)?}}/g;
|
|
106
|
+
const name = segment.replace(replaceRegex, (match, varName, filterStr) => {
|
|
107
|
+
const replacement = replacementsMap[varName] || "";
|
|
108
|
+
return applyFilters(replacement, filterStr);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return getSimpleFilename(name);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
nameSegments[nameSegments.length - 1] = getSimpleFilename(
|
|
115
|
+
nameSegments[nameSegments.length - 1],
|
|
116
|
+
ext
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return nameSegments.join(path.sep);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const getFolderName = ({ feed, template }) => {
|
|
123
|
+
const replacementsMap = {
|
|
124
|
+
podcast_title: feed.title || "",
|
|
125
|
+
podcast_link: feed.link || "",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const replaceRegex = /{{(\w+)(\|[^}]+)?}}/g;
|
|
129
|
+
const name = template.replace(replaceRegex, (_, varName, filterStr) => {
|
|
130
|
+
const replacement = replacementsMap[varName] || "";
|
|
131
|
+
const filtered = applyFilters(replacement, filterStr);
|
|
132
|
+
return getSafeName(filtered);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return name;
|
|
136
|
+
};
|
package/bin/util.js
CHANGED
|
@@ -160,6 +160,117 @@ export const AUDIO_TYPES_TO_EXTS = {
|
|
|
160
160
|
"video/x-m4v": ".m4v",
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
+
export const IMAGE_TYPES_TO_EXTS = {
|
|
164
|
+
"image/jpeg": ".jpg",
|
|
165
|
+
"image/jpg": ".jpg",
|
|
166
|
+
"image/png": ".png",
|
|
167
|
+
"image/gif": ".gif",
|
|
168
|
+
"image/webp": ".webp",
|
|
169
|
+
"image/bmp": ".bmp",
|
|
170
|
+
"image/tiff": ".tiff",
|
|
171
|
+
"image/avif": ".avif",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const TRANSCRIPT_TYPES_TO_EXTS = {
|
|
175
|
+
"application/json": ".json",
|
|
176
|
+
"application/srt": ".srt",
|
|
177
|
+
"application/ttml+xml": ".ttml",
|
|
178
|
+
"application/x-subrip": ".srt",
|
|
179
|
+
"text/html": ".html",
|
|
180
|
+
"text/plain": ".txt",
|
|
181
|
+
"text/srt": ".srt",
|
|
182
|
+
"text/vtt": ".vtt",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const MIME_TO_EXT = {
|
|
186
|
+
...AUDIO_TYPES_TO_EXTS,
|
|
187
|
+
...IMAGE_TYPES_TO_EXTS,
|
|
188
|
+
...TRANSCRIPT_TYPES_TO_EXTS,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const getExtFromMime = (mime) => MIME_TO_EXT[mime] || null;
|
|
192
|
+
|
|
193
|
+
const MEDIA_CATEGORIES = {
|
|
194
|
+
audio: "audio",
|
|
195
|
+
image: "image",
|
|
196
|
+
transcript: "transcript",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const VALID_AUDIO_EXTS_SET = new Set(Object.values(AUDIO_TYPES_TO_EXTS));
|
|
200
|
+
const VALID_IMAGE_EXTS_SET = new Set(Object.values(IMAGE_TYPES_TO_EXTS));
|
|
201
|
+
const VALID_TRANSCRIPT_EXTS_SET = new Set(
|
|
202
|
+
Object.values(TRANSCRIPT_TYPES_TO_EXTS)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const getExtCategory = (ext) => {
|
|
206
|
+
if (VALID_AUDIO_EXTS_SET.has(ext)) {
|
|
207
|
+
return MEDIA_CATEGORIES.audio;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (VALID_IMAGE_EXTS_SET.has(ext)) {
|
|
211
|
+
return MEDIA_CATEGORIES.image;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (VALID_TRANSCRIPT_EXTS_SET.has(ext)) {
|
|
215
|
+
return MEDIA_CATEGORIES.transcript;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const getMimeCategory = (mime) => {
|
|
221
|
+
if (AUDIO_TYPES_TO_EXTS[mime]) {
|
|
222
|
+
return MEDIA_CATEGORIES.audio;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (IMAGE_TYPES_TO_EXTS[mime]) {
|
|
226
|
+
return MEDIA_CATEGORIES.image;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (TRANSCRIPT_TYPES_TO_EXTS[mime]) {
|
|
230
|
+
return MEDIA_CATEGORIES.transcript;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const correctExtensionFromMime = ({
|
|
237
|
+
outputPath,
|
|
238
|
+
key,
|
|
239
|
+
contentType,
|
|
240
|
+
onCorrect,
|
|
241
|
+
}) => {
|
|
242
|
+
const mimeType = contentType?.split(";")[0];
|
|
243
|
+
const mimeExt = mimeType ? getExtFromMime(mimeType) : null;
|
|
244
|
+
|
|
245
|
+
if (!mimeExt) {
|
|
246
|
+
return { outputPath, key };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const currentExt = path.extname(outputPath);
|
|
250
|
+
if (mimeExt === currentExt) {
|
|
251
|
+
return { outputPath, key };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const currentCategory = getExtCategory(currentExt);
|
|
255
|
+
const mimeCategory = getMimeCategory(mimeType);
|
|
256
|
+
|
|
257
|
+
if (currentCategory && mimeCategory && currentCategory !== mimeCategory) {
|
|
258
|
+
return { outputPath, key };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const basePath = currentExt
|
|
262
|
+
? outputPath.slice(0, -currentExt.length)
|
|
263
|
+
: outputPath;
|
|
264
|
+
const baseKey = key && currentExt ? key.slice(0, -currentExt.length) : key;
|
|
265
|
+
|
|
266
|
+
onCorrect?.(currentExt || "(none)", mimeExt);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
outputPath: basePath + mimeExt,
|
|
270
|
+
key: baseKey ? baseKey + mimeExt : null,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
|
|
163
274
|
export const VALID_AUDIO_EXTS = [
|
|
164
275
|
...new Set(Object.values(AUDIO_TYPES_TO_EXTS)),
|
|
165
276
|
];
|
|
@@ -168,7 +279,7 @@ export const getIsAudioUrl = (url) => {
|
|
|
168
279
|
let ext;
|
|
169
280
|
try {
|
|
170
281
|
ext = getUrlExt(url);
|
|
171
|
-
} catch
|
|
282
|
+
} catch {
|
|
172
283
|
return false;
|
|
173
284
|
}
|
|
174
285
|
|
package/bin/validate.js
CHANGED
|
@@ -13,11 +13,11 @@ export const createParseNumber = ({ min, max, name, required = true }) => {
|
|
|
13
13
|
logErrorAndExit(`${name} must be a number`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
if (
|
|
16
|
+
if (min !== undefined && number < min) {
|
|
17
17
|
logErrorAndExit(`${name} must be >= ${min}`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
if (
|
|
20
|
+
if (max !== undefined && number > max) {
|
|
21
21
|
logErrorAndExit(
|
|
22
22
|
`${name} must be <= ${
|
|
23
23
|
max === Number.MAX_SAFE_INTEGER ? "Number.MAX_SAFE_INTEGER" : max
|
|
@@ -26,7 +26,7 @@ export const createParseNumber = ({ min, max, name, required = true }) => {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
return number;
|
|
29
|
-
} catch
|
|
29
|
+
} catch {
|
|
30
30
|
logErrorAndExit(`Unable to parse ${name}`);
|
|
31
31
|
}
|
|
32
32
|
};
|
package/package.json
CHANGED
|
@@ -1,62 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "podcast-dl",
|
|
3
|
-
"version": "11.
|
|
4
|
-
"description": "A CLI for downloading podcasts.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": "./bin/bin.js",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "rimraf ./binaries && rimraf ./dist && node build.cjs",
|
|
9
|
-
"lint": "eslint ./bin"
|
|
10
|
-
},
|
|
11
|
-
"lint-staged": {
|
|
12
|
-
"*.{js,json,md}": [
|
|
13
|
-
"prettier --write"
|
|
14
|
-
]
|
|
15
|
-
},
|
|
16
|
-
"husky": {
|
|
17
|
-
"hooks": {
|
|
18
|
-
"pre-commit": "lint-staged",
|
|
19
|
-
"pre-push": "npm run lint"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"keywords": [
|
|
23
|
-
"podcast",
|
|
24
|
-
"podcasts",
|
|
25
|
-
"downloader",
|
|
26
|
-
"cli"
|
|
27
|
-
],
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=18.17.0"
|
|
30
|
-
},
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "https://github.com/lightpohl/podcast-dl.git"
|
|
34
|
-
},
|
|
35
|
-
"files": [
|
|
36
|
-
"bin"
|
|
37
|
-
],
|
|
38
|
-
"author": "Joshua Pohl",
|
|
39
|
-
"license": "MIT",
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"
|
|
43
|
-
"eslint
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "podcast-dl",
|
|
3
|
+
"version": "11.5.0",
|
|
4
|
+
"description": "A CLI for downloading podcasts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "./bin/bin.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "rimraf ./binaries && rimraf ./dist && node build.cjs",
|
|
9
|
+
"lint": "eslint ./bin"
|
|
10
|
+
},
|
|
11
|
+
"lint-staged": {
|
|
12
|
+
"*.{js,json,md}": [
|
|
13
|
+
"prettier --write"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"husky": {
|
|
17
|
+
"hooks": {
|
|
18
|
+
"pre-commit": "lint-staged",
|
|
19
|
+
"pre-push": "npm run lint"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"podcast",
|
|
24
|
+
"podcasts",
|
|
25
|
+
"downloader",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.17.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/lightpohl/podcast-dl.git"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin"
|
|
37
|
+
],
|
|
38
|
+
"author": "Joshua Pohl",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@eslint/js": "^9.18.0",
|
|
42
|
+
"@yao-pkg/pkg": "^6.6.0",
|
|
43
|
+
"eslint": "^9.18.0",
|
|
44
|
+
"globals": "^15.14.0",
|
|
45
|
+
"husky": "^4.2.5",
|
|
46
|
+
"lint-staged": "^10.1.7",
|
|
47
|
+
"prettier": "2.3.2",
|
|
48
|
+
"rimraf": "^3.0.2",
|
|
49
|
+
"webpack": "^5.75.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"command-exists": "^1.2.9",
|
|
53
|
+
"commander": "^12.1.0",
|
|
54
|
+
"dayjs": "^1.8.25",
|
|
55
|
+
"filenamify": "^6.0.0",
|
|
56
|
+
"global-agent": "^3.0.0",
|
|
57
|
+
"got": "^11.0.2",
|
|
58
|
+
"p-limit": "^4.0.0",
|
|
59
|
+
"pluralize": "^8.0.0",
|
|
60
|
+
"rss-parser": "^3.12.0",
|
|
61
|
+
"throttle-debounce": "^3.0.1"
|
|
62
|
+
}
|
|
63
|
+
}
|