mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-02-03 08:49:40 +00:00
Add:OPML Upload for bulk adding podcasts #588
This commit is contained in:
parent
e5469cc0f8
commit
514893646a
16 changed files with 359 additions and 18 deletions
|
|
@ -104,7 +104,7 @@ class PodcastController {
|
|||
return res.status(500).send('Bad response from feed request')
|
||||
}
|
||||
Logger.debug(`[PdocastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
||||
var payload = await parsePodcastRssFeedXml(data.data, includeRaw)
|
||||
var payload = await parsePodcastRssFeedXml(data.data, false, includeRaw)
|
||||
if (!payload) {
|
||||
return res.status(500).send('Invalid podcast RSS feed')
|
||||
}
|
||||
|
|
@ -119,6 +119,15 @@ class PodcastController {
|
|||
})
|
||||
}
|
||||
|
||||
async getOPMLFeeds(req, res) {
|
||||
if (!req.body.opmlText) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const rssFeedsData = await this.podcastManager.getOPMLFeeds(req.body.opmlText)
|
||||
res.json(rssFeedsData)
|
||||
}
|
||||
|
||||
async checkNewEpisodes(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
|||
const Logger = require('../Logger')
|
||||
|
||||
const { downloadFile } = require('../utils/fileUtils')
|
||||
const opmlParser = require('../utils/parsers/parseOPML')
|
||||
const prober = require('../utils/prober')
|
||||
const LibraryFile = require('../objects/files/LibraryFile')
|
||||
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
||||
|
|
@ -258,7 +259,7 @@ class PodcastManager {
|
|||
return newEpisodes
|
||||
}
|
||||
|
||||
getPodcastFeed(feedUrl) {
|
||||
getPodcastFeed(feedUrl, excludeEpisodeMetadata = false) {
|
||||
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}"`)
|
||||
return axios.get(feedUrl, { timeout: 5000 }).then(async (data) => {
|
||||
if (!data || !data.data) {
|
||||
|
|
@ -266,7 +267,7 @@ class PodcastManager {
|
|||
return false
|
||||
}
|
||||
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}" success - parsing xml`)
|
||||
var payload = await parsePodcastRssFeedXml(data.data)
|
||||
var payload = await parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata)
|
||||
if (!payload) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -276,5 +277,29 @@ class PodcastManager {
|
|||
return false
|
||||
})
|
||||
}
|
||||
|
||||
async getOPMLFeeds(opmlText) {
|
||||
var extractedFeeds = opmlParser.parse(opmlText)
|
||||
if (!extractedFeeds || !extractedFeeds.length) {
|
||||
Logger.error('[PodcastManager] getOPMLFeeds: No RSS feeds found in OPML')
|
||||
return {
|
||||
error: 'No RSS feeds found in OPML'
|
||||
}
|
||||
}
|
||||
|
||||
var rssFeedData = []
|
||||
|
||||
for (let feed of extractedFeeds) {
|
||||
var feedData = await this.getPodcastFeed(feed.feedUrl, true)
|
||||
if (feedData) {
|
||||
feedData.metadata.feedUrl = feed.feedUrl
|
||||
rssFeedData.push(feedData)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
feeds: rssFeedData
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = PodcastManager
|
||||
|
|
@ -2,7 +2,7 @@ const Path = require('path')
|
|||
const Logger = require('../../Logger')
|
||||
const BookMetadata = require('../metadata/BookMetadata')
|
||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
|
||||
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
|
||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||
const { readTextFile } = require('../../utils/fileUtils')
|
||||
const AudioFile = require('../files/AudioFile')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||
const parseNameString = require('../../utils/parseNameString')
|
||||
const parseNameString = require('../../utils/parsers/parseNameString')
|
||||
class BookMetadata {
|
||||
constructor(metadata) {
|
||||
this.title = null
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ class ApiRouter {
|
|||
//
|
||||
this.router.post('/podcasts', PodcastController.create.bind(this))
|
||||
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
|
||||
this.router.post('/podcasts/opml', PodcastController.getOPMLFeeds.bind(this))
|
||||
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
|
||||
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
|
||||
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
|
||||
|
|
|
|||
24
server/utils/parsers/parseOPML.js
Normal file
24
server/utils/parsers/parseOPML.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
const h = require('htmlparser2')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
function parse(opmlText) {
|
||||
var feeds = []
|
||||
var parser = new h.Parser({
|
||||
onopentag: (name, attribs) => {
|
||||
if (name === "outline" && attribs.type === 'rss') {
|
||||
if (!attribs.xmlurl) {
|
||||
Logger.error('[parseOPML] Invalid opml outline tag has no xmlurl attribute')
|
||||
} else {
|
||||
feeds.push({
|
||||
title: attribs.title || 'No Title',
|
||||
text: attribs.text || '',
|
||||
feedUrl: attribs.xmlurl
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
parser.write(opmlText)
|
||||
return feeds
|
||||
}
|
||||
module.exports.parse = parse
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
const { xmlToJSON } = require('./index')
|
||||
const htmlSanitizer = require('./htmlSanitizer')
|
||||
const { xmlToJSON } = require('../index')
|
||||
const htmlSanitizer = require('../htmlSanitizer')
|
||||
|
||||
function parseCreators(metadata) {
|
||||
if (!metadata['dc:creator']) return null
|
||||
|
|
@ -131,7 +131,7 @@ function extractPodcastEpisodes(items) {
|
|||
return episodes
|
||||
}
|
||||
|
||||
function cleanPodcastJson(rssJson) {
|
||||
function cleanPodcastJson(rssJson, excludeEpisodeMetadata) {
|
||||
if (!rssJson.channel || !rssJson.channel.length) {
|
||||
Logger.error(`[podcastUtil] Invalid podcast no channel object`)
|
||||
return null
|
||||
|
|
@ -142,13 +142,17 @@ function cleanPodcastJson(rssJson) {
|
|||
return null
|
||||
}
|
||||
var podcast = {
|
||||
metadata: extractPodcastMetadata(channel),
|
||||
episodes: extractPodcastEpisodes(channel.item)
|
||||
metadata: extractPodcastMetadata(channel)
|
||||
}
|
||||
if (!excludeEpisodeMetadata) {
|
||||
podcast.episodes = extractPodcastEpisodes(channel.item)
|
||||
} else {
|
||||
podcast.numEpisodes = channel.item.length
|
||||
}
|
||||
return podcast
|
||||
}
|
||||
|
||||
module.exports.parsePodcastRssFeedXml = async (xml, includeRaw = false) => {
|
||||
module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = false, includeRaw = false) => {
|
||||
if (!xml) return null
|
||||
var json = await xmlToJSON(xml)
|
||||
if (!json || !json.rss) {
|
||||
|
|
@ -156,7 +160,7 @@ module.exports.parsePodcastRssFeedXml = async (xml, includeRaw = false) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const podcast = cleanPodcastJson(json.rss)
|
||||
const podcast = cleanPodcastJson(json.rss, excludeEpisodeMetadata)
|
||||
if (!podcast) return null
|
||||
|
||||
if (includeRaw) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue