mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:RSS feeds for audiobooks #606
This commit is contained in:
		
							parent
							
								
									06cc2a1b21
								
							
						
					
					
						commit
						2a235b8324
					
				| @ -122,7 +122,7 @@ export default { | |||||||
| 
 | 
 | ||||||
|       console.log('Payload', payload) |       console.log('Payload', payload) | ||||||
|       this.$axios |       this.$axios | ||||||
|         .$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload) |         .$post(`/api/items/${this.libraryItemId}/open-feed`, payload) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|           if (data.success) { |           if (data.success) { | ||||||
|             console.log('Opened RSS Feed', data) |             console.log('Opened RSS Feed', data) | ||||||
| @ -143,7 +143,7 @@ export default { | |||||||
|     closeFeed() { |     closeFeed() { | ||||||
|       this.processing = true |       this.processing = true | ||||||
|       this.$axios |       this.$axios | ||||||
|         .$post(`/api/podcasts/${this.libraryItem.id}/close-feed`) |         .$post(`/api/items/${this.libraryItem.id}/close-feed`) | ||||||
|         .then(() => { |         .then(() => { | ||||||
|           this.$toast.success('RSS Feed Closed') |           this.$toast.success('RSS Feed Closed') | ||||||
|           this.show = false |           this.show = false | ||||||
|  | |||||||
| @ -383,10 +383,10 @@ export default { | |||||||
|       return this.$store.getters['user/getUserCanDownload'] |       return this.$store.getters['user/getUserCanDownload'] | ||||||
|     }, |     }, | ||||||
|     showRssFeedBtn() { |     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 |       // 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: { |   methods: { | ||||||
|  | |||||||
| @ -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) { |   middleware(req, res, next) { | ||||||
|     var item = this.db.libraryItems.find(li => li.id === req.params.id) |     var item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||||
|     if (!item || !item.media) return res.sendStatus(404) |     if (!item || !item.media) return res.sendStatus(404) | ||||||
|  | |||||||
| @ -173,37 +173,6 @@ class PodcastController { | |||||||
|     res.sendStatus(200) |     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) { |   async updateEpisode(req, res) { | ||||||
|     var libraryItem = req.libraryItem |     var libraryItem = req.libraryItem | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const fs = require('fs-extra') | const fs = require('fs-extra') | ||||||
|  | const date = require('date-and-time') | ||||||
| const { Podcast } = require('podcast') | const { Podcast } = require('podcast') | ||||||
| const { getId } = require('../utils/index') |  | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| 
 | 
 | ||||||
| // Not functional at the moment
 | // Not functional at the moment
 | ||||||
| @ -60,36 +60,72 @@ class RssFeedManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   openFeed(userId, slug, libraryItem, serverAddress) { |   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}` |     const feedUrl = `${serverAddress}/feed/${slug}` | ||||||
|     // Removed Podcast npm package and ip package
 |     const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName | ||||||
|  | 
 | ||||||
|     const feed = new Podcast({ |     const feed = new Podcast({ | ||||||
|       title: podcast.metadata.title, |       title: mediaMetadata.title, | ||||||
|       description: podcast.metadata.description, |       description: mediaMetadata.description, | ||||||
|       feedUrl, |       feedUrl, | ||||||
|       siteUrl: serverAddress, |       siteUrl: `${serverAddress}/items/${libraryItem.id}`, | ||||||
|       imageUrl: podcast.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`, |       imageUrl: media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`, | ||||||
|       author: podcast.metadata.author || 'advplyr', |       author: author || 'advplyr', | ||||||
|       language: 'en' |       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({ |     if (isPodcast) { // PODCAST EPISODES
 | ||||||
|         title: episode.title, |       media.episodes.forEach((episode) => { | ||||||
|         description: episode.description || '', |         var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/') | ||||||
|         enclosure: { |         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}`, |           url: `${serverAddress}${contentUrl}`, | ||||||
|           type: episode.audioTrack.mimeType, |           author: author || 'advplyr' | ||||||
|           size: episode.size |         }) | ||||||
|         }, |  | ||||||
|         date: episode.pubDate || '', |  | ||||||
|         url: `${serverAddress}${contentUrl}`, |  | ||||||
|         author: podcast.metadata.author || 'advplyr' |  | ||||||
|       }) |       }) | ||||||
|     }) |     } else { // AUDIOBOOK EPISODES
 | ||||||
|  | 
 | ||||||
|  |       // Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
 | ||||||
|  |       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 = { |     const feedData = { | ||||||
|       id: slug, |       id: slug, | ||||||
| @ -97,7 +133,7 @@ class RssFeedManager { | |||||||
|       userId, |       userId, | ||||||
|       libraryItemId: libraryItem.id, |       libraryItemId: libraryItem.id, | ||||||
|       libraryItemPath: libraryItem.path, |       libraryItemPath: libraryItem.path, | ||||||
|       mediaCoverPath: podcast.coverPath, |       mediaCoverPath: media.coverPath, | ||||||
|       serverAddress: serverAddress, |       serverAddress: serverAddress, | ||||||
|       feedUrl, |       feedUrl, | ||||||
|       feed |       feed | ||||||
| @ -106,7 +142,7 @@ class RssFeedManager { | |||||||
|     return feedData |     return feedData | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   openPodcastFeed(user, libraryItem, options) { |   openFeedForItem(user, libraryItem, options) { | ||||||
|     const serverAddress = options.serverAddress |     const serverAddress = options.serverAddress | ||||||
|     const slug = options.slug |     const slug = options.slug | ||||||
| 
 | 
 | ||||||
| @ -118,12 +154,12 @@ class RssFeedManager { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const feedData = this.openFeed(user.id, slug, libraryItem, serverAddress) |     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 }) |     this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl }) | ||||||
|     return feedData |     return feedData | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   closePodcastFeedForItem(libraryItemId) { |   closeFeedForItem(libraryItemId) { | ||||||
|     var feed = this.findFeedForItem(libraryItemId) |     var feed = this.findFeedForItem(libraryItemId) | ||||||
|     if (!feed) return |     if (!feed) return | ||||||
|     this.closeRssFeed(feed.id) |     this.closeRssFeed(feed.id) | ||||||
|  | |||||||
| @ -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/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.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/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/delete', LibraryItemController.batchDelete.bind(this)) | ||||||
|     this.router.post('/items/batch/update', LibraryItemController.batchUpdate.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/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.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/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)) |     this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this)) | ||||||
| 
 | 
 | ||||||
|     //
 |     //
 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user