From 678dceefed166d2932ff242cfc778de561d3512b Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 2 May 2022 16:42:30 -0500 Subject: [PATCH] Add:Experimental generate podcast RSS feed #553 --- .../components/modals/rssfeed/ViewModal.vue | 96 +++++++++++++++++++ client/pages/item/_id/index.vue | 49 ++++++++-- server/Server.js | 5 +- server/controllers/LibraryItemController.js | 5 + server/controllers/PodcastController.js | 13 ++- server/managers/RssFeedManager.js | 51 +++++++++- server/routers/ApiRouter.js | 1 + 7 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 client/components/modals/rssfeed/ViewModal.vue diff --git a/client/components/modals/rssfeed/ViewModal.vue b/client/components/modals/rssfeed/ViewModal.vue new file mode 100644 index 00000000..82a7193a --- /dev/null +++ b/client/components/modals/rssfeed/ViewModal.vue @@ -0,0 +1,96 @@ + + + diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index b57566a2..dbca379e 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -158,8 +158,8 @@ - - + + @@ -183,6 +183,7 @@ + @@ -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) diff --git a/server/Server.js b/server/Server.js index b3c9a159..70da7c86 100644 --- a/server/Server.js +++ b/server/Server.js @@ -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) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index e89605ba..91928d1c 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -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 => { diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 88cc0888..ea22ce0e 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -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 diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 867ce187..39dc96d2 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -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 \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 2e280efd..834ca62b 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -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)) //