From b6e4f3a8c5e6ca867fa20de514807a1419980014 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 5 Mar 2022 18:54:24 -0600 Subject: [PATCH] Add:Podcast RSS feed parser --- server/ApiController.js | 26 +++++++++ server/utils/podcastUtils.js | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 server/utils/podcastUtils.js diff --git a/server/ApiController.js b/server/ApiController.js index 45969c75..b63cc784 100644 --- a/server/ApiController.js +++ b/server/ApiController.js @@ -2,9 +2,11 @@ const express = require('express') const Path = require('path') const fs = require('fs-extra') const date = require('date-and-time') +const axios = require('axios') const Logger = require('./Logger') const { isObject } = require('./utils/index') +const { parsePodcastRssFeedXml } = require('./utils/podcastUtils') const BookController = require('./controllers/BookController') const LibraryController = require('./controllers/LibraryController') @@ -179,6 +181,8 @@ class ApiController { this.router.post('/syncLocal', this.syncLocal.bind(this)) this.router.post('/streams/:id/close', this.closeStream.bind(this)) + + this.router.post('/getPodcastFeed', this.getPodcastFeed.bind(this)) } async findBooks(req, res) { @@ -527,5 +531,27 @@ class ApiController { this.streamManager.closeStreamApiRequest(userId, streamId) res.sendStatus(200) } + + getPodcastFeed(req, res) { + var url = req.body.rssFeed + if (!url) { + return res.status(400).send('Bad request') + } + + axios.get(url).then(async (data) => { + if (!data || !data.data) { + Logger.error('Invalid podcast feed request response') + return res.status(500).send('Bad response from feed request') + } + var podcast = await parsePodcastRssFeedXml(data.data) + if (!podcast) { + return res.status(500).send('Invalid podcast RSS feed') + } + res.json(podcast) + }).catch((error) => { + console.error('Failed', error) + res.status(500).send(error) + }) + } } module.exports = ApiController \ No newline at end of file diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js new file mode 100644 index 00000000..d0a2e607 --- /dev/null +++ b/server/utils/podcastUtils.js @@ -0,0 +1,110 @@ +const Logger = require('../Logger') +const { xmlToJSON } = require('./index') + +function extractFirstArrayItem(json, key) { + if (!json[key] || !json[key].length) return null + return json[key][0] +} + +function extractImage(channel) { + if (!channel.image || !channel.image.url || !channel.image.url.length) { + if (!channel['itunes:image'] || !channel['itunes:image'].length || !channel['itunes:image'][0]['$']) { + return null + } + var itunesImage = channel['itunes:image'][0]['$'] + return itunesImage.href || null + } + return channel.image.url[0] || null +} + +function extractCategories(channel) { + if (!channel['itunes:category'] || !channel['itunes:category'].length) return [] + var categories = channel['itunes:category'] + var cleanedCats = [] + categories.forEach((cat) => { + if (!cat['$'] || !cat['$'].text) return + var cattext = cat['$'].text + if (cat['itunes:category']) { + var subcats = extractCategories(cat) + if (subcats.length) { + cleanedCats = cleanedCats.concat(subcats.map((subcat) => `${cattext}:${subcat}`)) + } else { + cleanedCats.push(cattext) + } + } else { + cleanedCats.push(cattext) + } + }) + return cleanedCats +} + +function extractPodcastMetadata(channel) { + var arrayFields = ['title', 'language', 'description', 'itunes:explicit', 'itunes:author'] + var metadata = { + image: extractImage(channel), + categories: extractCategories(channel) + } + arrayFields.forEach((key) => { + var cleanKey = key.split(':').pop() + metadata[cleanKey] = extractFirstArrayItem(channel, key) + }) + return metadata +} + +function extractEpisodeData(item) { + // Episode must have url + if (!item.enclosure || !item.enclosure.length || !item.enclosure[0]['$'] || !item.enclosure[0]['$'].url) { + Logger.error(`[podcastUtils] Invalid podcast episode data`) + return null + } + var arrayFields = ['title', 'pubDate', 'description', 'itunes:episodeType', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit'] + var episode = { + enclosure: { + ...item.enclosure[0]['$'] + } + } + arrayFields.forEach((key) => { + var cleanKey = key.split(':').pop() + episode[cleanKey] = extractFirstArrayItem(item, key) + }) + return episode +} + +function extractPodcastEpisodes(items) { + var episodes = [] + items.forEach((item) => { + var cleaned = extractEpisodeData(item) + if (cleaned) { + episodes.push(cleaned) + } + }) + return episodes +} + +function cleanPodcastJson(rssJson) { + if (!rssJson.channel || !rssJson.channel.length) { + Logger.error(`[podcastUtil] Invalid podcast no channel object`) + return null + } + var channel = rssJson.channel[0] + if (!channel.item || !channel.item.length) { + Logger.error(`[podcastUtil] Invalid podcast no episodes`) + return null + } + + var podcast = { + metadata: extractPodcastMetadata(channel), + episodes: extractPodcastEpisodes(channel.item) + } + return podcast +} + +module.exports.parsePodcastRssFeedXml = async (xml) => { + if (!xml) return null + var json = await xmlToJSON(xml) + if (!json || !json.rss) { + Logger.error('[podcastUtils] Invalid XML or RSS feed') + return null + } + return cleanPodcastJson(json.rss) +} \ No newline at end of file