mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Add:OPML Upload for bulk adding podcasts #588
This commit is contained in:
parent
e5469cc0f8
commit
514893646a
71
client/components/cards/PodcastFeedSummaryCard.vue
Normal file
71
client/components/cards/PodcastFeedSummaryCard.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="w-full p-2 border border-white border-opacity-10 rounded">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="w-16 min-w-16">
|
||||||
|
<div class="w-full h-16 bg-primary">
|
||||||
|
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-400 text-xxs pt-1 text-center">{{ numEpisodes }} Episodes</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow pl-2" :style="{ maxWidth: detailsWidth + 'px' }">
|
||||||
|
<p class="mb-1">{{ title }}</p>
|
||||||
|
<p class="text-xs mb-1 text-gray-300">{{ author }}</p>
|
||||||
|
<p class="text-xs mb-2 text-gray-200">{{ description }}</p>
|
||||||
|
<p class="text-xs truncate text-blue-200">
|
||||||
|
Folder: <span class="font-mono">{{ folderPath }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
feed: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
libraryFolderPath: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
width: 900
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return this.metadata.title || 'No Title'
|
||||||
|
},
|
||||||
|
image() {
|
||||||
|
return this.metadata.imageUrl
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.metadata.description || ''
|
||||||
|
},
|
||||||
|
author() {
|
||||||
|
return this.metadata.author || ''
|
||||||
|
},
|
||||||
|
metadata() {
|
||||||
|
return this.feed || {}
|
||||||
|
},
|
||||||
|
numEpisodes() {
|
||||||
|
return this.feed.numEpisodes || 0
|
||||||
|
},
|
||||||
|
folderPath() {
|
||||||
|
if (!this.libraryFolderPath) return ''
|
||||||
|
return `${this.libraryFolderPath}\\${this.$sanitizeFilename(this.title)}`
|
||||||
|
},
|
||||||
|
detailsWidth() {
|
||||||
|
return this.width - 85
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
updated() {
|
||||||
|
this.width = this.$refs.wrapper.clientWidth
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.width = this.$refs.wrapper.clientWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
168
client/components/modals/podcast/OpmlFeedsModal.vue
Normal file
168
client/components/modals/podcast/OpmlFeedsModal.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="opml-feeds-modal" :width="1000" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
|
<div class="w-full p-4">
|
||||||
|
<div class="flex items-center -mx-2 mb-2">
|
||||||
|
<div class="w-full md:w-2/3 p-2">
|
||||||
|
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" label="Folder" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full md:w-1/3 p-2 pt-6">
|
||||||
|
<ui-checkbox v-model="autoDownloadEpisodes" label="Auto Download New Episodes" checkbox-bg="primary" border-color="gray-600" label-class="text-sm font-semibold pl-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-lg font-semibold mb-2">Podcasts to Add</p>
|
||||||
|
|
||||||
|
<div class="w-full overflow-y-auto" style="max-height: 50vh">
|
||||||
|
<template v-for="(feed, index) in feedMetadata">
|
||||||
|
<cards-podcast-feed-summary-card :key="index" :feed="feed" :library-folder-path="selectedFolderPath" class="my-1" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center py-4">
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" @click="submit">Add Podcasts</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
feeds: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
selectedFolderId: null,
|
||||||
|
fullPath: null,
|
||||||
|
autoDownloadEpisodes: false,
|
||||||
|
feedMetadata: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return 'OPML Feeds'
|
||||||
|
},
|
||||||
|
currentLibrary() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
|
},
|
||||||
|
folders() {
|
||||||
|
if (!this.currentLibrary) return []
|
||||||
|
return this.currentLibrary.folders || []
|
||||||
|
},
|
||||||
|
folderItems() {
|
||||||
|
return this.folders.map((fold) => {
|
||||||
|
return {
|
||||||
|
value: fold.id,
|
||||||
|
text: fold.fullPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectedFolder() {
|
||||||
|
return this.folders.find((f) => f.id === this.selectedFolderId)
|
||||||
|
},
|
||||||
|
selectedFolderPath() {
|
||||||
|
if (!this.selectedFolder) return ''
|
||||||
|
return this.selectedFolder.fullPath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toFeedMetadata(feed) {
|
||||||
|
var metadata = feed.metadata
|
||||||
|
return {
|
||||||
|
title: metadata.title,
|
||||||
|
author: metadata.author,
|
||||||
|
description: metadata.description,
|
||||||
|
releaseDate: '',
|
||||||
|
genres: [...metadata.categories],
|
||||||
|
feedUrl: metadata.feedUrl,
|
||||||
|
imageUrl: metadata.image,
|
||||||
|
itunesPageUrl: '',
|
||||||
|
itunesId: '',
|
||||||
|
itunesArtistId: '',
|
||||||
|
language: '',
|
||||||
|
numEpisodes: feed.numEpisodes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.feedMetadata = this.feeds.map(this.toFeedMetadata)
|
||||||
|
|
||||||
|
if (this.folderItems[0]) {
|
||||||
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit() {
|
||||||
|
this.processing = true
|
||||||
|
var newFeedPayloads = this.feedMetadata.map((metadata) => {
|
||||||
|
return {
|
||||||
|
path: `${this.selectedFolderPath}\\${this.$sanitizeFilename(metadata.title)}`,
|
||||||
|
folderId: this.selectedFolderId,
|
||||||
|
libraryId: this.currentLibrary.id,
|
||||||
|
media: {
|
||||||
|
metadata: {
|
||||||
|
...metadata
|
||||||
|
},
|
||||||
|
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('New feed payloads', newFeedPayloads)
|
||||||
|
|
||||||
|
for (const podcastPayload of newFeedPayloads) {
|
||||||
|
await this.$axios
|
||||||
|
.$post('/api/podcasts', podcastPayload)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(`${podcastPayload.media.metadata.title}: Podcast created successfully`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to create podcast'
|
||||||
|
console.error('Failed to create podcast', podcastPayload, error)
|
||||||
|
this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#podcast-wrapper {
|
||||||
|
min-height: 400px;
|
||||||
|
max-height: 80vh;
|
||||||
|
}
|
||||||
|
#episodes-scroll {
|
||||||
|
max-height: calc(80vh - 200px);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<input ref="fileInput" id="hidden-input" type="file" :accept="accept" class="hidden" @change="inputChanged" />
|
<input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" />
|
||||||
<ui-btn @click="clickUpload" color="primary" type="text"><slot /></ui-btn>
|
<ui-btn @click="clickUpload" color="primary" type="text"><slot /></ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<app-book-shelf-toolbar page="podcast-search" />
|
<app-book-shelf-toolbar page="podcast-search" />
|
||||||
|
|
||||||
<div class="w-full h-full overflow-y-auto p-12 relative">
|
<div class="w-full h-full overflow-y-auto p-12 relative">
|
||||||
<div class="w-full max-w-3xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto flex">
|
||||||
<form @submit.prevent="submit" class="flex">
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
<ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2" />
|
<ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2" />
|
||||||
<ui-btn type="submit" :disabled="processing">Submit</ui-btn>
|
<ui-btn type="submit" :disabled="processing">Submit</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
|
<ui-file-input :accept="'.opml, .txt'" class="mx-2" @change="opmlFileUpload"> Upload OPML File </ui-file-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full max-w-3xl mx-auto py-4">
|
<div class="w-full max-w-3xl mx-auto py-4">
|
||||||
@ -32,6 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-podcast-new-modal v-model="showNewPodcastModal" :podcast-data="selectedPodcast" :podcast-feed-data="selectedPodcastFeed" />
|
<modals-podcast-new-modal v-model="showNewPodcastModal" :podcast-data="selectedPodcast" :podcast-feed-data="selectedPodcastFeed" />
|
||||||
|
<modals-podcast-opml-feeds-modal v-model="showOPMLFeedsModal" :feeds="opmlFeeds" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -62,7 +65,9 @@ export default {
|
|||||||
processing: false,
|
processing: false,
|
||||||
showNewPodcastModal: false,
|
showNewPodcastModal: false,
|
||||||
selectedPodcast: null,
|
selectedPodcast: null,
|
||||||
selectedPodcastFeed: null
|
selectedPodcastFeed: null,
|
||||||
|
showOPMLFeedsModal: false,
|
||||||
|
opmlFeeds: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -71,6 +76,36 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async opmlFileUpload(file) {
|
||||||
|
this.processing = true
|
||||||
|
var txt = await new Promise((resolve) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result)
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!txt || !txt.includes('<opml') || !txt.includes('<outline ')) {
|
||||||
|
// Quick lazy check for valid OPML
|
||||||
|
this.$toast.error('Invalid OPML file <opml> tag not found OR an <outline> tag was not found')
|
||||||
|
this.processing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$axios
|
||||||
|
.$post(`/api/podcasts/opml`, { opmlText: txt })
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data)
|
||||||
|
this.opmlFeeds = data.feeds || []
|
||||||
|
this.showOPMLFeedsModal = true
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to parse OPML file')
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
},
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.searchInput) return
|
if (!this.searchInput) return
|
||||||
|
|
||||||
|
@ -64,8 +64,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input ref="fileInput" id="hidden-input" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
<input ref="fileInput" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
||||||
<input ref="fileFolderInput" id="hidden-input" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
<input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ module.exports = {
|
|||||||
minWidth: {
|
minWidth: {
|
||||||
'6': '1.5rem',
|
'6': '1.5rem',
|
||||||
'12': '3rem',
|
'12': '3rem',
|
||||||
|
'16': '4rem',
|
||||||
'24': '6rem',
|
'24': '6rem',
|
||||||
'32': '8rem',
|
'32': '8rem',
|
||||||
'48': '12rem',
|
'48': '12rem',
|
||||||
@ -75,6 +76,9 @@ module.exports = {
|
|||||||
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
||||||
book: ['Gentium Book Basic', 'serif']
|
book: ['Gentium Book Basic', 'serif']
|
||||||
},
|
},
|
||||||
|
fontSize: {
|
||||||
|
xxs: '0.625rem'
|
||||||
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
'50': 50
|
'50': 50
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ class PodcastController {
|
|||||||
return res.status(500).send('Bad response from feed request')
|
return res.status(500).send('Bad response from feed request')
|
||||||
}
|
}
|
||||||
Logger.debug(`[PdocastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
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) {
|
if (!payload) {
|
||||||
return res.status(500).send('Invalid podcast RSS feed')
|
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) {
|
async checkNewEpisodes(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user)
|
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 Logger = require('../Logger')
|
||||||
|
|
||||||
const { downloadFile } = require('../utils/fileUtils')
|
const { downloadFile } = require('../utils/fileUtils')
|
||||||
|
const opmlParser = require('../utils/parsers/parseOPML')
|
||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
||||||
@ -258,7 +259,7 @@ class PodcastManager {
|
|||||||
return newEpisodes
|
return newEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
getPodcastFeed(feedUrl) {
|
getPodcastFeed(feedUrl, excludeEpisodeMetadata = false) {
|
||||||
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}"`)
|
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}"`)
|
||||||
return axios.get(feedUrl, { timeout: 5000 }).then(async (data) => {
|
return axios.get(feedUrl, { timeout: 5000 }).then(async (data) => {
|
||||||
if (!data || !data.data) {
|
if (!data || !data.data) {
|
||||||
@ -266,7 +267,7 @@ class PodcastManager {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}" success - parsing xml`)
|
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}" success - parsing xml`)
|
||||||
var payload = await parsePodcastRssFeedXml(data.data)
|
var payload = await parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata)
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -276,5 +277,29 @@ class PodcastManager {
|
|||||||
return false
|
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
|
module.exports = PodcastManager
|
@ -2,7 +2,7 @@ const Path = require('path')
|
|||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const BookMetadata = require('../metadata/BookMetadata')
|
const BookMetadata = require('../metadata/BookMetadata')
|
||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
|
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
|
||||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||||
const { readTextFile } = require('../../utils/fileUtils')
|
const { readTextFile } = require('../../utils/fileUtils')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
const parseNameString = require('../../utils/parseNameString')
|
const parseNameString = require('../../utils/parsers/parseNameString')
|
||||||
class BookMetadata {
|
class BookMetadata {
|
||||||
constructor(metadata) {
|
constructor(metadata) {
|
||||||
this.title = null
|
this.title = null
|
||||||
|
@ -182,6 +182,7 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
this.router.post('/podcasts', PodcastController.create.bind(this))
|
this.router.post('/podcasts', PodcastController.create.bind(this))
|
||||||
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.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/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/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
|
||||||
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.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 { xmlToJSON } = require('../index')
|
||||||
const htmlSanitizer = require('./htmlSanitizer')
|
const htmlSanitizer = require('../htmlSanitizer')
|
||||||
|
|
||||||
function parseCreators(metadata) {
|
function parseCreators(metadata) {
|
||||||
if (!metadata['dc:creator']) return null
|
if (!metadata['dc:creator']) return null
|
@ -131,7 +131,7 @@ function extractPodcastEpisodes(items) {
|
|||||||
return episodes
|
return episodes
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanPodcastJson(rssJson) {
|
function cleanPodcastJson(rssJson, excludeEpisodeMetadata) {
|
||||||
if (!rssJson.channel || !rssJson.channel.length) {
|
if (!rssJson.channel || !rssJson.channel.length) {
|
||||||
Logger.error(`[podcastUtil] Invalid podcast no channel object`)
|
Logger.error(`[podcastUtil] Invalid podcast no channel object`)
|
||||||
return null
|
return null
|
||||||
@ -142,13 +142,17 @@ function cleanPodcastJson(rssJson) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
var podcast = {
|
var podcast = {
|
||||||
metadata: extractPodcastMetadata(channel),
|
metadata: extractPodcastMetadata(channel)
|
||||||
episodes: extractPodcastEpisodes(channel.item)
|
}
|
||||||
|
if (!excludeEpisodeMetadata) {
|
||||||
|
podcast.episodes = extractPodcastEpisodes(channel.item)
|
||||||
|
} else {
|
||||||
|
podcast.numEpisodes = channel.item.length
|
||||||
}
|
}
|
||||||
return podcast
|
return podcast
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.parsePodcastRssFeedXml = async (xml, includeRaw = false) => {
|
module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = false, includeRaw = false) => {
|
||||||
if (!xml) return null
|
if (!xml) return null
|
||||||
var json = await xmlToJSON(xml)
|
var json = await xmlToJSON(xml)
|
||||||
if (!json || !json.rss) {
|
if (!json || !json.rss) {
|
||||||
@ -156,7 +160,7 @@ module.exports.parsePodcastRssFeedXml = async (xml, includeRaw = false) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const podcast = cleanPodcastJson(json.rss)
|
const podcast = cleanPodcastJson(json.rss, excludeEpisodeMetadata)
|
||||||
if (!podcast) return null
|
if (!podcast) return null
|
||||||
|
|
||||||
if (includeRaw) {
|
if (includeRaw) {
|
||||||
|
Loading…
Reference in New Issue
Block a user