podcast-dl 10.4.0 → 11.0.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/ffmpeg.js ADDED
@@ -0,0 +1,101 @@
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 } 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
+ subtitle,
56
+ comment,
57
+ disc,
58
+ track,
59
+ "episode-type": episodeType,
60
+ date,
61
+ };
62
+
63
+ const metadataString = Object.keys(metaKeysToValues)
64
+ .map((key) => {
65
+ if (!metaKeysToValues[key]) {
66
+ return null;
67
+ }
68
+
69
+ const argValue = escapeArgForShell(metaKeysToValues[key]);
70
+
71
+ return argValue ? `-metadata ${key}=${argValue}` : null;
72
+ })
73
+ .filter((segment) => !!segment)
74
+ .join(" ");
75
+
76
+ command += ` -map_metadata 0 ${metadataString} -codec copy`;
77
+ }
78
+
79
+ if (shouldEmbedImage) {
80
+ command += ` -map 0 -map 1`;
81
+ } else {
82
+ command += ` -map 0`;
83
+ }
84
+
85
+ const tmpMp3Path = `${outputPath}.tmp${ext}`;
86
+ command += ` ${escapeArgForShell(tmpMp3Path)}`;
87
+ logMessage("Running command: " + command, LOG_LEVELS.debug);
88
+
89
+ try {
90
+ await execWithPromise(command, { stdio: "ignore" });
91
+ } catch (error) {
92
+ if (fs.existsSync(tmpMp3Path)) {
93
+ fs.unlinkSync(tmpMp3Path);
94
+ }
95
+
96
+ throw error;
97
+ }
98
+
99
+ fs.unlinkSync(outputPath);
100
+ fs.renameSync(tmpMp3Path, outputPath);
101
+ };
package/bin/items.js ADDED
@@ -0,0 +1,203 @@
1
+ import dayjs from "dayjs";
2
+ import path from "path";
3
+ import { getItemFilename } from "./naming.js";
4
+ import {
5
+ getEpisodeAudioUrlAndExt,
6
+ getImageUrl,
7
+ getLoopControls,
8
+ getTranscriptUrl,
9
+ getUrlExt,
10
+ } from "./util.js";
11
+ import { logErrorAndExit } from "./logger.js";
12
+
13
+ export const ITEM_LIST_FORMATS = ["table", "json"];
14
+
15
+ export const getItemsToDownload = ({
16
+ addMp3MetadataFlag,
17
+ basePath,
18
+ feed,
19
+ limit,
20
+ offset,
21
+ reverse,
22
+ before,
23
+ after,
24
+ episodeDigits,
25
+ episodeNumOffset,
26
+ episodeRegex,
27
+ episodeRegexExclude,
28
+ episodeSourceOrder,
29
+ episodeTemplate,
30
+ episodeCustomTemplateOptions,
31
+ includeEpisodeImages,
32
+ includeEpisodeTranscripts,
33
+ episodeTranscriptTypes,
34
+ }) => {
35
+ const { startIndex, shouldGo, next } = getLoopControls({
36
+ offset,
37
+ reverse,
38
+ length: feed.items.length,
39
+ });
40
+
41
+ let i = startIndex;
42
+ const items = [];
43
+
44
+ while (shouldGo(i)) {
45
+ const { title, pubDate } = feed.items[i];
46
+ const pubDateDay = dayjs(new Date(pubDate));
47
+ let isValid = true;
48
+
49
+ if (episodeRegex) {
50
+ const generatedEpisodeRegex = new RegExp(episodeRegex);
51
+ if (title && !generatedEpisodeRegex.test(title)) {
52
+ isValid = false;
53
+ }
54
+ }
55
+
56
+ if (episodeRegexExclude) {
57
+ const generatedEpisodeRegexExclude = new RegExp(episodeRegexExclude);
58
+ if (title && generatedEpisodeRegexExclude.test(title)) {
59
+ isValid = false;
60
+ }
61
+ }
62
+
63
+ if (before) {
64
+ const beforeDateDay = dayjs(new Date(before));
65
+ if (
66
+ !pubDateDay.isSame(beforeDateDay, "day") &&
67
+ !pubDateDay.isBefore(beforeDateDay, "day")
68
+ ) {
69
+ isValid = false;
70
+ }
71
+ }
72
+
73
+ if (after) {
74
+ const afterDateDay = dayjs(new Date(after));
75
+ if (
76
+ !pubDateDay.isSame(afterDateDay, "day") &&
77
+ !pubDateDay.isAfter(afterDateDay, "day")
78
+ ) {
79
+ isValid = false;
80
+ }
81
+ }
82
+
83
+ const { url: episodeAudioUrl } = getEpisodeAudioUrlAndExt(
84
+ feed.items[i],
85
+ episodeSourceOrder
86
+ );
87
+
88
+ if (isValid) {
89
+ const item = feed.items[i];
90
+ item._originalIndex = i;
91
+
92
+ if (includeEpisodeImages || addMp3MetadataFlag) {
93
+ const episodeImageUrl = getImageUrl(item);
94
+
95
+ if (episodeImageUrl) {
96
+ const episodeImageFileExt = getUrlExt(episodeImageUrl);
97
+
98
+ const episodeImageName = getItemFilename({
99
+ item,
100
+ feed,
101
+ url: episodeAudioUrl,
102
+ ext: episodeImageFileExt,
103
+ template: episodeTemplate,
104
+ customTemplateOptions: episodeCustomTemplateOptions,
105
+ width: episodeDigits,
106
+ offset: episodeNumOffset,
107
+ });
108
+
109
+ const outputImagePath = path.resolve(basePath, episodeImageName);
110
+ item._episodeImage = {
111
+ url: episodeImageUrl,
112
+ outputPath: outputImagePath,
113
+ };
114
+ }
115
+ }
116
+
117
+ if (includeEpisodeTranscripts) {
118
+ const episodeTranscriptUrl = getTranscriptUrl(
119
+ item,
120
+ episodeTranscriptTypes
121
+ );
122
+
123
+ if (episodeTranscriptUrl) {
124
+ const episodeTranscriptFileExt = getUrlExt(episodeTranscriptUrl);
125
+
126
+ const episodeTranscriptName = getItemFilename({
127
+ item,
128
+ feed,
129
+ url: episodeAudioUrl,
130
+ ext: episodeTranscriptFileExt,
131
+ template: episodeTemplate,
132
+ width: episodeDigits,
133
+ offset: episodeNumOffset,
134
+ });
135
+
136
+ const outputTranscriptPath = path.resolve(
137
+ basePath,
138
+ episodeTranscriptName
139
+ );
140
+
141
+ item._episodeTranscript = {
142
+ url: episodeTranscriptUrl,
143
+ outputPath: outputTranscriptPath,
144
+ };
145
+ }
146
+ }
147
+
148
+ items.push(item);
149
+ }
150
+
151
+ i = next(i);
152
+ }
153
+
154
+ return limit ? items.slice(0, limit) : items;
155
+ };
156
+
157
+ export const logItemsList = ({
158
+ type,
159
+ feed,
160
+ limit,
161
+ offset,
162
+ reverse,
163
+ before,
164
+ after,
165
+ episodeRegex,
166
+ episodeRegexExclude,
167
+ }) => {
168
+ const items = getItemsToDownload({
169
+ feed,
170
+ limit,
171
+ offset,
172
+ reverse,
173
+ before,
174
+ after,
175
+ episodeRegex,
176
+ episodeRegexExclude,
177
+ });
178
+
179
+ if (!items.length) {
180
+ logErrorAndExit("No episodes found with provided criteria to list");
181
+ }
182
+
183
+ const isJson = type === "json";
184
+
185
+ const output = items.map((item) => {
186
+ const data = {
187
+ episodeNum: feed.items.length - item._originalIndex,
188
+ title: item.title,
189
+ pubDate: item.pubDate,
190
+ };
191
+
192
+ return data;
193
+ });
194
+
195
+ if (isJson) {
196
+ // eslint-disable-next-line no-console
197
+ console.log(JSON.stringify(output));
198
+ return;
199
+ }
200
+
201
+ // eslint-disable-next-line no-console
202
+ console.table(output);
203
+ };
package/bin/logger.js CHANGED
@@ -1,25 +1,25 @@
1
1
  /* eslint-disable no-console */
2
2
 
3
- const ERROR_STATUSES = {
3
+ export const ERROR_STATUSES = {
4
4
  general: 1,
5
5
  nothingDownloaded: 2,
6
6
  completedWithErrors: 3,
7
7
  };
8
8
 
9
- const LOG_LEVEL_TYPES = {
9
+ export const LOG_LEVEL_TYPES = {
10
10
  debug: "debug",
11
11
  quiet: "quiet",
12
12
  silent: "silent",
13
13
  static: "static",
14
14
  };
15
15
 
16
- const LOG_LEVELS = {
16
+ export const LOG_LEVELS = {
17
17
  debug: 0,
18
18
  info: 1,
19
19
  important: 2,
20
20
  };
21
21
 
22
- const getShouldOutputProgressIndicator = () => {
22
+ export const getShouldOutputProgressIndicator = () => {
23
23
  return (
24
24
  process.stdout.isTTY &&
25
25
  process.env.LOG_LEVEL !== LOG_LEVEL_TYPES.static &&
@@ -28,7 +28,7 @@ const getShouldOutputProgressIndicator = () => {
28
28
  );
29
29
  };
30
30
 
31
- const logMessage = (message = "", logLevel = 1) => {
31
+ export const logMessage = (message = "", logLevel = 1) => {
32
32
  if (
33
33
  !process.env.LOG_LEVEL ||
34
34
  process.env.LOG_LEVEL === LOG_LEVEL_TYPES.debug ||
@@ -51,7 +51,7 @@ const logMessage = (message = "", logLevel = 1) => {
51
51
  }
52
52
  };
53
53
 
54
- const getLogMessageWithMarker = (marker) => {
54
+ export const getLogMessageWithMarker = (marker) => {
55
55
  return (message, logLevel) => {
56
56
  if (marker) {
57
57
  logMessage(`${marker} | ${message}`, logLevel);
@@ -61,7 +61,7 @@ const getLogMessageWithMarker = (marker) => {
61
61
  };
62
62
  };
63
63
 
64
- const logError = (msg, error) => {
64
+ export const logError = (msg, error) => {
65
65
  if (process.env.LOG_LEVEL === LOG_LEVEL_TYPES.silent) {
66
66
  return;
67
67
  }
@@ -73,7 +73,7 @@ const logError = (msg, error) => {
73
73
  }
74
74
  };
75
75
 
76
- const logErrorAndExit = (msg, error) => {
76
+ export const logErrorAndExit = (msg, error) => {
77
77
  console.error(msg);
78
78
 
79
79
  if (error) {
@@ -82,13 +82,3 @@ const logErrorAndExit = (msg, error) => {
82
82
 
83
83
  process.exit(ERROR_STATUSES.general);
84
84
  };
85
-
86
- export {
87
- ERROR_STATUSES,
88
- getShouldOutputProgressIndicator,
89
- getLogMessageWithMarker,
90
- LOG_LEVELS,
91
- logMessage,
92
- logError,
93
- logErrorAndExit,
94
- };
package/bin/meta.js ADDED
@@ -0,0 +1,33 @@
1
+ import fs from "fs";
2
+ import { logMessage } from "./logger.js";
3
+ import { getPublicObject } from "./util.js";
4
+
5
+ export const writeFeedMeta = ({ outputPath, feed, override }) => {
6
+ const output = getPublicObject(feed, ["items"]);
7
+
8
+ try {
9
+ if (override || !fs.existsSync(outputPath)) {
10
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
11
+ } else {
12
+ logMessage("Feed metadata exists locally. Skipping...");
13
+ }
14
+ } catch (error) {
15
+ throw new Error(
16
+ `Unable to save metadata file for feed: ${error.toString()}`
17
+ );
18
+ }
19
+ };
20
+
21
+ export const writeItemMeta = ({ marker, outputPath, item, override }) => {
22
+ const output = getPublicObject(item);
23
+
24
+ try {
25
+ if (override || !fs.existsSync(outputPath)) {
26
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 4));
27
+ } else {
28
+ logMessage(`${marker} | Episode metadata exists locally. Skipping...`);
29
+ }
30
+ } catch (error) {
31
+ throw new Error("Unable to save meta file for episode", error);
32
+ }
33
+ };
package/bin/naming.js CHANGED
@@ -1,24 +1,24 @@
1
- import path from "path";
2
- import filenamify from "filenamify";
3
1
  import dayjs from "dayjs";
2
+ import filenamify from "filenamify";
3
+ import path from "path";
4
4
 
5
5
  const INVALID_CHAR_REPLACE = "_";
6
6
  const MAX_LENGTH_FILENAME = process.env.MAX_LENGTH_FILENAME
7
7
  ? parseInt(process.env.MAX_LENGTH_FILENAME)
8
8
  : 255;
9
9
 
10
- const getSafeName = (name, maxLength = MAX_LENGTH_FILENAME) => {
10
+ export const getSafeName = (name, maxLength = MAX_LENGTH_FILENAME) => {
11
11
  return filenamify(name, {
12
12
  replacement: INVALID_CHAR_REPLACE,
13
13
  maxLength,
14
14
  });
15
15
  };
16
16
 
17
- const getSimpleFilename = (name, ext = "") => {
17
+ export const getSimpleFilename = (name, ext = "") => {
18
18
  return `${getSafeName(name, MAX_LENGTH_FILENAME - (ext?.length ?? 0))}${ext}`;
19
19
  };
20
20
 
21
- const getItemFilename = ({
21
+ export const getItemFilename = ({
22
22
  item,
23
23
  ext,
24
24
  url,
@@ -92,7 +92,7 @@ const getItemFilename = ({
92
92
  return nameSegments.join(path.sep);
93
93
  };
94
94
 
95
- const getFolderName = ({ feed, template }) => {
95
+ export const getFolderName = ({ feed, template }) => {
96
96
  const templateReplacementsTuples = [
97
97
  ["podcast_title", feed.title || ""],
98
98
  ["podcast_link", feed.link || ""],
@@ -110,21 +110,3 @@ const getFolderName = ({ feed, template }) => {
110
110
 
111
111
  return name;
112
112
  };
113
-
114
- const getArchiveFilename = ({ pubDate, name, ext }) => {
115
- const formattedPubDate = pubDate
116
- ? dayjs(new Date(pubDate)).format("YYYYMMDD")
117
- : null;
118
-
119
- const baseName = formattedPubDate ? `${formattedPubDate}-${name}` : name;
120
-
121
- return `${baseName}${ext}`;
122
- };
123
-
124
- export {
125
- getArchiveFilename,
126
- getFolderName,
127
- getItemFilename,
128
- getSafeName,
129
- getSimpleFilename,
130
- };