podcast-dl 11.5.0 → 11.6.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
@@ -49,11 +49,12 @@ Type values surrounded in square brackets (`[]`) can be used as boolean options
49
49
  | --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is `"enclosure,link"`. |
50
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
51
  | --season | Number | false | Only download episodes from specified season. Note: this will only work if the RSS feed includes the `itunes:season` tag on episodes. |
52
- | --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**) |
53
- | --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) |
54
- | --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) |
52
+ | --embed-metadata | | false | Add metadata to episode files. (**ffmpeg required**) |
53
+ | --audio-format | String | false | Convert audio to format: mp3, m4a, aac, opus, ogg, flac, wav. (**ffmpeg required**) |
54
+ | --adjust-bitrate | String (e.g. "48k") | false | Adjust bitrate of episodes. (**ffmpeg required**) |
55
+ | --mono | | false | Force episodes into mono. (**ffmpeg required**) |
55
56
  | --override | | false | Override local files on collision. |
56
- | --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`. |
57
+ | --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes `--embed-metadata`, `--audio-format`, `--adjust-bitrate`, `--mono`, and `--exec`. |
57
58
  | --reverse | | false | Reverse download direction and start at last RSS item. |
58
59
  | --info | | false | Print retrieved podcast info instead of downloading. |
59
60
  | --list | [String] | false | Print episode list instead of downloading. Defaults to `"table"` when used as a boolean option. `"json"` is also supported. |
package/bin/async.js CHANGED
@@ -182,12 +182,13 @@ export const download = async (options) => {
182
182
  };
183
183
 
184
184
  export const downloadItemsAsync = async ({
185
- addMp3MetadataFlag,
186
185
  archive,
187
186
  archivePrefix,
188
187
  attempts,
188
+ audioFormat,
189
189
  basePath,
190
190
  bitrate,
191
+ embedMetadataFlag,
191
192
  episodeTemplate,
192
193
  episodeCustomTemplateOptions,
193
194
  episodeDigits,
@@ -313,20 +314,21 @@ export const downloadItemsAsync = async ({
313
314
  const hasEpisodeImage =
314
315
  item._episodeImage && fs.existsSync(item._episodeImage.outputPath);
315
316
 
316
- if (addMp3MetadataFlag || bitrate || mono) {
317
+ if (embedMetadataFlag || bitrate || mono || audioFormat) {
317
318
  logMessage("Running ffmpeg...");
318
319
  await runFfmpeg({
319
- feed,
320
- item,
320
+ audioFormat,
321
321
  bitrate,
322
- mono,
323
- itemIndex: item._originalIndex,
324
- outputPath: finalEpisodePath,
322
+ embedMetadata: embedMetadataFlag,
325
323
  episodeImageOutputPath: hasEpisodeImage
326
324
  ? item._episodeImage.outputPath
327
325
  : undefined,
328
- addMp3Metadata: addMp3MetadataFlag,
329
326
  ext: audioFileExt,
327
+ feed,
328
+ item,
329
+ itemIndex: item._originalIndex,
330
+ mono,
331
+ outputPath: finalEpisodePath,
330
332
  });
331
333
  }
332
334
 
package/bin/bin.js CHANGED
@@ -32,6 +32,7 @@ const {
32
32
  after,
33
33
  alwaysPostprocess,
34
34
  attempts,
35
+ audioFormat,
35
36
  before,
36
37
  episodeDigits,
37
38
  episodeNumOffset,
@@ -60,12 +61,15 @@ const {
60
61
  threads,
61
62
  url,
62
63
  userAgent,
63
- addMp3Metadata: addMp3MetadataFlag,
64
+ embedMetadata,
65
+ addMp3Metadata,
64
66
  adjustBitrate: bitrate,
65
67
  season,
66
68
  trustExt,
67
69
  } = opts;
68
70
 
71
+ const embedMetadataFlag = embedMetadata || addMp3Metadata;
72
+
69
73
  let { archive } = opts;
70
74
 
71
75
  const main = async () => {
@@ -209,7 +213,7 @@ const main = async () => {
209
213
  const targetItems = getItemsToDownload({
210
214
  archive,
211
215
  archivePrefix,
212
- addMp3MetadataFlag,
216
+ embedMetadataFlag,
213
217
  basePath,
214
218
  feed,
215
219
  limit,
@@ -239,12 +243,13 @@ const main = async () => {
239
243
  );
240
244
 
241
245
  const { numEpisodesDownloaded, hasErrors } = await downloadItemsAsync({
242
- addMp3MetadataFlag,
243
246
  archive,
244
247
  archivePrefix,
245
248
  attempts,
249
+ audioFormat,
246
250
  basePath,
247
251
  bitrate,
252
+ embedMetadataFlag,
248
253
  episodeTemplate,
249
254
  episodeCustomTemplateOptions,
250
255
  episodeDigits,
package/bin/commander.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ITEM_LIST_FORMATS } from "./items.js";
2
2
  import { logErrorAndExit } from "./logger.js";
3
- import { AUDIO_ORDER_TYPES, TRANSCRIPT_TYPES } from "./util.js";
3
+ import { AUDIO_FORMATS, AUDIO_ORDER_TYPES, TRANSCRIPT_TYPES } from "./util.js";
4
4
  import { createParseNumber, hasFfmpeg } from "./validate.js";
5
5
 
6
6
  export const setupCommander = (program) => {
@@ -126,20 +126,36 @@ export const setupCommander = (program) => {
126
126
  "download episodes only before this date (inclusive)"
127
127
  )
128
128
  .option(
129
- "--add-mp3-metadata",
130
- "attempts to add a base level of metadata to episode files using ffmpeg",
129
+ "--embed-metadata",
130
+ "add metadata to episode files using ffmpeg",
131
131
  hasFfmpeg
132
132
  )
133
133
  .option(
134
- "--adjust-bitrate <string>",
135
- "attempts to adjust bitrate of episode files using ffmpeg",
134
+ "--add-mp3-metadata",
135
+ "deprecated: use --embed-metadata instead",
136
136
  hasFfmpeg
137
137
  )
138
138
  .option(
139
- "--mono",
140
- "attempts to force episode files into mono using ffmpeg",
139
+ "--audio-format <string>",
140
+ "convert audio to format (mp3, m4a, aac, opus, ogg, flac, wav)",
141
+ (value) => {
142
+ if (!AUDIO_FORMATS[value]) {
143
+ logErrorAndExit(
144
+ `Invalid audio format: ${value}\nSupported formats: ${Object.keys(
145
+ AUDIO_FORMATS
146
+ ).join(", ")}`
147
+ );
148
+ }
149
+
150
+ return hasFfmpeg(value);
151
+ }
152
+ )
153
+ .option(
154
+ "--adjust-bitrate <string>",
155
+ "adjust bitrate of episode files using ffmpeg",
141
156
  hasFfmpeg
142
157
  )
158
+ .option("--mono", "force episode files into mono using ffmpeg", hasFfmpeg)
143
159
  .option("--override", "override local files on collision")
144
160
  .option(
145
161
  "--always-postprocess",
package/bin/ffmpeg.js CHANGED
@@ -1,105 +1,138 @@
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 { AUDIO_FORMATS, escapeArgForShell, isWin } from "./util.js";
6
+
7
+ export const runFfmpeg = async ({
8
+ audioFormat,
9
+ bitrate,
10
+ embedMetadata,
11
+ episodeImageOutputPath,
12
+ ext,
13
+ feed,
14
+ item,
15
+ itemIndex,
16
+ mono,
17
+ outputPath,
18
+ }) => {
19
+ if (!fs.existsSync(outputPath)) {
20
+ return;
21
+ }
22
+
23
+ const shouldEmbedImage = embedMetadata && episodeImageOutputPath;
24
+ const targetFormat = audioFormat ? AUDIO_FORMATS[audioFormat] : null;
25
+ const outputExt = targetFormat ? targetFormat.ext : ext;
26
+
27
+ let command = `ffmpeg -loglevel quiet -i ${escapeArgForShell(outputPath)}`;
28
+
29
+ if (shouldEmbedImage) {
30
+ command += ` -i ${escapeArgForShell(episodeImageOutputPath)}`;
31
+ }
32
+
33
+ if (bitrate) {
34
+ command += ` -b:a ${bitrate}`;
35
+ }
36
+
37
+ if (mono) {
38
+ command += " -ac 1";
39
+ }
40
+
41
+ if (targetFormat) {
42
+ command += ` -c:a ${targetFormat.codec}`;
43
+ } else if (embedMetadata && !bitrate && !mono) {
44
+ command += ` -c:a copy`;
45
+ }
46
+
47
+ if (shouldEmbedImage) {
48
+ const supportsAttachedPic = targetFormat
49
+ ? targetFormat.attachedPic
50
+ : Object.values(AUDIO_FORMATS).find((f) => f.ext === ext)?.attachedPic;
51
+ command += supportsAttachedPic
52
+ ? ` -c:v copy -disposition:v:0 attached_pic`
53
+ : ` -c:v copy`;
54
+ }
55
+
56
+ if (embedMetadata) {
57
+ const album = feed.title || "";
58
+ const artist = item.itunes?.author || item.author || "";
59
+ const title = item.title || "";
60
+ const subtitle = item.itunes?.subtitle || "";
61
+ const comment = item.contentSnippet || item.content || "";
62
+ const disc = item.itunes?.season || "";
63
+ const track = item.itunes?.episode || `${feed.items.length - itemIndex}`;
64
+ const episodeType = item.itunes?.episodeType || "";
65
+ const date = item.pubDate
66
+ ? dayjs(new Date(item.pubDate)).format("YYYY-MM-DD")
67
+ : "";
68
+
69
+ const metaKeysToValues = {
70
+ album,
71
+ artist,
72
+ album_artist: artist,
73
+ title,
74
+ disc,
75
+ track,
76
+ "episode-type": episodeType,
77
+ date,
78
+ };
79
+
80
+ if (!isWin) {
81
+ // Due to limited escape options, these metadata fields often break in Windows
82
+ metaKeysToValues.comment = comment;
83
+ metaKeysToValues.subtitle = subtitle;
84
+ }
85
+
86
+ const metadataString = Object.keys(metaKeysToValues)
87
+ .map((key) => {
88
+ if (!metaKeysToValues[key]) {
89
+ return null;
90
+ }
91
+
92
+ const argValue = escapeArgForShell(metaKeysToValues[key]);
93
+
94
+ return argValue ? `-metadata ${key}=${argValue}` : null;
95
+ })
96
+ .filter((segment) => !!segment)
97
+ .join(" ");
98
+
99
+ command += ` -map_metadata 0 ${metadataString}`;
100
+ }
101
+
102
+ if (shouldEmbedImage) {
103
+ command += ` -map 0:a -map 1`;
104
+ } else if (targetFormat) {
105
+ command += ` -map 0:a`;
106
+ } else {
107
+ command += ` -map 0`;
108
+ }
109
+
110
+ const tmpPath = `${outputPath}.tmp${outputExt}`;
111
+ command += ` ${escapeArgForShell(tmpPath)}`;
112
+ logMessage("Running command: " + command, LOG_LEVELS.debug);
113
+
114
+ try {
115
+ await execWithPromise(command, { stdio: "ignore" });
116
+ } catch (error) {
117
+ if (fs.existsSync(tmpPath)) {
118
+ fs.unlinkSync(tmpPath);
119
+ }
120
+
121
+ throw error;
122
+ }
123
+
124
+ fs.unlinkSync(outputPath);
125
+
126
+ const finalOutputPath = (() => {
127
+ if (!targetFormat) {
128
+ return outputPath;
129
+ }
130
+
131
+ const hasExt = /\.[^.]+$/.test(outputPath);
132
+ return hasExt
133
+ ? outputPath.replace(/\.[^.]+$/, outputExt)
134
+ : outputPath + outputExt;
135
+ })();
136
+
137
+ fs.renameSync(tmpPath, finalOutputPath);
138
+ };
package/bin/items.js CHANGED
@@ -1,247 +1,247 @@
1
- import dayjs from "dayjs";
2
- import path from "path";
3
- import { getArchive, getArchiveFilename, getArchiveKey } from "./archive.js";
4
- import { logErrorAndExit } from "./logger.js";
5
- import { getItemFilename } from "./naming.js";
6
- import {
7
- getEpisodeAudioUrlAndExt,
8
- getImageUrl,
9
- getLoopControls,
10
- getTranscriptUrl,
11
- getUrlExt,
12
- } from "./util.js";
13
-
14
- export const ITEM_LIST_FORMATS = ["table", "json"];
15
-
16
- export const getItemsToDownload = ({
17
- archive,
18
- archivePrefix,
19
- addMp3MetadataFlag,
20
- basePath,
21
- feed,
22
- limit,
23
- offset,
24
- reverse,
25
- before,
26
- after,
27
- episodeDigits,
28
- episodeNumOffset,
29
- episodeRegex,
30
- episodeRegexExclude,
31
- episodeSourceOrder,
32
- episodeTemplate,
33
- episodeCustomTemplateOptions,
34
- includeEpisodeImages,
35
- includeEpisodeTranscripts,
36
- episodeTranscriptTypes,
37
- season,
38
- }) => {
39
- const { startIndex, shouldGo, next } = getLoopControls({
40
- offset,
41
- reverse,
42
- length: feed.items.length,
43
- });
44
-
45
- let i = startIndex;
46
- const items = [];
47
-
48
- const savedArchive = archive ? getArchive(archive) : [];
49
-
50
- while (shouldGo(i)) {
51
- const { title, pubDate, itunes } = feed.items[i];
52
- const actualSeasonNum = itunes?.season ? parseInt(itunes.season) : null;
53
- const pubDateDay = dayjs(new Date(pubDate));
54
- let isValid = true;
55
-
56
- if (episodeRegex) {
57
- const generatedEpisodeRegex = new RegExp(episodeRegex);
58
- if (title && !generatedEpisodeRegex.test(title)) {
59
- isValid = false;
60
- }
61
- }
62
-
63
- if (episodeRegexExclude) {
64
- const generatedEpisodeRegexExclude = new RegExp(episodeRegexExclude);
65
- if (title && generatedEpisodeRegexExclude.test(title)) {
66
- isValid = false;
67
- }
68
- }
69
-
70
- if (before) {
71
- const beforeDateDay = dayjs(new Date(before));
72
- if (
73
- !pubDateDay.isSame(beforeDateDay, "day") &&
74
- !pubDateDay.isBefore(beforeDateDay, "day")
75
- ) {
76
- isValid = false;
77
- }
78
- }
79
-
80
- if (after) {
81
- const afterDateDay = dayjs(new Date(after));
82
- if (
83
- !pubDateDay.isSame(afterDateDay, "day") &&
84
- !pubDateDay.isAfter(afterDateDay, "day")
85
- ) {
86
- isValid = false;
87
- }
88
- }
89
-
90
- if (season && season != actualSeasonNum) {
91
- isValid = false;
92
- }
93
-
94
- const { url: episodeAudioUrl, ext: audioFileExt } =
95
- getEpisodeAudioUrlAndExt(feed.items[i], episodeSourceOrder);
96
-
97
- const key = getArchiveKey({
98
- prefix: archivePrefix,
99
- name: getArchiveFilename({
100
- pubDate,
101
- name: title,
102
- ext: audioFileExt,
103
- }),
104
- });
105
-
106
- if (key && savedArchive.includes(key)) {
107
- isValid = false;
108
- }
109
-
110
- if (isValid) {
111
- const item = feed.items[i];
112
- item._originalIndex = i;
113
- item.seasonNum = actualSeasonNum;
114
-
115
- if (includeEpisodeImages || addMp3MetadataFlag) {
116
- const episodeImageUrl = getImageUrl(item);
117
-
118
- if (episodeImageUrl) {
119
- const episodeImageFileExt = getUrlExt(episodeImageUrl);
120
- const episodeImageArchiveKey = getArchiveKey({
121
- prefix: archivePrefix,
122
- name: getArchiveFilename({
123
- pubDate,
124
- name: title,
125
- ext: episodeImageFileExt,
126
- }),
127
- });
128
-
129
- const episodeImageName = getItemFilename({
130
- item,
131
- feed,
132
- url: episodeAudioUrl,
133
- ext: episodeImageFileExt,
134
- template: episodeTemplate,
135
- customTemplateOptions: episodeCustomTemplateOptions,
136
- width: episodeDigits,
137
- offset: episodeNumOffset,
138
- });
139
-
140
- const outputImagePath = path.resolve(basePath, episodeImageName);
141
- item._episodeImage = {
142
- url: episodeImageUrl,
143
- outputPath: outputImagePath,
144
- key: episodeImageArchiveKey,
145
- };
146
- }
147
- }
148
-
149
- if (includeEpisodeTranscripts) {
150
- const episodeTranscriptUrl = getTranscriptUrl(
151
- item,
152
- episodeTranscriptTypes
153
- );
154
-
155
- if (episodeTranscriptUrl) {
156
- const episodeTranscriptFileExt = getUrlExt(episodeTranscriptUrl);
157
- const episodeTranscriptArchiveKey = getArchiveKey({
158
- prefix: archivePrefix,
159
- name: getArchiveFilename({
160
- pubDate,
161
- name: title,
162
- ext: episodeTranscriptFileExt,
163
- }),
164
- });
165
-
166
- const episodeTranscriptName = getItemFilename({
167
- item,
168
- feed,
169
- url: episodeAudioUrl,
170
- ext: episodeTranscriptFileExt,
171
- template: episodeTemplate,
172
- width: episodeDigits,
173
- offset: episodeNumOffset,
174
- });
175
-
176
- const outputTranscriptPath = path.resolve(
177
- basePath,
178
- episodeTranscriptName
179
- );
180
-
181
- item._episodeTranscript = {
182
- url: episodeTranscriptUrl,
183
- outputPath: outputTranscriptPath,
184
- key: episodeTranscriptArchiveKey,
185
- };
186
- }
187
- }
188
-
189
- items.push(item);
190
- }
191
-
192
- i = next(i);
193
- }
194
-
195
- return limit ? items.slice(0, limit) : items;
196
- };
197
-
198
- export const logItemsList = ({
199
- type,
200
- feed,
201
- limit,
202
- offset,
203
- reverse,
204
- before,
205
- after,
206
- episodeRegex,
207
- episodeRegexExclude,
208
- season,
209
- }) => {
210
- const items = getItemsToDownload({
211
- feed,
212
- limit,
213
- offset,
214
- reverse,
215
- before,
216
- after,
217
- episodeRegex,
218
- episodeRegexExclude,
219
- season,
220
- });
221
-
222
- if (!items.length) {
223
- logErrorAndExit("No episodes found with provided criteria to list");
224
- }
225
-
226
- const isJson = type === "json";
227
-
228
- const output = items.map((item) => {
229
- const data = {
230
- seasonNum: item.seasonNum,
231
- episodeNum: feed.items.length - item._originalIndex,
232
- title: item.title,
233
- pubDate: item.pubDate,
234
- };
235
-
236
- return data;
237
- });
238
-
239
- if (isJson) {
240
- // eslint-disable-next-line no-console
241
- console.log(JSON.stringify(output));
242
- return;
243
- }
244
-
245
- // eslint-disable-next-line no-console
246
- console.table(output);
247
- };
1
+ import dayjs from "dayjs";
2
+ import path from "path";
3
+ import { getArchive, getArchiveFilename, getArchiveKey } from "./archive.js";
4
+ import { logErrorAndExit } from "./logger.js";
5
+ import { getItemFilename } from "./naming.js";
6
+ import {
7
+ getEpisodeAudioUrlAndExt,
8
+ getImageUrl,
9
+ getLoopControls,
10
+ getTranscriptUrl,
11
+ getUrlExt,
12
+ } from "./util.js";
13
+
14
+ export const ITEM_LIST_FORMATS = ["table", "json"];
15
+
16
+ export const getItemsToDownload = ({
17
+ archive,
18
+ archivePrefix,
19
+ embedMetadataFlag,
20
+ basePath,
21
+ feed,
22
+ limit,
23
+ offset,
24
+ reverse,
25
+ before,
26
+ after,
27
+ episodeDigits,
28
+ episodeNumOffset,
29
+ episodeRegex,
30
+ episodeRegexExclude,
31
+ episodeSourceOrder,
32
+ episodeTemplate,
33
+ episodeCustomTemplateOptions,
34
+ includeEpisodeImages,
35
+ includeEpisodeTranscripts,
36
+ episodeTranscriptTypes,
37
+ season,
38
+ }) => {
39
+ const { startIndex, shouldGo, next } = getLoopControls({
40
+ offset,
41
+ reverse,
42
+ length: feed.items.length,
43
+ });
44
+
45
+ let i = startIndex;
46
+ const items = [];
47
+
48
+ const savedArchive = archive ? getArchive(archive) : [];
49
+
50
+ while (shouldGo(i)) {
51
+ const { title, pubDate, itunes } = feed.items[i];
52
+ const actualSeasonNum = itunes?.season ? parseInt(itunes.season) : null;
53
+ const pubDateDay = dayjs(new Date(pubDate));
54
+ let isValid = true;
55
+
56
+ if (episodeRegex) {
57
+ const generatedEpisodeRegex = new RegExp(episodeRegex);
58
+ if (title && !generatedEpisodeRegex.test(title)) {
59
+ isValid = false;
60
+ }
61
+ }
62
+
63
+ if (episodeRegexExclude) {
64
+ const generatedEpisodeRegexExclude = new RegExp(episodeRegexExclude);
65
+ if (title && generatedEpisodeRegexExclude.test(title)) {
66
+ isValid = false;
67
+ }
68
+ }
69
+
70
+ if (before) {
71
+ const beforeDateDay = dayjs(new Date(before));
72
+ if (
73
+ !pubDateDay.isSame(beforeDateDay, "day") &&
74
+ !pubDateDay.isBefore(beforeDateDay, "day")
75
+ ) {
76
+ isValid = false;
77
+ }
78
+ }
79
+
80
+ if (after) {
81
+ const afterDateDay = dayjs(new Date(after));
82
+ if (
83
+ !pubDateDay.isSame(afterDateDay, "day") &&
84
+ !pubDateDay.isAfter(afterDateDay, "day")
85
+ ) {
86
+ isValid = false;
87
+ }
88
+ }
89
+
90
+ if (season && season != actualSeasonNum) {
91
+ isValid = false;
92
+ }
93
+
94
+ const { url: episodeAudioUrl, ext: audioFileExt } =
95
+ getEpisodeAudioUrlAndExt(feed.items[i], episodeSourceOrder);
96
+
97
+ const key = getArchiveKey({
98
+ prefix: archivePrefix,
99
+ name: getArchiveFilename({
100
+ pubDate,
101
+ name: title,
102
+ ext: audioFileExt,
103
+ }),
104
+ });
105
+
106
+ if (key && savedArchive.includes(key)) {
107
+ isValid = false;
108
+ }
109
+
110
+ if (isValid) {
111
+ const item = feed.items[i];
112
+ item._originalIndex = i;
113
+ item.seasonNum = actualSeasonNum;
114
+
115
+ if (includeEpisodeImages || embedMetadataFlag) {
116
+ const episodeImageUrl = getImageUrl(item);
117
+
118
+ if (episodeImageUrl) {
119
+ const episodeImageFileExt = getUrlExt(episodeImageUrl);
120
+ const episodeImageArchiveKey = getArchiveKey({
121
+ prefix: archivePrefix,
122
+ name: getArchiveFilename({
123
+ pubDate,
124
+ name: title,
125
+ ext: episodeImageFileExt,
126
+ }),
127
+ });
128
+
129
+ const episodeImageName = getItemFilename({
130
+ item,
131
+ feed,
132
+ url: episodeAudioUrl,
133
+ ext: episodeImageFileExt,
134
+ template: episodeTemplate,
135
+ customTemplateOptions: episodeCustomTemplateOptions,
136
+ width: episodeDigits,
137
+ offset: episodeNumOffset,
138
+ });
139
+
140
+ const outputImagePath = path.resolve(basePath, episodeImageName);
141
+ item._episodeImage = {
142
+ url: episodeImageUrl,
143
+ outputPath: outputImagePath,
144
+ key: episodeImageArchiveKey,
145
+ };
146
+ }
147
+ }
148
+
149
+ if (includeEpisodeTranscripts) {
150
+ const episodeTranscriptUrl = getTranscriptUrl(
151
+ item,
152
+ episodeTranscriptTypes
153
+ );
154
+
155
+ if (episodeTranscriptUrl) {
156
+ const episodeTranscriptFileExt = getUrlExt(episodeTranscriptUrl);
157
+ const episodeTranscriptArchiveKey = getArchiveKey({
158
+ prefix: archivePrefix,
159
+ name: getArchiveFilename({
160
+ pubDate,
161
+ name: title,
162
+ ext: episodeTranscriptFileExt,
163
+ }),
164
+ });
165
+
166
+ const episodeTranscriptName = getItemFilename({
167
+ item,
168
+ feed,
169
+ url: episodeAudioUrl,
170
+ ext: episodeTranscriptFileExt,
171
+ template: episodeTemplate,
172
+ width: episodeDigits,
173
+ offset: episodeNumOffset,
174
+ });
175
+
176
+ const outputTranscriptPath = path.resolve(
177
+ basePath,
178
+ episodeTranscriptName
179
+ );
180
+
181
+ item._episodeTranscript = {
182
+ url: episodeTranscriptUrl,
183
+ outputPath: outputTranscriptPath,
184
+ key: episodeTranscriptArchiveKey,
185
+ };
186
+ }
187
+ }
188
+
189
+ items.push(item);
190
+ }
191
+
192
+ i = next(i);
193
+ }
194
+
195
+ return limit ? items.slice(0, limit) : items;
196
+ };
197
+
198
+ export const logItemsList = ({
199
+ type,
200
+ feed,
201
+ limit,
202
+ offset,
203
+ reverse,
204
+ before,
205
+ after,
206
+ episodeRegex,
207
+ episodeRegexExclude,
208
+ season,
209
+ }) => {
210
+ const items = getItemsToDownload({
211
+ feed,
212
+ limit,
213
+ offset,
214
+ reverse,
215
+ before,
216
+ after,
217
+ episodeRegex,
218
+ episodeRegexExclude,
219
+ season,
220
+ });
221
+
222
+ if (!items.length) {
223
+ logErrorAndExit("No episodes found with provided criteria to list");
224
+ }
225
+
226
+ const isJson = type === "json";
227
+
228
+ const output = items.map((item) => {
229
+ const data = {
230
+ seasonNum: item.seasonNum,
231
+ episodeNum: feed.items.length - item._originalIndex,
232
+ title: item.title,
233
+ pubDate: item.pubDate,
234
+ };
235
+
236
+ return data;
237
+ });
238
+
239
+ if (isJson) {
240
+ // eslint-disable-next-line no-console
241
+ console.log(JSON.stringify(output));
242
+ return;
243
+ }
244
+
245
+ // eslint-disable-next-line no-console
246
+ console.table(output);
247
+ };
package/bin/util.js CHANGED
@@ -1,8 +1,18 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import rssParser from "rss-parser";
3
+ import rssParser from "../lib/rss-parser/parser.js";
4
4
  import { logErrorAndExit, logMessage } from "./logger.js";
5
5
 
6
+ export const AUDIO_FORMATS = {
7
+ m4a: { codec: "aac", ext: ".m4a", attachedPic: true },
8
+ aac: { codec: "aac", ext: ".aac" },
9
+ mp3: { codec: "libmp3lame", ext: ".mp3", attachedPic: true },
10
+ opus: { codec: "libopus", ext: ".opus" },
11
+ ogg: { codec: "libvorbis", ext: ".ogg" },
12
+ flac: { codec: "flac", ext: ".flac" },
13
+ wav: { codec: "pcm_s16le", ext: ".wav" },
14
+ };
15
+
6
16
  export const isWin = process.platform === "win32";
7
17
 
8
18
  export const defaultRssParserConfig = {
@@ -10,9 +20,6 @@ export const defaultRssParserConfig = {
10
20
  headers: {
11
21
  Accept: "*/*",
12
22
  },
13
- customFields: {
14
- item: [["podcast:transcript", "podcastTranscripts", { keepArray: true }]],
15
- },
16
23
  };
17
24
 
18
25
  /*
package/bin/validate.js CHANGED
@@ -32,8 +32,10 @@ export const createParseNumber = ({ min, max, name, required = true }) => {
32
32
  };
33
33
  };
34
34
 
35
- export const hasFfmpeg = () => {
35
+ export const hasFfmpeg = (value) => {
36
36
  if (!commandExistsSync("ffmpeg")) {
37
37
  logErrorAndExit('option specified requires "ffmpeg" be available');
38
38
  }
39
+
40
+ return value;
39
41
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "podcast-dl",
3
- "version": "11.5.0",
3
+ "version": "11.6.0",
4
4
  "description": "A CLI for downloading podcasts.",
5
5
  "type": "module",
6
6
  "bin": "./bin/bin.js",
@@ -57,7 +57,8 @@
57
57
  "got": "^11.0.2",
58
58
  "p-limit": "^4.0.0",
59
59
  "pluralize": "^8.0.0",
60
- "rss-parser": "^3.12.0",
60
+ "entities": "^2.0.3",
61
+ "xml2js": "^0.5.0",
61
62
  "throttle-debounce": "^3.0.1"
62
63
  }
63
64
  }