From e803dcd325b38a708651c7023b77d5c183b128ec Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 26 Dec 2022 16:58:36 -0600 Subject: [PATCH] Update:RSS feed API routes --- .../components/modals/rssfeed/ViewModal.vue | 40 +++++------ client/pages/item/_id/index.vue | 14 ++-- server/controllers/LibraryItemController.js | 38 +---------- server/controllers/RSSFeedController.js | 68 +++++++++++++++++++ server/managers/RssFeedManager.js | 33 ++++----- server/routers/ApiRouter.js | 15 ++-- 6 files changed, 123 insertions(+), 85 deletions(-) create mode 100644 server/controllers/RSSFeedController.js diff --git a/client/components/modals/rssfeed/ViewModal.vue b/client/components/modals/rssfeed/ViewModal.vue index 32f871ae..0133dc9f 100644 --- a/client/components/modals/rssfeed/ViewModal.vue +++ b/client/components/modals/rssfeed/ViewModal.vue @@ -6,13 +6,13 @@
-
+

{{ $strings.HeaderRSSFeedIsOpen }}

- + - content_copy + content_copy
@@ -28,7 +28,7 @@
- {{ $strings.ButtonCloseFeed }} + {{ $strings.ButtonCloseFeed }} {{ $strings.ButtonOpenFeed }}
@@ -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) - } + console.log('Opened RSS Feed', data) + 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() {} diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 15890f63..c36fa8e2 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -174,7 +174,7 @@ - +
@@ -200,7 +200,7 @@
- + @@ -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() { diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 7c3c9c08..c439f091 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -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 diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js new file mode 100644 index 00000000..fc151a1d --- /dev/null +++ b/server/controllers/RSSFeedController.js @@ -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() \ No newline at end of file diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 42892c90..fc87c309 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -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] diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index a4baa06b..05fc135e 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -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 //