-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
112 lines (99 loc) · 3.08 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
import path from "node:path";
import { fileURLToPath } from "url";
import { mkdir, writeFile } from "node:fs/promises";
import fs from "node:fs";
import { Readable } from "node:stream";
import { finished } from "node:stream/promises";
import { XMLParser } from "fast-xml-parser";
function logger(verbose, args) {
if (!verbose) return () => {};
return (...args) => console.log(...args);
}
async function podcastDownloader(
feedURL,
outputFolder = "out",
verbose = false
) {
if (!feedURL) throw new Error("Feed URL is required");
const log = logger(verbose);
log("✅ input", { feedURL, outputFolder });
// fetch rss feed
const response = await fetch(feedURL);
if (!response.ok) {
throw new Error("Failed to fetch feed");
}
log("✅ got feed response");
const XMLBody = await response.text();
log("✅ start parsing XML");
const parser = new XMLParser({
ignoreAttributes: false,
});
const objectBody = parser.parse(XMLBody);
log("✅ end parsing XML");
const channel = objectBody?.rss?.channel;
const summary = {
title: channel?.title.trim(),
link: channel?.link,
description: channel?.description.trim(),
image: channel?.podcast_image,
};
log("✅ summary", summary);
log("✅ item example", channel?.item[0]);
const episodes = channel?.item.map((item) => {
return {
season: item["itunes:season"]["#text"],
episode: item["itunes:episode"]["#text"],
title: item?.title.trim(),
description: item?.description_item_stripped.trim(),
link: item?.link,
audio: item?.enclosure["@_url"],
pubDate: item?.pubDate_friendly,
pubDate_sortable: item?.pubDate_sortable,
};
});
log("✅ episode count", episodes.length);
log("✅ episode example", episodes[0]);
// make an output folder if it does not exist
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const outputPath = path.resolve(__dirname, outputFolder);
log("✅ about to create output path", outputPath);
try {
await mkdir(outputPath, { recursive: true });
} catch (error) {
log("⛔️ output error", error);
throw new Error("Failed to create output folder");
}
// save all podcast+episode info in text files
const xmlPromise = writeFile(
path.resolve(outputPath, "response.xml"),
XMLBody
);
const summaryPromise = writeFile(
path.resolve(outputPath, "summary.json"),
JSON.stringify(summary, null, 4)
);
const episodesPromise = writeFile(
path.resolve(outputPath, "episodes.json"),
JSON.stringify(episodes, null, 4)
);
await Promise.all([xmlPromise, summaryPromise, episodesPromise]);
let loopIndex = 0;
// download podcast episode audio from url
for (const episode of episodes) {
loopIndex++;
log(
`Fetching ${loopIndex}/${episodes.length} S${episode.season}E${episode.episode}`
);
const fileName =
`${summary.title}-${episode.season}-${episode.episode}.mp3`.replace(
" ",
"_"
);
const stream = fs.createWriteStream(path.resolve(outputPath, fileName));
const { body } = await fetch(episode.audio);
await finished(Readable.fromWeb(body).pipe(stream));
}
log("✅ all done");
}
export default podcastDownloader;