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/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 (err) {
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 (typeof min !== undefined && number < min) {
16
+ if (min !== undefined && number < min) {
17
17
  logErrorAndExit(`${name} must be >= ${min}`);
18
18
  }
19
19
 
20
- if (typeof max !== undefined && number > max) {
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 (error) {
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.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
- "@yao-pkg/pkg": "^6.6.0",
42
- "eslint": "^6.8.0",
43
- "eslint-config-prettier": "^6.11.0",
44
- "husky": "^4.2.5",
45
- "lint-staged": "^10.1.7",
46
- "prettier": "2.3.2",
47
- "rimraf": "^3.0.2",
48
- "webpack": "^5.75.0"
49
- },
50
- "dependencies": {
51
- "command-exists": "^1.2.9",
52
- "commander": "^12.1.0",
53
- "dayjs": "^1.8.25",
54
- "filenamify": "^6.0.0",
55
- "global-agent": "^3.0.0",
56
- "got": "^11.0.2",
57
- "p-limit": "^4.0.0",
58
- "pluralize": "^8.0.0",
59
- "rss-parser": "^3.12.0",
60
- "throttle-debounce": "^3.0.1"
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
+ }