podcast-dl 11.6.0 → 11.6.2
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/LICENSE +21 -21
- package/bin/archive.js +39 -39
- package/bin/bin.js +0 -0
- package/bin/exec.js +30 -30
- package/bin/logger.js +84 -84
- package/bin/meta.js +66 -66
- package/bin/naming.js +144 -136
- package/lib/rss-parser/README.md +3 -0
- package/lib/rss-parser/fields.js +66 -0
- package/lib/rss-parser/parser.js +387 -0
- package/lib/rss-parser/utils.js +101 -0
- package/package.json +65 -64
package/bin/naming.js
CHANGED
|
@@ -1,136 +1,144 @@
|
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ext
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
[
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
["
|
|
94
|
-
["
|
|
95
|
-
["
|
|
96
|
-
["
|
|
97
|
-
["
|
|
98
|
-
["
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
// Replace periods with underscores BEFORE filenamify truncation.
|
|
42
|
+
// filenamify treats periods as extension delimiters and preserves content
|
|
43
|
+
// after the last period while truncating from the START, which destroys
|
|
44
|
+
// dates and other important prefix content in podcast titles.
|
|
45
|
+
const sanitized = name.replace(/\./g, INVALID_CHAR_REPLACE);
|
|
46
|
+
return filenamify(sanitized, {
|
|
47
|
+
replacement: INVALID_CHAR_REPLACE,
|
|
48
|
+
maxLength,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const getSimpleFilename = (name, ext = "") => {
|
|
53
|
+
return `${getSafeName(name, MAX_LENGTH_FILENAME - (ext?.length ?? 0))}${ext}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const getItemFilename = ({
|
|
57
|
+
item,
|
|
58
|
+
ext,
|
|
59
|
+
url,
|
|
60
|
+
feed,
|
|
61
|
+
template,
|
|
62
|
+
width,
|
|
63
|
+
customTemplateOptions = [],
|
|
64
|
+
offset = 0,
|
|
65
|
+
}) => {
|
|
66
|
+
const episodeNum = feed.items.length - item._originalIndex + offset;
|
|
67
|
+
const title = item.title || "";
|
|
68
|
+
|
|
69
|
+
const releaseYear = item.pubDate
|
|
70
|
+
? dayjs(new Date(item.pubDate)).format("YYYY")
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
const releaseMonth = item.pubDate
|
|
74
|
+
? dayjs(new Date(item.pubDate)).format("MM")
|
|
75
|
+
: null;
|
|
76
|
+
|
|
77
|
+
const releaseDay = item.pubDate
|
|
78
|
+
? dayjs(new Date(item.pubDate)).format("DD")
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
const releaseDate = item.pubDate
|
|
82
|
+
? dayjs(new Date(item.pubDate)).format("YYYYMMDD")
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
const customReplacementTuples = customTemplateOptions.map((option, i) => {
|
|
86
|
+
const matchRegex = new RegExp(option);
|
|
87
|
+
const match = title.match(matchRegex);
|
|
88
|
+
|
|
89
|
+
return match && match[0] ? [`custom_${i}`, match[0]] : [`custom_${i}`, ""];
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const templateReplacementsTuples = [
|
|
93
|
+
["title", title],
|
|
94
|
+
["release_date", releaseDate || ""],
|
|
95
|
+
["release_year", releaseYear || ""],
|
|
96
|
+
["release_month", releaseMonth || ""],
|
|
97
|
+
["release_day", releaseDay || ""],
|
|
98
|
+
["episode_num", `${episodeNum}`.padStart(width, "0")],
|
|
99
|
+
["url", url],
|
|
100
|
+
["podcast_title", feed.title || ""],
|
|
101
|
+
["podcast_link", feed.link || ""],
|
|
102
|
+
["duration", item.itunes?.duration || ""],
|
|
103
|
+
["guid", item.guid],
|
|
104
|
+
...customReplacementTuples,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const replacementsMap = Object.fromEntries(templateReplacementsTuples);
|
|
108
|
+
const templateSegments = template.trim().split(path.sep);
|
|
109
|
+
const nameSegments = templateSegments.map((segment, index) => {
|
|
110
|
+
const replaceRegex = /{{(\w+)(\|[^}]+)?}}/g;
|
|
111
|
+
const name = segment.replace(replaceRegex, (match, varName, filterStr) => {
|
|
112
|
+
const replacement = replacementsMap[varName] || "";
|
|
113
|
+
return applyFilters(replacement, filterStr);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Only truncate non-final segments here (they don't get an extension)
|
|
117
|
+
// Final segment is truncated below with the extension accounted for
|
|
118
|
+
const isLastSegment = index === templateSegments.length - 1;
|
|
119
|
+
return isLastSegment ? name : getSimpleFilename(name);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
nameSegments[nameSegments.length - 1] = getSimpleFilename(
|
|
123
|
+
nameSegments[nameSegments.length - 1],
|
|
124
|
+
ext
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return nameSegments.join(path.sep);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const getFolderName = ({ feed, template }) => {
|
|
131
|
+
const replacementsMap = {
|
|
132
|
+
podcast_title: feed.title || "",
|
|
133
|
+
podcast_link: feed.link || "",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const replaceRegex = /{{(\w+)(\|[^}]+)?}}/g;
|
|
137
|
+
const name = template.replace(replaceRegex, (_, varName, filterStr) => {
|
|
138
|
+
const replacement = replacementsMap[varName] || "";
|
|
139
|
+
const filtered = applyFilters(replacement, filterStr);
|
|
140
|
+
return getSafeName(filtered);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return name;
|
|
144
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const feed = [
|
|
2
|
+
["author", "creator"],
|
|
3
|
+
["dc:publisher", "publisher"],
|
|
4
|
+
["dc:creator", "creator"],
|
|
5
|
+
["dc:source", "source"],
|
|
6
|
+
["dc:title", "title"],
|
|
7
|
+
["dc:type", "type"],
|
|
8
|
+
"title",
|
|
9
|
+
"description",
|
|
10
|
+
"author",
|
|
11
|
+
"pubDate",
|
|
12
|
+
"webMaster",
|
|
13
|
+
"managingEditor",
|
|
14
|
+
"generator",
|
|
15
|
+
"link",
|
|
16
|
+
"language",
|
|
17
|
+
"copyright",
|
|
18
|
+
"lastBuildDate",
|
|
19
|
+
"docs",
|
|
20
|
+
"generator",
|
|
21
|
+
"ttl",
|
|
22
|
+
"rating",
|
|
23
|
+
"skipHours",
|
|
24
|
+
"skipDays",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const item = [
|
|
28
|
+
["author", "creator"],
|
|
29
|
+
["dc:creator", "creator"],
|
|
30
|
+
["dc:date", "date"],
|
|
31
|
+
["dc:language", "language"],
|
|
32
|
+
["dc:rights", "rights"],
|
|
33
|
+
["dc:source", "source"],
|
|
34
|
+
["dc:title", "title"],
|
|
35
|
+
"title",
|
|
36
|
+
"link",
|
|
37
|
+
"pubDate",
|
|
38
|
+
"author",
|
|
39
|
+
"summary",
|
|
40
|
+
["content:encoded", "content:encoded", { includeSnippet: true }],
|
|
41
|
+
"enclosure",
|
|
42
|
+
"dc:creator",
|
|
43
|
+
"dc:date",
|
|
44
|
+
"comments",
|
|
45
|
+
["podcast:transcript", "podcastTranscripts", { keepArray: true }],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const mapItunesField = (f) => ["itunes:" + f, f];
|
|
49
|
+
|
|
50
|
+
export const podcastFeed = ["author", "subtitle", "summary", "explicit"].map(
|
|
51
|
+
mapItunesField
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export const podcastItem = [
|
|
55
|
+
"author",
|
|
56
|
+
"subtitle",
|
|
57
|
+
"summary",
|
|
58
|
+
"explicit",
|
|
59
|
+
"duration",
|
|
60
|
+
"image",
|
|
61
|
+
"episode",
|
|
62
|
+
"image",
|
|
63
|
+
"season",
|
|
64
|
+
"keywords",
|
|
65
|
+
"episodeType",
|
|
66
|
+
].map(mapItunesField);
|