podcast-dl 11.1.1 → 11.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/bin/util.js CHANGED
@@ -1,299 +1,299 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import rssParser from "rss-parser";
4
- import { logErrorAndExit, logMessage } from "./logger.js";
5
-
6
- export const isWin = process.platform === "win32";
7
-
8
- export const defaultRssParserConfig = {
9
- defaultRSS: 2.0,
10
- headers: {
11
- Accept: "*/*",
12
- },
13
- customFields: {
14
- item: [["podcast:transcript", "podcastTranscripts", { keepArray: true }]],
15
- },
16
- };
17
-
18
- /*
19
- Escape arguments for a shell command used with exec.
20
- Borrowed from shell-escape: https://github.com/xxorax/node-shell-escape/
21
- Additionally, @see https://www.robvanderwoude.com/escapechars.php for why
22
- we avoid trying to escape complex sequences in Windows.
23
- */
24
- export const escapeArgForShell = (arg) => {
25
- let result = arg;
26
-
27
- if (/[^A-Za-z0-9_/:=-]/.test(result)) {
28
- if (isWin) {
29
- return `"${result}"`;
30
- } else {
31
- result = "'" + result.replace(/'/g, "'\\''") + "'";
32
- result = result
33
- .replace(/^(?:'')+/g, "") // unduplicate single-quote at the beginning
34
- .replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
35
- }
36
- }
37
-
38
- return result;
39
- };
40
-
41
- export const getTempPath = (path) => {
42
- return `${path}.tmp`;
43
- };
44
-
45
- export const prepareOutputPath = (outputPath) => {
46
- const outputPathSegments = outputPath.split(path.sep);
47
- outputPathSegments.pop();
48
-
49
- const directoryOutputPath = outputPathSegments.join(path.sep);
50
-
51
- if (directoryOutputPath.length) {
52
- fs.mkdirSync(directoryOutputPath, { recursive: true });
53
- }
54
- };
55
-
56
- export const getPublicObject = (object, exclude = []) => {
57
- const output = {};
58
- Object.keys(object).forEach((key) => {
59
- if (!key.startsWith("_") && !exclude.includes(key) && object[key]) {
60
- output[key] = object[key];
61
- }
62
- });
63
-
64
- return output;
65
- };
66
-
67
- export const getFileString = (filePath) => {
68
- const fullPath = path.resolve(process.cwd(), filePath);
69
-
70
- if (!fs.existsSync(fullPath)) {
71
- return null;
72
- }
73
-
74
- const data = fs.readFileSync(fullPath, "utf8");
75
-
76
- if (!data) {
77
- return null;
78
- }
79
-
80
- return data;
81
- };
82
-
83
- export const getJsonFile = (filePath) => {
84
- const fullPath = path.resolve(process.cwd(), filePath);
85
-
86
- if (!fs.existsSync(fullPath)) {
87
- return null;
88
- }
89
-
90
- const data = fs.readFileSync(fullPath);
91
-
92
- if (!data) {
93
- return null;
94
- }
95
-
96
- return JSON.parse(data);
97
- };
98
-
99
- export const getLoopControls = ({ offset, length, reverse }) => {
100
- if (reverse) {
101
- const startIndex = length - 1 - offset;
102
- const min = -1;
103
- const shouldGo = (i) => i > min;
104
- const decrement = (i) => i - 1;
105
-
106
- return {
107
- startIndex,
108
- shouldGo,
109
- next: decrement,
110
- };
111
- }
112
-
113
- const startIndex = 0 + offset;
114
- const max = length;
115
- const shouldGo = (i) => i < max;
116
- const increment = (i) => i + 1;
117
-
118
- return {
119
- startIndex,
120
- shouldGo,
121
- next: increment,
122
- };
123
- };
124
-
125
- export const logFeedInfo = (feed) => {
126
- logMessage(feed.title);
127
- logMessage(feed.description);
128
- logMessage();
129
- };
130
-
131
- export const getUrlExt = (url) => {
132
- if (!url) {
133
- return "";
134
- }
135
-
136
- const { pathname } = new URL(url);
137
-
138
- if (!pathname) {
139
- return "";
140
- }
141
-
142
- const ext = path.extname(pathname);
143
- return ext;
144
- };
145
-
146
- export const AUDIO_TYPES_TO_EXTS = {
147
- "audio/aac": ".aac",
148
- "audio/flac": ".flac",
149
- "audio/mp3": ".mp3",
150
- "audio/mp4": ".m4a",
151
- "audio/mpeg": ".mp3",
152
- "audio/ogg": ".ogg",
153
- "audio/opus": ".opus",
154
- "audio/vorbis": ".ogg",
155
- "audio/wav": ".wav",
156
- "audio/x-m4a": ".m4a",
157
- "audio/x-wav": ".wav",
158
- "video/mp4": ".mp4",
159
- "video/quicktime": ".mov",
160
- "video/x-m4v": ".m4v",
161
- };
162
-
163
- export const VALID_AUDIO_EXTS = [
164
- ...new Set(Object.values(AUDIO_TYPES_TO_EXTS)),
165
- ];
166
-
167
- export const getIsAudioUrl = (url) => {
168
- let ext;
169
- try {
170
- ext = getUrlExt(url);
171
- } catch (err) {
172
- return false;
173
- }
174
-
175
- if (!ext) {
176
- return false;
177
- }
178
-
179
- return VALID_AUDIO_EXTS.includes(ext);
180
- };
181
-
182
- export const AUDIO_ORDER_TYPES = {
183
- enclosure: "enclosure",
184
- link: "link",
185
- };
186
-
187
- export const getEpisodeAudioUrlAndExt = (
188
- { enclosure, link },
189
- order = [AUDIO_ORDER_TYPES.enclosure, AUDIO_ORDER_TYPES.link]
190
- ) => {
191
- for (const source of order) {
192
- if (source === AUDIO_ORDER_TYPES.link && link && getIsAudioUrl(link)) {
193
- return { url: link, ext: getUrlExt(link) };
194
- }
195
-
196
- if (source === AUDIO_ORDER_TYPES.enclosure && enclosure) {
197
- if (getIsAudioUrl(enclosure.url)) {
198
- return { url: enclosure.url, ext: getUrlExt(enclosure.url) };
199
- }
200
-
201
- if (enclosure.url && AUDIO_TYPES_TO_EXTS[enclosure.type]) {
202
- return { url: enclosure.url, ext: AUDIO_TYPES_TO_EXTS[enclosure.type] };
203
- }
204
- }
205
- }
206
-
207
- return { url: null, ext: null };
208
- };
209
-
210
- export const getImageUrl = ({ image, itunes }) => {
211
- if (image?.url) {
212
- return image.url;
213
- }
214
-
215
- if (image?.link) {
216
- return image.link;
217
- }
218
-
219
- if (itunes?.image) {
220
- return itunes.image;
221
- }
222
-
223
- return null;
224
- };
225
-
226
- export const TRANSCRIPT_TYPES = {
227
- "application/json": "application/json",
228
- "application/srr": "application/srr",
229
- "application/srt": "application/srt",
230
- "application/x-subrip": "application/x-subrip",
231
- "text/html": "text/html",
232
- "text/plain": "text/plain",
233
- "text/vtt": "text/vtt",
234
- };
235
-
236
- // @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#transcript
237
- export const getTranscriptUrl = (item, transcriptTypes = []) => {
238
- if (!item.podcastTranscripts?.length) {
239
- return null;
240
- }
241
-
242
- for (const transcriptType of transcriptTypes) {
243
- const matchingTranscriptType = item.podcastTranscripts.find(
244
- (transcript) =>
245
- !!transcript?.["$"]?.url && transcript?.["$"]?.type === transcriptType
246
- );
247
-
248
- if (matchingTranscriptType) {
249
- return matchingTranscriptType?.["$"]?.url;
250
- }
251
- }
252
-
253
- return null;
254
- };
255
-
256
- export const getFileFeed = async (filePath, parserConfig) => {
257
- const config = parserConfig
258
- ? getJsonFile(parserConfig)
259
- : defaultRssParserConfig;
260
- const rssString = getFileString(filePath);
261
-
262
- if (parserConfig && !config) {
263
- logErrorAndExit(`Unable to load parser config: ${parserConfig}`);
264
- }
265
-
266
- const parser = new rssParser(config);
267
-
268
- let feed;
269
- try {
270
- feed = await parser.parseString(rssString);
271
- } catch (err) {
272
- logErrorAndExit("Unable to parse local RSS file", err);
273
- }
274
-
275
- return feed;
276
- };
277
-
278
- export const getUrlFeed = async (url, parserConfig) => {
279
- const config = parserConfig
280
- ? getJsonFile(parserConfig)
281
- : defaultRssParserConfig;
282
-
283
- if (parserConfig && !config) {
284
- logErrorAndExit(`Unable to load parser config: ${parserConfig}`);
285
- }
286
-
287
- const parser = new rssParser(config);
288
-
289
- const { href } = new URL(url);
290
-
291
- let feed;
292
- try {
293
- feed = await parser.parseURL(href);
294
- } catch (err) {
295
- logErrorAndExit("Unable to parse RSS URL", err);
296
- }
297
-
298
- return feed;
299
- };
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import rssParser from "rss-parser";
4
+ import { logErrorAndExit, logMessage } from "./logger.js";
5
+
6
+ export const isWin = process.platform === "win32";
7
+
8
+ export const defaultRssParserConfig = {
9
+ defaultRSS: 2.0,
10
+ headers: {
11
+ Accept: "*/*",
12
+ },
13
+ customFields: {
14
+ item: [["podcast:transcript", "podcastTranscripts", { keepArray: true }]],
15
+ },
16
+ };
17
+
18
+ /*
19
+ Escape arguments for a shell command used with exec.
20
+ Borrowed from shell-escape: https://github.com/xxorax/node-shell-escape/
21
+ Additionally, @see https://www.robvanderwoude.com/escapechars.php for why
22
+ we avoid trying to escape complex sequences in Windows.
23
+ */
24
+ export const escapeArgForShell = (arg) => {
25
+ let result = arg;
26
+
27
+ if (/[^A-Za-z0-9_/:=-]/.test(result)) {
28
+ if (isWin) {
29
+ return `"${result}"`;
30
+ } else {
31
+ result = "'" + result.replace(/'/g, "'\\''") + "'";
32
+ result = result
33
+ .replace(/^(?:'')+/g, "") // unduplicate single-quote at the beginning
34
+ .replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
35
+ }
36
+ }
37
+
38
+ return result;
39
+ };
40
+
41
+ export const getTempPath = (path) => {
42
+ return `${path}.tmp`;
43
+ };
44
+
45
+ export const prepareOutputPath = (outputPath) => {
46
+ const outputPathSegments = outputPath.split(path.sep);
47
+ outputPathSegments.pop();
48
+
49
+ const directoryOutputPath = outputPathSegments.join(path.sep);
50
+
51
+ if (directoryOutputPath.length) {
52
+ fs.mkdirSync(directoryOutputPath, { recursive: true });
53
+ }
54
+ };
55
+
56
+ export const getPublicObject = (object, exclude = []) => {
57
+ const output = {};
58
+ Object.keys(object).forEach((key) => {
59
+ if (!key.startsWith("_") && !exclude.includes(key) && object[key]) {
60
+ output[key] = object[key];
61
+ }
62
+ });
63
+
64
+ return output;
65
+ };
66
+
67
+ export const getFileString = (filePath) => {
68
+ const fullPath = path.resolve(process.cwd(), filePath);
69
+
70
+ if (!fs.existsSync(fullPath)) {
71
+ return null;
72
+ }
73
+
74
+ const data = fs.readFileSync(fullPath, "utf8");
75
+
76
+ if (!data) {
77
+ return null;
78
+ }
79
+
80
+ return data;
81
+ };
82
+
83
+ export const getJsonFile = (filePath) => {
84
+ const fullPath = path.resolve(process.cwd(), filePath);
85
+
86
+ if (!fs.existsSync(fullPath)) {
87
+ return null;
88
+ }
89
+
90
+ const data = fs.readFileSync(fullPath);
91
+
92
+ if (!data) {
93
+ return null;
94
+ }
95
+
96
+ return JSON.parse(data);
97
+ };
98
+
99
+ export const getLoopControls = ({ offset, length, reverse }) => {
100
+ if (reverse) {
101
+ const startIndex = length - 1 - offset;
102
+ const min = -1;
103
+ const shouldGo = (i) => i > min;
104
+ const decrement = (i) => i - 1;
105
+
106
+ return {
107
+ startIndex,
108
+ shouldGo,
109
+ next: decrement,
110
+ };
111
+ }
112
+
113
+ const startIndex = 0 + offset;
114
+ const max = length;
115
+ const shouldGo = (i) => i < max;
116
+ const increment = (i) => i + 1;
117
+
118
+ return {
119
+ startIndex,
120
+ shouldGo,
121
+ next: increment,
122
+ };
123
+ };
124
+
125
+ export const logFeedInfo = (feed) => {
126
+ logMessage(feed.title);
127
+ logMessage(feed.description);
128
+ logMessage();
129
+ };
130
+
131
+ export const getUrlExt = (url) => {
132
+ if (!url) {
133
+ return "";
134
+ }
135
+
136
+ const { pathname } = new URL(url);
137
+
138
+ if (!pathname) {
139
+ return "";
140
+ }
141
+
142
+ const ext = path.extname(pathname);
143
+ return ext;
144
+ };
145
+
146
+ export const AUDIO_TYPES_TO_EXTS = {
147
+ "audio/aac": ".aac",
148
+ "audio/flac": ".flac",
149
+ "audio/mp3": ".mp3",
150
+ "audio/mp4": ".m4a",
151
+ "audio/mpeg": ".mp3",
152
+ "audio/ogg": ".ogg",
153
+ "audio/opus": ".opus",
154
+ "audio/vorbis": ".ogg",
155
+ "audio/wav": ".wav",
156
+ "audio/x-m4a": ".m4a",
157
+ "audio/x-wav": ".wav",
158
+ "video/mp4": ".mp4",
159
+ "video/quicktime": ".mov",
160
+ "video/x-m4v": ".m4v",
161
+ };
162
+
163
+ export const VALID_AUDIO_EXTS = [
164
+ ...new Set(Object.values(AUDIO_TYPES_TO_EXTS)),
165
+ ];
166
+
167
+ export const getIsAudioUrl = (url) => {
168
+ let ext;
169
+ try {
170
+ ext = getUrlExt(url);
171
+ } catch (err) {
172
+ return false;
173
+ }
174
+
175
+ if (!ext) {
176
+ return false;
177
+ }
178
+
179
+ return VALID_AUDIO_EXTS.includes(ext);
180
+ };
181
+
182
+ export const AUDIO_ORDER_TYPES = {
183
+ enclosure: "enclosure",
184
+ link: "link",
185
+ };
186
+
187
+ export const getEpisodeAudioUrlAndExt = (
188
+ { enclosure, link },
189
+ order = [AUDIO_ORDER_TYPES.enclosure, AUDIO_ORDER_TYPES.link]
190
+ ) => {
191
+ for (const source of order) {
192
+ if (source === AUDIO_ORDER_TYPES.link && link && getIsAudioUrl(link)) {
193
+ return { url: link, ext: getUrlExt(link) };
194
+ }
195
+
196
+ if (source === AUDIO_ORDER_TYPES.enclosure && enclosure) {
197
+ if (getIsAudioUrl(enclosure.url)) {
198
+ return { url: enclosure.url, ext: getUrlExt(enclosure.url) };
199
+ }
200
+
201
+ if (enclosure.url && AUDIO_TYPES_TO_EXTS[enclosure.type]) {
202
+ return { url: enclosure.url, ext: AUDIO_TYPES_TO_EXTS[enclosure.type] };
203
+ }
204
+ }
205
+ }
206
+
207
+ return { url: null, ext: null };
208
+ };
209
+
210
+ export const getImageUrl = ({ image, itunes }) => {
211
+ if (image?.url && typeof image.url === "string") {
212
+ return image.url;
213
+ }
214
+
215
+ if (image?.link && typeof image.link === "string") {
216
+ return image.link;
217
+ }
218
+
219
+ if (itunes?.image && typeof itunes.image === "string") {
220
+ return itunes.image;
221
+ }
222
+
223
+ return null;
224
+ };
225
+
226
+ export const TRANSCRIPT_TYPES = {
227
+ "application/json": "application/json",
228
+ "application/srr": "application/srr",
229
+ "application/srt": "application/srt",
230
+ "application/x-subrip": "application/x-subrip",
231
+ "text/html": "text/html",
232
+ "text/plain": "text/plain",
233
+ "text/vtt": "text/vtt",
234
+ };
235
+
236
+ // @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#transcript
237
+ export const getTranscriptUrl = (item, transcriptTypes = []) => {
238
+ if (!item.podcastTranscripts?.length) {
239
+ return null;
240
+ }
241
+
242
+ for (const transcriptType of transcriptTypes) {
243
+ const matchingTranscriptType = item.podcastTranscripts.find(
244
+ (transcript) =>
245
+ !!transcript?.["$"]?.url && transcript?.["$"]?.type === transcriptType
246
+ );
247
+
248
+ if (matchingTranscriptType) {
249
+ return matchingTranscriptType?.["$"]?.url;
250
+ }
251
+ }
252
+
253
+ return null;
254
+ };
255
+
256
+ export const getFileFeed = async (filePath, parserConfig) => {
257
+ const config = parserConfig
258
+ ? getJsonFile(parserConfig)
259
+ : defaultRssParserConfig;
260
+ const rssString = getFileString(filePath);
261
+
262
+ if (parserConfig && !config) {
263
+ logErrorAndExit(`Unable to load parser config: ${parserConfig}`);
264
+ }
265
+
266
+ const parser = new rssParser(config);
267
+
268
+ let feed;
269
+ try {
270
+ feed = await parser.parseString(rssString);
271
+ } catch (err) {
272
+ logErrorAndExit("Unable to parse local RSS file", err);
273
+ }
274
+
275
+ return feed;
276
+ };
277
+
278
+ export const getUrlFeed = async (url, parserConfig) => {
279
+ const config = parserConfig
280
+ ? getJsonFile(parserConfig)
281
+ : defaultRssParserConfig;
282
+
283
+ if (parserConfig && !config) {
284
+ logErrorAndExit(`Unable to load parser config: ${parserConfig}`);
285
+ }
286
+
287
+ const parser = new rssParser(config);
288
+
289
+ const { href } = new URL(url);
290
+
291
+ let feed;
292
+ try {
293
+ feed = await parser.parseURL(href);
294
+ } catch (err) {
295
+ logErrorAndExit("Unable to parse RSS URL", err);
296
+ }
297
+
298
+ return feed;
299
+ };
package/bin/validate.js CHANGED
@@ -1,39 +1,39 @@
1
- import { sync as commandExistsSync } from "command-exists";
2
- import { logErrorAndExit } from "./logger.js";
3
-
4
- export const createParseNumber = ({ min, max, name, required = true }) => {
5
- return (value) => {
6
- if (!value && !required) {
7
- return undefined;
8
- }
9
-
10
- try {
11
- let number = parseInt(value);
12
- if (isNaN(number)) {
13
- logErrorAndExit(`${name} must be a number`);
14
- }
15
-
16
- if (typeof min !== undefined && number < min) {
17
- logErrorAndExit(`${name} must be >= ${min}`);
18
- }
19
-
20
- if (typeof max !== undefined && number > max) {
21
- logErrorAndExit(
22
- `${name} must be <= ${
23
- max === Number.MAX_SAFE_INTEGER ? "Number.MAX_SAFE_INTEGER" : max
24
- }`
25
- );
26
- }
27
-
28
- return number;
29
- } catch (error) {
30
- logErrorAndExit(`Unable to parse ${name}`);
31
- }
32
- };
33
- };
34
-
35
- export const hasFfmpeg = () => {
36
- if (!commandExistsSync("ffmpeg")) {
37
- logErrorAndExit('option specified requires "ffmpeg" be available');
38
- }
39
- };
1
+ import { sync as commandExistsSync } from "command-exists";
2
+ import { logErrorAndExit } from "./logger.js";
3
+
4
+ export const createParseNumber = ({ min, max, name, required = true }) => {
5
+ return (value) => {
6
+ if (!value && !required) {
7
+ return undefined;
8
+ }
9
+
10
+ try {
11
+ let number = parseInt(value);
12
+ if (isNaN(number)) {
13
+ logErrorAndExit(`${name} must be a number`);
14
+ }
15
+
16
+ if (typeof min !== undefined && number < min) {
17
+ logErrorAndExit(`${name} must be >= ${min}`);
18
+ }
19
+
20
+ if (typeof max !== undefined && number > max) {
21
+ logErrorAndExit(
22
+ `${name} must be <= ${
23
+ max === Number.MAX_SAFE_INTEGER ? "Number.MAX_SAFE_INTEGER" : max
24
+ }`
25
+ );
26
+ }
27
+
28
+ return number;
29
+ } catch (error) {
30
+ logErrorAndExit(`Unable to parse ${name}`);
31
+ }
32
+ };
33
+ };
34
+
35
+ export const hasFfmpeg = () => {
36
+ if (!commandExistsSync("ffmpeg")) {
37
+ logErrorAndExit('option specified requires "ffmpeg" be available');
38
+ }
39
+ };