diff --git a/client/components/modals/rssfeed/ViewModal.vue b/client/components/modals/rssfeed/ViewModal.vue index 56b036f5..c4a8c4ab 100644 --- a/client/components/modals/rssfeed/ViewModal.vue +++ b/client/components/modals/rssfeed/ViewModal.vue @@ -122,7 +122,7 @@ export default { console.log('Payload', payload) this.$axios - .$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload) + .$post(`/api/items/${this.libraryItemId}/open-feed`, payload) .then((data) => { if (data.success) { console.log('Opened RSS Feed', data) @@ -143,7 +143,7 @@ export default { closeFeed() { this.processing = true this.$axios - .$post(`/api/podcasts/${this.libraryItem.id}/close-feed`) + .$post(`/api/items/${this.libraryItem.id}/close-feed`) .then(() => { this.$toast.success('RSS Feed Closed') this.show = false diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 1097de37..2b25d0a1 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -383,10 +383,10 @@ export default { return this.$store.getters['user/getUserCanDownload'] }, showRssFeedBtn() { - if (!this.rssFeedUrl && !this.podcastEpisodes.length) return false // Cannot open RSS feed with no episodes + if (!this.rssFeedUrl && !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.isPodcast && (this.userIsAdminOrUp || this.rssFeedUrl) + return this.userIsAdminOrUp || this.rssFeedUrl } }, methods: { diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 88600323..6765e5f1 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -405,6 +405,38 @@ 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(500) + } + + const feedData = 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(500) + } + + this.rssFeedManager.closeFeedForItem(req.params.id) + + res.sendStatus(200) + } + middleware(req, res, next) { var item = this.db.libraryItems.find(li => li.id === req.params.id) if (!item || !item.media) return res.sendStatus(404) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index c3de4e57..e83e108f 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -173,37 +173,6 @@ class PodcastController { res.sendStatus(200) } - async openPodcastFeed(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to open podcast feed`, req.user.username) - return res.sendStatus(500) - } - - const feedData = this.rssFeedManager.openPodcastFeed(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 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 454e8a75..7d46381e 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -1,7 +1,7 @@ const Path = require('path') const fs = require('fs-extra') +const date = require('date-and-time') const { Podcast } = require('podcast') -const { getId } = require('../utils/index') const Logger = require('../Logger') // Not functional at the moment @@ -60,36 +60,72 @@ class RssFeedManager { } openFeed(userId, slug, libraryItem, serverAddress) { - const podcast = libraryItem.media + const media = libraryItem.media + const mediaMetadata = media.metadata + const isPodcast = libraryItem.mediaType === 'podcast' const feedUrl = `${serverAddress}/feed/${slug}` - // Removed Podcast npm package and ip package + const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName + const feed = new Podcast({ - title: podcast.metadata.title, - description: podcast.metadata.description, + title: mediaMetadata.title, + description: mediaMetadata.description, feedUrl, - siteUrl: serverAddress, - imageUrl: podcast.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`, - author: podcast.metadata.author || 'advplyr', + siteUrl: `${serverAddress}/items/${libraryItem.id}`, + imageUrl: media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`, + author: author || 'advplyr', language: 'en' }) - podcast.episodes.forEach((episode) => { - var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/') - contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`) - feed.addItem({ - title: episode.title, - description: episode.description || '', - enclosure: { + if (isPodcast) { // PODCAST EPISODES + media.episodes.forEach((episode) => { + var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/') + contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`) + + feed.addItem({ + title: episode.title, + description: episode.description || '', + enclosure: { + url: `${serverAddress}${contentUrl}`, + type: episode.audioTrack.mimeType, + size: episode.size + }, + date: episode.pubDate || '', url: `${serverAddress}${contentUrl}`, - type: episode.audioTrack.mimeType, - size: episode.size - }, - date: episode.pubDate || '', - url: `${serverAddress}${contentUrl}`, - author: podcast.metadata.author || 'advplyr' + author: author || 'advplyr' + }) }) - }) + } else { // AUDIOBOOK EPISODES + + // Example: Fri, 04 Feb 2015 00:00:00 GMT + const audiobookPubDate = date.format(new Date(libraryItem.addedAt), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') + + media.tracks.forEach((audioTrack) => { + var contentUrl = audioTrack.contentUrl.replace(/\\/g, '/') + contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`) + + var title = audioTrack.title + if (media.chapters.length) { + // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title + var matchingChapter = media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1) + if (matchingChapter && matchingChapter.title) title = matchingChapter.title + } + + feed.addItem({ + title, + description: '', + enclosure: { + url: `${serverAddress}${contentUrl}`, + type: audioTrack.mimeType, + size: audioTrack.metadata.size + }, + date: audiobookPubDate, + url: `${serverAddress}${contentUrl}`, + author: author || 'advplyr' + }) + }) + } + const feedData = { id: slug, @@ -97,7 +133,7 @@ class RssFeedManager { userId, libraryItemId: libraryItem.id, libraryItemPath: libraryItem.path, - mediaCoverPath: podcast.coverPath, + mediaCoverPath: media.coverPath, serverAddress: serverAddress, feedUrl, feed @@ -106,7 +142,7 @@ class RssFeedManager { return feedData } - openPodcastFeed(user, libraryItem, options) { + openFeedForItem(user, libraryItem, options) { const serverAddress = options.serverAddress const slug = options.slug @@ -118,12 +154,12 @@ class RssFeedManager { } const feedData = this.openFeed(user.id, slug, libraryItem, serverAddress) - Logger.debug(`[RssFeedManager] Opened podcast feed ${feedData.feedUrl}`) + Logger.debug(`[RssFeedManager] Opened RSS feed ${feedData.feedUrl}`) this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl }) return feedData } - closePodcastFeedForItem(libraryItemId) { + closeFeedForItem(libraryItemId) { var feed = this.findFeedForItem(libraryItemId) if (!feed) return this.closeRssFeed(feed.id) diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 7b0a3451..d2b67114 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -95,6 +95,8 @@ class ApiRouter { this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.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/batch/delete', LibraryItemController.batchDelete.bind(this)) this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this)) @@ -186,8 +188,6 @@ class ApiRouter { this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this)) 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)) //