podcast-dl 11.0.0 → 11.1.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/README.md CHANGED
@@ -24,41 +24,47 @@ Either `--url` or `--file` must be provided.
24
24
 
25
25
  Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required).
26
26
 
27
- | Option | Type | Required | Description |
28
- | --------------------------------- | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
- | --url | String | true\* | URL to podcast RSS feed. |
30
- | --file | String | true\* | Path to local RSS file. |
31
- | --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
32
- | --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
33
- | --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. |
34
- | --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
35
- | --episode-custom-template-options | <String...> | false | Provide custom options for the episode template. See "Template Options" for details. |
36
- | --include-meta | | false | Write out podcast metadata to JSON. |
37
- | --include-episode-meta | | false | Write out individual episode metadata **to** JSON. |
38
- | --include-episode-images | | false | Download found episode images. |
39
- | --include-episode-transcripts | | false | Download found episode transcripts. |
40
- | --offset | Number | false | Offset starting download position. Default is 0. |
41
- | --limit | Number | false | Max number of episodes to download. Downloads all by default. |
42
- | --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
43
- | --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
44
- | --episode-regex | String | false | Match episode title against provided regex before starting download. |
45
- | --episode-regex-exclude | String | false | Matched episode titles against provided regex will be excluded. |
46
- | --episode-digits | Number | false | Minimum number of digits to use for episode numbering (e.g. 3 would generate "001" instead of "1"). Default is 0. |
47
- | --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. |
48
- | --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". |
49
- | --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain". |
50
- | --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
51
- | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) |
52
- | --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) |
53
- | --override | | false | Override local files on collision. |
54
- | --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec. |
55
- | --reverse | | false | Reverse download direction and start at last RSS item. |
56
- | --info | | false | Print retrieved podcast info instead of downloading. |
57
- | --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
58
- | --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
59
- | --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). |
60
- | --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
61
- | --help | | false | Output usage information. |
27
+ | Option | Type | Required | Description |
28
+ | --------------------------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | --url | String | true\* | URL to podcast RSS feed. |
30
+ | --file | String | true\* | Path to local RSS file. |
31
+ | --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. |
32
+ | --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. |
33
+ | --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. |
34
+ | --archive | [String] | false | Download or write out items not listed in archive file. Generates archive file at path if not found. Defaults to "./{{podcast_title}}/archive.json" when used as a boolean option. See "Template Options" for more details. |
35
+ | --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. |
36
+ | --episode-custom-template-options | <String...> | false | Provide custom options for the episode template. See "Template Options" for details. |
37
+ | --include-meta | | false | Write out podcast metadata to JSON. |
38
+ | --include-episode-meta | | false | Write out individual episode metadata **to** JSON. |
39
+ | --include-episode-images | | false | Download found episode images. |
40
+ | --include-episode-transcripts | | false | Download found episode transcripts. |
41
+ | --offset | Number | false | Offset starting download position. Default is 0. |
42
+ | --limit | Number | false | Max number of episodes to download. Downloads all by default. |
43
+ | --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). |
44
+ | --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) |
45
+ | --episode-regex | String | false | Match episode title against provided regex before starting download. |
46
+ | --episode-regex-exclude | String | false | Matched episode titles against provided regex will be excluded. |
47
+ | --episode-digits | Number | false | Minimum number of digits to use for episode numbering (e.g. 3 would generate "001" instead of "1"). Default is 0. |
48
+ | --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. |
49
+ | --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". |
50
+ | --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain". |
51
+ | --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) |
52
+ | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) |
53
+ | --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) |
54
+ | --override | | false | Override local files on collision. |
55
+ | --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec. |
56
+ | --reverse | | false | Reverse download direction and start at last RSS item. |
57
+ | --info | | false | Print retrieved podcast info instead of downloading. |
58
+ | --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. |
59
+ | --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. |
60
+ | --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). |
61
+ | --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). |
62
+ | --help | | false | Output usage information. |
63
+
64
+ ## Archive
65
+
66
+ - If passed the `--archive [path]` option, `podcast-dl` will generate/use a JSON archive at the provided path.
67
+ - Before downloading an episode or writing out metadata, it'll check if the item was saved previously and abort the save if found.
62
68
 
63
69
  ## Template Options
64
70
 
@@ -68,7 +74,7 @@ Options that support templates allow users to specify a template for the generat
68
74
 
69
75
  `--episode-template "{{release_date}}-{{title}}"`
70
76
 
71
- ### `--out-dir`
77
+ ### `--out-dir` & `--archive`
72
78
 
73
79
  - `podcast_title`: Title of the podcast feed.
74
80
  - `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL.
package/bin/archive.js ADDED
@@ -0,0 +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
+ };
package/bin/async.js CHANGED
@@ -5,6 +5,12 @@ import _path from "path";
5
5
  import stream from "stream";
6
6
  import { throttle } from "throttle-debounce";
7
7
  import { promisify } from "util";
8
+ import {
9
+ getArchiveFilename,
10
+ getArchiveKey,
11
+ getIsInArchive,
12
+ writeToArchive,
13
+ } from "./archive.js";
8
14
  import { runExec } from "./exec.js";
9
15
  import { runFfmpeg } from "./ffmpeg.js";
10
16
  import {
@@ -32,6 +38,8 @@ export const download = async (options) => {
32
38
  marker,
33
39
  url,
34
40
  outputPath,
41
+ key,
42
+ archive,
35
43
  override,
36
44
  alwaysPostprocess,
37
45
  onAfterDownload,
@@ -50,6 +58,11 @@ export const download = async (options) => {
50
58
  return;
51
59
  }
52
60
 
61
+ if (key && archive && getIsInArchive({ key, archive })) {
62
+ logMessage("Download exists in archive. Skipping...");
63
+ return;
64
+ }
65
+
53
66
  let headResponse = null;
54
67
  try {
55
68
  headResponse = await got(url, {
@@ -140,10 +153,20 @@ export const download = async (options) => {
140
153
  if (onAfterDownload) {
141
154
  await onAfterDownload();
142
155
  }
156
+
157
+ if (key && archive) {
158
+ try {
159
+ writeToArchive({ key, archive });
160
+ } catch (error) {
161
+ throw new Error(`Error writing to archive: ${error.toString()}`);
162
+ }
163
+ }
143
164
  };
144
165
 
145
166
  export const downloadItemsAsync = async ({
146
167
  addMp3MetadataFlag,
168
+ archive,
169
+ archivePrefix,
147
170
  attempts,
148
171
  basePath,
149
172
  bitrate,
@@ -195,9 +218,18 @@ export const downloadItemsAsync = async ({
195
218
 
196
219
  try {
197
220
  await download({
221
+ archive,
198
222
  override,
199
223
  alwaysPostprocess,
200
224
  marker,
225
+ key: getArchiveKey({
226
+ prefix: archivePrefix,
227
+ name: getArchiveFilename({
228
+ name: item.title,
229
+ pubDate: item.pubDate,
230
+ ext: audioFileExt,
231
+ }),
232
+ }),
201
233
  maxAttempts: attempts,
202
234
  outputPath: outputPodcastPath,
203
235
  url: episodeAudioUrl,
@@ -205,7 +237,9 @@ export const downloadItemsAsync = async ({
205
237
  if (item._episodeImage) {
206
238
  try {
207
239
  await download({
240
+ archive,
208
241
  override,
242
+ key: item._episodeImage.key,
209
243
  marker: item._episodeImage.url,
210
244
  maxAttempts: attempts,
211
245
  outputPath: item._episodeImage.outputPath,
@@ -224,7 +258,9 @@ export const downloadItemsAsync = async ({
224
258
  if (item._episodeTranscript) {
225
259
  try {
226
260
  await download({
261
+ archive,
227
262
  override,
263
+ key: item._episodeTranscript.key,
228
264
  marker: item._episodeTranscript.url,
229
265
  maxAttempts: attempts,
230
266
  outputPath: item._episodeTranscript.outputPath,
@@ -296,8 +332,17 @@ export const downloadItemsAsync = async ({
296
332
  logMessage("Saving episode metadata...");
297
333
  writeItemMeta({
298
334
  marker,
335
+ archive,
299
336
  override,
300
337
  item,
338
+ key: getArchiveKey({
339
+ prefix: archivePrefix,
340
+ name: getArchiveFilename({
341
+ pubDate: item.pubDate,
342
+ name: item.title,
343
+ ext: episodeMetaExt,
344
+ }),
345
+ }),
301
346
  outputPath: outputEpisodeMetaPath,
302
347
  });
303
348
  } catch (error) {
package/bin/bin.js CHANGED
@@ -5,6 +5,7 @@ import fs from "fs";
5
5
  import { bootstrap as bootstrapProxy } from "global-agent";
6
6
  import _path from "path";
7
7
  import pluralize from "pluralize";
8
+ import { getArchiveKey } from "./archive.js";
8
9
  import { download, downloadItemsAsync } from "./async.js";
9
10
  import { setupCommander } from "./commander.js";
10
11
  import { getItemsToDownload, logItemsList } from "./items.js";
@@ -62,6 +63,8 @@ const {
62
63
  adjustBitrate: bitrate,
63
64
  } = opts;
64
65
 
66
+ let { archive } = opts;
67
+
65
68
  const main = async () => {
66
69
  if (!url && !file) {
67
70
  logErrorAndExit("No URL or file location provided");
@@ -79,6 +82,15 @@ const main = async () => {
79
82
  ? await getUrlFeed(url, parserConfig)
80
83
  : await getFileFeed(file, parserConfig);
81
84
 
85
+ const archivePrefix = (() => {
86
+ if (feed.feedUrl || url) {
87
+ const { hostname, pathname } = new URL(feed.feedUrl || url);
88
+ return `${hostname}${pathname}`;
89
+ }
90
+
91
+ return feed.title || file;
92
+ })();
93
+
82
94
  const basePath = _path.resolve(
83
95
  process.cwd(),
84
96
  getFolderName({ feed, template: outDir })
@@ -118,6 +130,14 @@ const main = async () => {
118
130
  fs.mkdirSync(basePath, { recursive: true });
119
131
  }
120
132
 
133
+ if (archive) {
134
+ archive =
135
+ typeof archive === "boolean"
136
+ ? "./{{podcast_title}}/archive.json"
137
+ : archive;
138
+ archive = getFolderName({ feed, template: archive });
139
+ }
140
+
121
141
  if (includeMeta) {
122
142
  const podcastImageUrl = getImageUrl(feed);
123
143
 
@@ -134,8 +154,15 @@ const main = async () => {
134
154
  try {
135
155
  logMessage("\nDownloading podcast image...");
136
156
  await download({
157
+ archive,
137
158
  override,
138
159
  marker: podcastImageUrl,
160
+ key: getArchiveKey({
161
+ prefix: archivePrefix,
162
+ name: `${
163
+ feed.title ? `${feed.title}.image` : "image"
164
+ }${podcastImageFileExt}`,
165
+ }),
139
166
  outputPath: outputImagePath,
140
167
  url: podcastImageUrl,
141
168
  maxAttempts: attempts,
@@ -156,8 +183,13 @@ const main = async () => {
156
183
  try {
157
184
  logMessage("\nSaving podcast metadata...");
158
185
  writeFeedMeta({
186
+ archive,
159
187
  override,
160
188
  feed,
189
+ key: getArchiveKey({
190
+ prefix: archivePrefix,
191
+ name: `${feed.title ? `${feed.title}.meta` : "meta"}.json`,
192
+ }),
161
193
  outputPath: outputMetaPath,
162
194
  });
163
195
  } catch (error) {
@@ -174,6 +206,8 @@ const main = async () => {
174
206
  }
175
207
 
176
208
  const targetItems = getItemsToDownload({
209
+ archive,
210
+ archivePrefix,
177
211
  addMp3MetadataFlag,
178
212
  basePath,
179
213
  feed,
@@ -204,6 +238,8 @@ const main = async () => {
204
238
 
205
239
  const { numEpisodesDownloaded, hasErrors } = await downloadItemsAsync({
206
240
  addMp3MetadataFlag,
241
+ archive,
242
+ archivePrefix,
207
243
  attempts,
208
244
  basePath,
209
245
  bitrate,
package/bin/commander.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { ITEM_LIST_FORMATS } from "./items.js";
2
+ import { logErrorAndExit } from "./logger.js";
1
3
  import { AUDIO_ORDER_TYPES, TRANSCRIPT_TYPES } from "./util.js";
2
4
  import { createParseNumber, hasFfmpeg } from "./validate.js";
3
- import { logErrorAndExit } from "./logger.js";
4
- import { ITEM_LIST_FORMATS } from "./items.js";
5
5
 
6
6
  export const setupCommander = (program) => {
7
7
  program
@@ -12,6 +12,10 @@ export const setupCommander = (program) => {
12
12
  "specify output directory",
13
13
  "./{{podcast_title}}"
14
14
  )
15
+ .option(
16
+ "--archive [path]",
17
+ "download or write only items not listed in archive file"
18
+ )
15
19
  .option(
16
20
  "--episode-template <string>",
17
21
  "template for generating episode related filenames",
package/bin/items.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import dayjs from "dayjs";
2
2
  import path from "path";
3
+ import { getArchive, getArchiveFilename, getArchiveKey } from "./archive.js";
4
+ import { logErrorAndExit } from "./logger.js";
3
5
  import { getItemFilename } from "./naming.js";
4
6
  import {
5
7
  getEpisodeAudioUrlAndExt,
@@ -8,11 +10,12 @@ import {
8
10
  getTranscriptUrl,
9
11
  getUrlExt,
10
12
  } from "./util.js";
11
- import { logErrorAndExit } from "./logger.js";
12
13
 
13
14
  export const ITEM_LIST_FORMATS = ["table", "json"];
14
15
 
15
16
  export const getItemsToDownload = ({
17
+ archive,
18
+ archivePrefix,
16
19
  addMp3MetadataFlag,
17
20
  basePath,
18
21
  feed,
@@ -41,6 +44,8 @@ export const getItemsToDownload = ({
41
44
  let i = startIndex;
42
45
  const items = [];
43
46
 
47
+ const savedArchive = archive ? getArchive(archive) : [];
48
+
44
49
  while (shouldGo(i)) {
45
50
  const { title, pubDate } = feed.items[i];
46
51
  const pubDateDay = dayjs(new Date(pubDate));
@@ -80,10 +85,21 @@ export const getItemsToDownload = ({
80
85
  }
81
86
  }
82
87
 
83
- const { url: episodeAudioUrl } = getEpisodeAudioUrlAndExt(
84
- feed.items[i],
85
- episodeSourceOrder
86
- );
88
+ const { url: episodeAudioUrl, ext: audioFileExt } =
89
+ getEpisodeAudioUrlAndExt(feed.items[i], episodeSourceOrder);
90
+
91
+ const key = getArchiveKey({
92
+ prefix: archivePrefix,
93
+ name: getArchiveFilename({
94
+ pubDate,
95
+ name: title,
96
+ ext: audioFileExt,
97
+ }),
98
+ });
99
+
100
+ if (key && savedArchive.includes(key)) {
101
+ isValid = false;
102
+ }
87
103
 
88
104
  if (isValid) {
89
105
  const item = feed.items[i];
@@ -94,6 +110,14 @@ export const getItemsToDownload = ({
94
110
 
95
111
  if (episodeImageUrl) {
96
112
  const episodeImageFileExt = getUrlExt(episodeImageUrl);
113
+ const episodeImageArchiveKey = getArchiveKey({
114
+ prefix: archivePrefix,
115
+ name: getArchiveFilename({
116
+ pubDate,
117
+ name: title,
118
+ ext: episodeImageFileExt,
119
+ }),
120
+ });
97
121
 
98
122
  const episodeImageName = getItemFilename({
99
123
  item,
@@ -110,6 +134,7 @@ export const getItemsToDownload = ({
110
134
  item._episodeImage = {
111
135
  url: episodeImageUrl,
112
136
  outputPath: outputImagePath,
137
+ key: episodeImageArchiveKey,
113
138
  };
114
139
  }
115
140
  }
@@ -122,6 +147,14 @@ export const getItemsToDownload = ({
122
147
 
123
148
  if (episodeTranscriptUrl) {
124
149
  const episodeTranscriptFileExt = getUrlExt(episodeTranscriptUrl);
150
+ const episodeTranscriptArchiveKey = getArchiveKey({
151
+ prefix: archivePrefix,
152
+ name: getArchiveFilename({
153
+ pubDate,
154
+ name: title,
155
+ ext: episodeTranscriptFileExt,
156
+ }),
157
+ });
125
158
 
126
159
  const episodeTranscriptName = getItemFilename({
127
160
  item,
@@ -141,6 +174,7 @@ export const getItemsToDownload = ({
141
174
  item._episodeTranscript = {
142
175
  url: episodeTranscriptUrl,
143
176
  outputPath: outputTranscriptPath,
177
+ key: episodeTranscriptArchiveKey,
144
178
  };
145
179
  }
146
180
  }
package/bin/meta.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import fs from "fs";
2
+ import { getIsInArchive, writeToArchive } from "./archive.js";
2
3
  import { logMessage } from "./logger.js";
3
4
  import { getPublicObject } from "./util.js";
4
5
 
5
- export const writeFeedMeta = ({ outputPath, feed, override }) => {
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
+ }
6
11
  const output = getPublicObject(feed, ["items"]);
7
12
 
8
13
  try {
@@ -11,6 +16,14 @@ export const writeFeedMeta = ({ outputPath, feed, override }) => {
11
16
  } else {
12
17
  logMessage("Feed metadata exists locally. Skipping...");
13
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
+ }
14
27
  } catch (error) {
15
28
  throw new Error(
16
29
  `Unable to save metadata file for feed: ${error.toString()}`
@@ -18,7 +31,19 @@ export const writeFeedMeta = ({ outputPath, feed, override }) => {
18
31
  }
19
32
  };
20
33
 
21
- export const writeItemMeta = ({ marker, outputPath, item, override }) => {
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
+
22
47
  const output = getPublicObject(item);
23
48
 
24
49
  try {
@@ -27,6 +52,14 @@ export const writeItemMeta = ({ marker, outputPath, item, override }) => {
27
52
  } else {
28
53
  logMessage(`${marker} | Episode metadata exists locally. Skipping...`);
29
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
+ }
30
63
  } catch (error) {
31
64
  throw new Error("Unable to save meta file for episode", error);
32
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "podcast-dl",
3
- "version": "11.0.0",
3
+ "version": "11.1.0",
4
4
  "description": "A CLI for downloading podcasts.",
5
5
  "type": "module",
6
6
  "bin": "./bin/bin.js",
@@ -26,7 +26,7 @@
26
26
  "cli"
27
27
  ],
28
28
  "engines": {
29
- "node": ">=22.16.0"
29
+ "node": ">=18.17.0"
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",