mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update:RSS feed API routes
This commit is contained in:
		
							parent
							
								
									775dedc338
								
							
						
					
					
						commit
						e803dcd325
					
				| @ -6,13 +6,13 @@ | ||||
|       </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 v-if="currentFeedUrl" class="w-full"> | ||||
|       <div v-if="currentFeed" class="w-full"> | ||||
|         <p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p> | ||||
| 
 | ||||
|         <div class="w-full relative"> | ||||
|           <ui-text-input v-model="currentFeedUrl" readonly /> | ||||
|           <ui-text-input v-model="currentFeed.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(currentFeedUrl)">content_copy</span> | ||||
|           <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(currentFeed.feedUrl)">content_copy</span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div v-else class="w-full"> | ||||
| @ -28,7 +28,7 @@ | ||||
|       </div> | ||||
|       <div v-show="userIsAdminOrUp" class="flex items-center pt-6"> | ||||
|         <div class="flex-grow" /> | ||||
|         <ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn> | ||||
|         <ui-btn v-if="currentFeed" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn> | ||||
|         <ui-btn v-else color="success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn> | ||||
|       </div> | ||||
|     </div> | ||||
| @ -43,13 +43,16 @@ export default { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     }, | ||||
|     feedUrl: String | ||||
|     feed: { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       processing: false, | ||||
|       newFeedSlug: null, | ||||
|       currentFeedUrl: null | ||||
|       currentFeed: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
| @ -106,7 +109,7 @@ export default { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       var sanitized = this.$sanitizeSlug(this.newFeedSlug) | ||||
|       const sanitized = this.$sanitizeSlug(this.newFeedSlug) | ||||
|       if (this.newFeedSlug !== sanitized) { | ||||
|         this.newFeedSlug = sanitized | ||||
|         this.$toast.warning('Slug had to be modified - Run again') | ||||
| @ -121,19 +124,15 @@ export default { | ||||
| 
 | ||||
|       console.log('Payload', payload) | ||||
|       this.$axios | ||||
|         .$post(`/api/items/${this.libraryItemId}/open-feed`, payload) | ||||
|         .$post(`/api/feeds/item/${this.libraryItemId}/open`, payload) | ||||
|         .then((data) => { | ||||
|           if (data.success) { | ||||
|           console.log('Opened RSS Feed', data) | ||||
|             this.currentFeedUrl = data.feedUrl | ||||
|           } else { | ||||
|             const errorMsg = data.error || 'Unknown error' | ||||
|             this.$toast.error(errorMsg) | ||||
|           } | ||||
|           this.currentFeed = data.feed | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to open RSS Feed', error) | ||||
|           this.$toast.error() | ||||
|           const errorMsg = error.response ? error.response.data : null | ||||
|           this.$toast.error(errorMsg || 'Failed to open RSS Feed') | ||||
|         }) | ||||
|     }, | ||||
|     copyToClipboard(str) { | ||||
| @ -142,22 +141,23 @@ export default { | ||||
|     closeFeed() { | ||||
|       this.processing = true | ||||
|       this.$axios | ||||
|         .$post(`/api/items/${this.libraryItem.id}/close-feed`) | ||||
|         .$post(`/api/feeds/${this.currentFeed.id}/close`) | ||||
|         .then(() => { | ||||
|           this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess) | ||||
|           this.show = false | ||||
|           this.processing = false | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to close RSS feed', error) | ||||
|           this.processing = false | ||||
|           this.$toast.error(this.$strings.ToastRSSFeedCloseFailed) | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           this.processing = false | ||||
|         }) | ||||
|     }, | ||||
|     init() { | ||||
|       if (!this.libraryItem) return | ||||
|       this.newFeedSlug = this.libraryItem.id | ||||
|       this.currentFeedUrl = this.feedUrl | ||||
|       this.currentFeed = this.feed | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|  | ||||
| @ -174,7 +174,7 @@ | ||||
| 
 | ||||
|             <!-- RSS feed --> | ||||
|             <ui-tooltip v-if="showRssFeedBtn" :text="$strings.LabelOpenRSSFeed" direction="top"> | ||||
|               <ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" /> | ||||
|               <ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeed ? 'success' : 'primary'" outlined @click="clickRSSFeed" /> | ||||
|             </ui-tooltip> | ||||
|           </div> | ||||
| 
 | ||||
| @ -200,7 +200,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" /> | ||||
|     <modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed="rssFeed" /> | ||||
|     <modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" /> | ||||
|   </div> | ||||
| </template> | ||||
| @ -223,7 +223,7 @@ export default { | ||||
|     } | ||||
|     return { | ||||
|       libraryItem: item, | ||||
|       rssFeedUrl: item.rssFeedUrl || null | ||||
|       rssFeed: item.rssFeed || null | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
| @ -432,10 +432,10 @@ export default { | ||||
|       return this.$store.getters['user/getUserCanDownload'] | ||||
|     }, | ||||
|     showRssFeedBtn() { | ||||
|       if (!this.rssFeedUrl && !this.podcastEpisodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks | ||||
|       if (!this.rssFeed && !this.podcastEpisodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks | ||||
| 
 | ||||
|       // If rss feed is open then show feed url to users otherwise just show to admins | ||||
|       return this.userIsAdminOrUp || this.rssFeedUrl | ||||
|       return this.userIsAdminOrUp || this.rssFeed | ||||
|     }, | ||||
|     showQueueBtn() { | ||||
|       if (!this.isBook) return false | ||||
| @ -655,13 +655,13 @@ export default { | ||||
|     rssFeedOpen(data) { | ||||
|       if (data.entityId === this.libraryItemId) { | ||||
|         console.log('RSS Feed Opened', data) | ||||
|         this.rssFeedUrl = data.feedUrl | ||||
|         this.rssFeed = data | ||||
|       } | ||||
|     }, | ||||
|     rssFeedClosed(data) { | ||||
|       if (data.entityId === this.libraryItemId) { | ||||
|         console.log('RSS Feed Closed', data) | ||||
|         this.rssFeedUrl = null | ||||
|         this.rssFeed = null | ||||
|       } | ||||
|     }, | ||||
|     queueBtnClick() { | ||||
|  | ||||
| @ -21,8 +21,8 @@ class LibraryItemController { | ||||
|       } | ||||
| 
 | ||||
|       if (includeEntities.includes('rssfeed')) { | ||||
|         var feedData = this.rssFeedManager.findFeedForItem(item.id) | ||||
|         item.rssFeedUrl = feedData ? feedData.feedUrl : null | ||||
|         const feedData = this.rssFeedManager.findFeedForItem(item.id) | ||||
|         item.rssFeed = feedData ? feedData.toJSONMinified() : null | ||||
|       } | ||||
| 
 | ||||
|       if (item.mediaType == 'book') { | ||||
| @ -432,38 +432,6 @@ class LibraryItemController { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/items/:id/open-feed
 | ||||
|   async openRSSFeed(req, res) { | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       Logger.error(`[LibraryItemController] Non-admin user attempted to open RSS feed`, req.user.username) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     const feedData = await this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body) | ||||
|     if (feedData.error) { | ||||
|       return res.json({ | ||||
|         success: false, | ||||
|         error: feedData.error | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       feedUrl: feedData.feedUrl | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async closeRSSFeed(req, res) { | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       Logger.error(`[LibraryItemController] Non-admin user attempted to close RSS feed`, req.user.username) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     await this.rssFeedManager.closeFeedForItem(req.params.id) | ||||
| 
 | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async toneScan(req, res) { | ||||
|     if (!req.libraryItem.media.audioFiles.length) { | ||||
|       return res.sendStatus(404) | ||||
| @ -481,7 +449,7 @@ class LibraryItemController { | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     var item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     const item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!item || !item.media) return res.sendStatus(404) | ||||
| 
 | ||||
|     // Check user can access this library item
 | ||||
|  | ||||
							
								
								
									
										68
									
								
								server/controllers/RSSFeedController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								server/controllers/RSSFeedController.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| 
 | ||||
| class RSSFeedController { | ||||
|   constructor() { } | ||||
| 
 | ||||
|   // POST: api/feeds/item/:itemId/open
 | ||||
|   async openRSSFeedForItem(req, res) { | ||||
|     const options = req.body || {} | ||||
| 
 | ||||
|     const item = this.db.libraryItems.find(li => li.id === req.params.itemId) | ||||
|     if (!item) return res.sendStatus(404) | ||||
| 
 | ||||
|     // Check user can access this library item
 | ||||
|     if (!req.user.checkCanAccessLibraryItem(item)) { | ||||
|       Logger.error(`[RSSFeedController] User "${req.user.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     // Check request body options exist
 | ||||
|     if (!options.serverAddress || !options.slug) { | ||||
|       Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`) | ||||
|       return res.status(400).send('Invalid request body') | ||||
|     } | ||||
| 
 | ||||
|     // Check item has audio tracks
 | ||||
|     if (!item.media.numTracks) { | ||||
|       Logger.error(`[RSSFeedController] Cannot open RSS feed for item "${item.media.metadata.title}" because it has no audio tracks`) | ||||
|       return res.status(400).send('Item has no audio tracks') | ||||
|     } | ||||
| 
 | ||||
|     // Check that this slug is not being used for another feed (slug will also be the Feed id)
 | ||||
|     if (this.rssFeedManager.feeds[options.slug]) { | ||||
|       Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`) | ||||
|       return res.status(400).send('Slug already in use') | ||||
|     } | ||||
| 
 | ||||
|     const feed = await this.rssFeedManager.openFeedForItem(req.user, item, req.body) | ||||
|     res.json({ | ||||
|       feed: feed.toJSONMinified() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/feeds/:id/close
 | ||||
|   async closeRSSFeed(req, res) { | ||||
|     await this.rssFeedManager.closeRssFeed(req.params.id) | ||||
| 
 | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     if (!req.user.isAdminOrUp) { // Only admins can manage rss feeds
 | ||||
|       Logger.error(`[RSSFeedController] Non-admin user attempted to make a request to an RSS feed route`, req.user.username) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     if (req.params.id) { | ||||
|       const feed = this.rssFeedManager.findFeed(req.params.id) | ||||
|       if (!feed) { | ||||
|         Logger.error(`[RSSFeedController] RSS feed not found with id "${req.params.id}"`) | ||||
|         return res.sendStatus(404) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     next() | ||||
|   } | ||||
| } | ||||
| module.exports = new RSSFeedController() | ||||
| @ -18,10 +18,10 @@ class RssFeedManager { | ||||
|   } | ||||
| 
 | ||||
|   async init() { | ||||
|     var feedObjects = await this.db.getAllEntities('feed') | ||||
|     const feedObjects = await this.db.getAllEntities('feed') | ||||
|     if (feedObjects && feedObjects.length) { | ||||
|       feedObjects.forEach((feedObj) => { | ||||
|         var feed = new Feed(feedObj) | ||||
|         const feed = new Feed(feedObj) | ||||
|         this.feeds[feed.id] = feed | ||||
|         Logger.info(`[RssFeedManager] Opened rss feed ${feed.feedUrl}`) | ||||
|       }) | ||||
| @ -32,8 +32,12 @@ class RssFeedManager { | ||||
|     return Object.values(this.feeds).find(feed => feed.entityId === libraryItemId) | ||||
|   } | ||||
| 
 | ||||
|   findFeed(feedId) { | ||||
|     return this.feeds[feedId] || null | ||||
|   } | ||||
| 
 | ||||
|   async getFeed(req, res) { | ||||
|     var feed = this.feeds[req.params.id] | ||||
|     const feed = this.feeds[req.params.id] | ||||
|     if (!feed) { | ||||
|       Logger.debug(`[RssFeedManager] Feed not found ${req.params.id}`) | ||||
|       res.sendStatus(404) | ||||
| @ -49,19 +53,19 @@ class RssFeedManager { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     var xml = feed.buildXml() | ||||
|     const xml = feed.buildXml() | ||||
|     res.set('Content-Type', 'text/xml') | ||||
|     res.send(xml) | ||||
|   } | ||||
| 
 | ||||
|   getFeedItem(req, res) { | ||||
|     var feed = this.feeds[req.params.id] | ||||
|     const feed = this.feeds[req.params.id] | ||||
|     if (!feed) { | ||||
|       Logger.debug(`[RssFeedManager] Feed not found ${req.params.id}`) | ||||
|       res.sendStatus(404) | ||||
|       return | ||||
|     } | ||||
|     var episodePath = feed.getEpisodePath(req.params.episodeId) | ||||
|     const episodePath = feed.getEpisodePath(req.params.episodeId) | ||||
|     if (!episodePath) { | ||||
|       Logger.error(`[RssFeedManager] Feed episode not found ${req.params.episodeId}`) | ||||
|       res.sendStatus(404) | ||||
| @ -71,7 +75,7 @@ class RssFeedManager { | ||||
|   } | ||||
| 
 | ||||
|   getFeedCover(req, res) { | ||||
|     var feed = this.feeds[req.params.id] | ||||
|     const feed = this.feeds[req.params.id] | ||||
|     if (!feed) { | ||||
|       Logger.debug(`[RssFeedManager] Feed not found ${req.params.id}`) | ||||
|       res.sendStatus(404) | ||||
| @ -85,7 +89,7 @@ class RssFeedManager { | ||||
| 
 | ||||
|     const extname = Path.extname(feed.coverPath).toLowerCase().slice(1) | ||||
|     res.type(`image/${extname}`) | ||||
|     var readStream = fs.createReadStream(feed.coverPath) | ||||
|     const readStream = fs.createReadStream(feed.coverPath) | ||||
|     readStream.pipe(res) | ||||
|   } | ||||
| 
 | ||||
| @ -93,32 +97,25 @@ class RssFeedManager { | ||||
|     const serverAddress = options.serverAddress | ||||
|     const slug = options.slug | ||||
| 
 | ||||
|     if (this.feeds[slug]) { | ||||
|       Logger.error(`[RssFeedManager] Slug already in use`) | ||||
|       return { | ||||
|         error: `Slug "${slug}" already in use` | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const feed = new Feed() | ||||
|     feed.setFromItem(user.id, slug, libraryItem, serverAddress) | ||||
|     this.feeds[feed.id] = feed | ||||
| 
 | ||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed ${feed.feedUrl}`) | ||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) | ||||
|     await this.db.insertEntity('feed', feed) | ||||
|     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) | ||||
|     return feed | ||||
|   } | ||||
| 
 | ||||
|   closeFeedForItem(libraryItemId) { | ||||
|     var feed = this.findFeedForItem(libraryItemId) | ||||
|     const feed = this.findFeedForItem(libraryItemId) | ||||
|     if (!feed) return | ||||
|     return this.closeRssFeed(feed.id) | ||||
|   } | ||||
| 
 | ||||
|   async closeRssFeed(id) { | ||||
|     if (!this.feeds[id]) return | ||||
|     var feed = this.feeds[id] | ||||
|     const feed = this.feeds[id] | ||||
|     await this.db.removeEntity('feed', id) | ||||
|     SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified()) | ||||
|     delete this.feeds[id] | ||||
|  | ||||
| @ -23,6 +23,7 @@ const NotificationController = require('../controllers/NotificationController') | ||||
| const SearchController = require('../controllers/SearchController') | ||||
| const CacheController = require('../controllers/CacheController') | ||||
| const ToolsController = require('../controllers/ToolsController') | ||||
| const RSSFeedController = require('../controllers/RSSFeedController') | ||||
| const MiscController = require('../controllers/MiscController') | ||||
| 
 | ||||
| const BookFinder = require('../finders/BookFinder') | ||||
| @ -104,8 +105,6 @@ class ApiRouter { | ||||
|     this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) | ||||
|     this.router.get('/items/:id/tone-object', LibraryItemController.middleware.bind(this), LibraryItemController.getToneMetadataObject.bind(this)) | ||||
|     this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this)) | ||||
|     this.router.post('/items/:id/open-feed', LibraryItemController.middleware.bind(this), LibraryItemController.openRSSFeed.bind(this)) | ||||
|     this.router.post('/items/:id/close-feed', LibraryItemController.middleware.bind(this), LibraryItemController.closeRSSFeed.bind(this)) | ||||
|     this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this)) | ||||
| 
 | ||||
|     this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this)) | ||||
| @ -231,7 +230,7 @@ class ApiRouter { | ||||
|     this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Notification Routes
 | ||||
|     // Notification Routes (Admin and up)
 | ||||
|     //
 | ||||
|     this.router.get('/notifications', NotificationController.middleware.bind(this), NotificationController.get.bind(this)) | ||||
|     this.router.patch('/notifications', NotificationController.middleware.bind(this), NotificationController.update.bind(this)) | ||||
| @ -252,18 +251,24 @@ class ApiRouter { | ||||
|     this.router.get('/search/chapters', SearchController.findChapters.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Cache Routes
 | ||||
|     // Cache Routes (Admin and up)
 | ||||
|     //
 | ||||
|     this.router.post('/cache/purge', CacheController.purgeCache.bind(this)) | ||||
|     this.router.post('/cache/items/purge', CacheController.purgeItemsCache.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Tools Routes
 | ||||
|     // Tools Routes (Admin and up)
 | ||||
|     //
 | ||||
|     this.router.post('/tools/item/:id/encode-m4b', ToolsController.itemMiddleware.bind(this), ToolsController.encodeM4b.bind(this)) | ||||
|     this.router.delete('/tools/item/:id/encode-m4b', ToolsController.itemMiddleware.bind(this), ToolsController.cancelM4bEncode.bind(this)) | ||||
|     this.router.post('/tools/item/:id/embed-metadata', ToolsController.itemMiddleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this)) | ||||
| 
 | ||||
|     // 
 | ||||
|     // RSS Feed Routes (Admin and up)
 | ||||
|     //
 | ||||
|     this.router.post('/feeds/item/:itemId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForItem.bind(this)) | ||||
|     this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Misc Routes
 | ||||
|     //
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user