mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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