mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 16:44:16 +01:00
Add:Experimental generate podcast RSS feed #553
This commit is contained in:
parent
8b38dda229
commit
678dceefed
96
client/components/modals/rssfeed/ViewModal.vue
Normal file
96
client/components/modals/rssfeed/ViewModal.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="rss-feed-modal" :width="600" :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="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||
<div class="w-full">
|
||||
<p class="text-lg font-semibold mb-4">Podcast RSS Feed is Open</p>
|
||||
|
||||
<div class="w-full relative">
|
||||
<ui-text-input v-model="feedUrl" readonly />
|
||||
|
||||
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
feedUrl: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
title() {
|
||||
return this.mediaMetadata.title
|
||||
},
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
closeFeed() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$post(`/api/podcasts/${this.libraryItem.id}/close-feed`)
|
||||
.then(() => {
|
||||
this.$toast.success('RSS Feed Closed')
|
||||
this.show = false
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to close RSS feed', error)
|
||||
this.processing = false
|
||||
this.$toast.error()
|
||||
})
|
||||
},
|
||||
init() {}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -158,8 +158,8 @@
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Experimental RSS feed open -->
|
||||
<ui-tooltip v-if="isPodcast && showExperimentalFeatures" text="Open RSS Feed" direction="top">
|
||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" outlined @click="openRSSFeed" />
|
||||
<ui-tooltip v-if="showRssFeedBtn" text="Open RSS Feed" direction="top">
|
||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
@ -183,6 +183,7 @@
|
||||
</div>
|
||||
|
||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
||||
<modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -194,7 +195,7 @@ export default {
|
||||
}
|
||||
|
||||
// Include episode downloads for podcasts
|
||||
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=authors,downloads`).catch((error) => {
|
||||
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=authors,downloads,rssfeed`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
@ -203,7 +204,8 @@ export default {
|
||||
return redirect('/')
|
||||
}
|
||||
return {
|
||||
libraryItem: item
|
||||
libraryItem: item,
|
||||
rssFeedUrl: item.rssFeedUrl || null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -214,7 +216,8 @@ export default {
|
||||
showPodcastEpisodeFeed: false,
|
||||
podcastFeedEpisodes: [],
|
||||
episodesDownloading: [],
|
||||
episodeDownloadsQueued: []
|
||||
episodeDownloadsQueued: [],
|
||||
showRssFeedModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -373,6 +376,11 @@ export default {
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
showRssFeedBtn() {
|
||||
if (!this.showExperimentalFeatures) return false
|
||||
// If rss feed is open then show feed url to users otherwise just show to admins
|
||||
return this.isPodcast && (this.userIsAdminOrUp || this.rssFeedUrl)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -483,6 +491,15 @@ export default {
|
||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
||||
},
|
||||
clickRSSFeed() {
|
||||
if (!this.rssFeedUrl) {
|
||||
if (confirm(`Are you sure you want to open an RSS Feed for this podcast?`)) {
|
||||
this.openRSSFeed()
|
||||
}
|
||||
} else {
|
||||
this.showRssFeedModal = true
|
||||
}
|
||||
},
|
||||
openRSSFeed() {
|
||||
const payload = {
|
||||
serverAddress: window.origin
|
||||
@ -493,7 +510,11 @@ export default {
|
||||
this.$axios
|
||||
.$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload)
|
||||
.then((data) => {
|
||||
console.log('Opened RSS Feed', data)
|
||||
if (data.success) {
|
||||
console.log('Opened RSS Feed', data)
|
||||
this.rssFeedUrl = data.feedUrl
|
||||
this.showRssFeedModal = true
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to open RSS Feed', error)
|
||||
@ -515,6 +536,18 @@ export default {
|
||||
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
||||
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
|
||||
}
|
||||
},
|
||||
rssFeedOpen(data) {
|
||||
if (data.libraryItemId === this.libraryItemId) {
|
||||
console.log('RSS Feed Opened', data)
|
||||
this.rssFeedUrl = data.feedUrl
|
||||
}
|
||||
},
|
||||
rssFeedClosed(data) {
|
||||
if (data.libraryItemId === this.libraryItemId) {
|
||||
console.log('RSS Feed Closed', data)
|
||||
this.rssFeedUrl = null
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -527,12 +560,16 @@ export default {
|
||||
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||
}
|
||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
|
||||
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
|
||||
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
|
||||
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
|
||||
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
|
||||
|
@ -75,7 +75,7 @@ class Server {
|
||||
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this))
|
||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.rssFeedManager = new RssFeedManager(this.db)
|
||||
this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
|
||||
|
||||
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
||||
|
||||
@ -205,6 +205,9 @@ class Server {
|
||||
Logger.info(`[Server] requesting rss feed ${req.params.id}`)
|
||||
this.rssFeedManager.getFeed(req, res)
|
||||
})
|
||||
app.get('/feed/:id/cover', (req, res) => {
|
||||
this.rssFeedManager.getFeedCover(req, res)
|
||||
})
|
||||
app.get('/feed/:id/item/*', (req, res) => {
|
||||
Logger.info(`[Server] requesting rss feed ${req.params.id}`)
|
||||
this.rssFeedManager.getFeedItem(req, res)
|
||||
|
@ -17,6 +17,11 @@ class LibraryItemController {
|
||||
item.userMediaProgress = req.user.getMediaProgress(item.id, episodeId)
|
||||
}
|
||||
|
||||
if (includeEntities.includes('rssfeed')) {
|
||||
var feedData = this.rssFeedManager.findFeedForItem(item.id)
|
||||
item.rssFeedUrl = feedData ? feedData.feedUrl : null
|
||||
}
|
||||
|
||||
if (item.mediaType == 'book') {
|
||||
if (includeEntities.includes('authors')) {
|
||||
item.media.metadata.authors = item.media.metadata.authors.map(au => {
|
||||
|
@ -168,7 +168,7 @@ class PodcastController {
|
||||
|
||||
async openPodcastFeed(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to open podcast feed`, req.user)
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to open podcast feed`, req.user.username)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
@ -180,6 +180,17 @@ class PodcastController {
|
||||
})
|
||||
}
|
||||
|
||||
async closePodcastFeed(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to close podcast feed`, req.user.username)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
this.rssFeedManager.closePodcastFeedForItem(req.params.id)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async updateEpisode(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
|
||||
|
@ -5,11 +5,16 @@ const Logger = require('../Logger')
|
||||
|
||||
// Not functional at the moment
|
||||
class RssFeedManager {
|
||||
constructor(db) {
|
||||
constructor(db, emitter) {
|
||||
this.db = db
|
||||
this.emitter = emitter
|
||||
this.feeds = {}
|
||||
}
|
||||
|
||||
findFeedForItem(libraryItemId) {
|
||||
return Object.values(this.feeds).find(feed => feed.libraryItemId === libraryItemId)
|
||||
}
|
||||
|
||||
getFeed(req, res) {
|
||||
var feedData = this.feeds[req.params.id]
|
||||
if (!feedData) {
|
||||
@ -34,7 +39,26 @@ class RssFeedManager {
|
||||
res.sendFile(fullPath)
|
||||
}
|
||||
|
||||
openFeed(feedId, libraryItem, serverAddress) {
|
||||
getFeedCover(req, res) {
|
||||
var feedData = this.feeds[req.params.id]
|
||||
if (!feedData) {
|
||||
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
|
||||
if (!feedData.mediaCoverPath) {
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
|
||||
const extname = Path.extname(feedData.mediaCoverPath).toLowerCase().slice(1)
|
||||
res.type(`image/${extname}`)
|
||||
var readStream = fs.createReadStream(feedData.mediaCoverPath)
|
||||
readStream.pipe(res)
|
||||
}
|
||||
|
||||
openFeed(userId, feedId, libraryItem, serverAddress) {
|
||||
const podcast = libraryItem.media
|
||||
|
||||
const feedUrl = `${serverAddress}/feed/${feedId}`
|
||||
@ -43,7 +67,8 @@ class RssFeedManager {
|
||||
title: podcast.metadata.title,
|
||||
description: podcast.metadata.description,
|
||||
feedUrl,
|
||||
imageUrl: `${serverAddress}/Logo.png`,
|
||||
siteUrl: serverAddress,
|
||||
imageUrl: podcast.coverPath ? `${serverAddress}/feed/${feedId}/cover` : `${serverAddress}/Logo.png`,
|
||||
author: podcast.metadata.author || 'advplyr',
|
||||
language: 'en'
|
||||
})
|
||||
@ -59,6 +84,7 @@ class RssFeedManager {
|
||||
type: episode.audioTrack.mimeType,
|
||||
size: episode.size
|
||||
},
|
||||
date: episode.pubDate || '',
|
||||
url: `${serverAddress}${contentUrl}`,
|
||||
author: podcast.metadata.author || 'advplyr'
|
||||
})
|
||||
@ -66,8 +92,10 @@ class RssFeedManager {
|
||||
|
||||
const feedData = {
|
||||
id: feedId,
|
||||
userId,
|
||||
libraryItemId: libraryItem.id,
|
||||
libraryItemPath: libraryItem.path,
|
||||
mediaCoverPath: podcast.coverPath,
|
||||
serverAddress: serverAddress,
|
||||
feedUrl,
|
||||
feed
|
||||
@ -79,9 +107,24 @@ class RssFeedManager {
|
||||
openPodcastFeed(user, libraryItem, options) {
|
||||
const serverAddress = options.serverAddress
|
||||
const feedId = getId('feed')
|
||||
const feedData = this.openFeed(feedId, libraryItem, serverAddress)
|
||||
const feedData = this.openFeed(user.id, feedId, libraryItem, serverAddress)
|
||||
Logger.debug(`[RssFeedManager] Opened podcast feed ${feedData.feedUrl}`)
|
||||
this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl })
|
||||
return feedData
|
||||
}
|
||||
|
||||
closePodcastFeedForItem(libraryItemId) {
|
||||
var feed = this.findFeedForItem(libraryItemId)
|
||||
if (!feed) return
|
||||
this.closeRssFeed(feed.id)
|
||||
}
|
||||
|
||||
closeRssFeed(id) {
|
||||
if (!this.feeds[id]) return
|
||||
var feedData = this.feeds[id]
|
||||
this.emitter('rss_feed_closed', { libraryItemId: feedData.libraryItemId, feedUrl: feedData.feedUrl })
|
||||
delete this.feeds[id]
|
||||
Logger.info(`[RssFeedManager] Closed RSS feed "${feedData.feedUrl}"`)
|
||||
}
|
||||
}
|
||||
module.exports = RssFeedManager
|
@ -186,6 +186,7 @@ class ApiRouter {
|
||||
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
|
||||
this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this))
|
||||
this.router.post('/podcasts/:id/open-feed', PodcastController.middleware.bind(this), PodcastController.openPodcastFeed.bind(this))
|
||||
this.router.post('/podcasts/:id/close-feed', PodcastController.middleware.bind(this), PodcastController.closePodcastFeed.bind(this))
|
||||
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this))
|
||||
|
||||
//
|
||||
|
Loading…
Reference in New Issue
Block a user