mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Persist RSS feeds in db #696, Update:RSS feed data model
This commit is contained in:
		
							parent
							
								
									881baa818d
								
							
						
					
					
						commit
						fbbceaa642
					
				| @ -27,7 +27,6 @@ | ||||
|         <p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p> | ||||
|       </div> | ||||
|       <div v-show="userIsAdminOrUp" class="flex items-center pt-6"> | ||||
|         <p class="text-xs text-gray-300">Note: RSS feed URLs are not authenticated</p> | ||||
|         <div class="flex-grow" /> | ||||
|         <ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn> | ||||
|         <ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn> | ||||
|  | ||||
| @ -534,13 +534,13 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     rssFeedOpen(data) { | ||||
|       if (data.libraryItemId === this.libraryItemId) { | ||||
|       if (data.entityId === this.libraryItemId) { | ||||
|         console.log('RSS Feed Opened', data) | ||||
|         this.rssFeedUrl = data.feedUrl | ||||
|       } | ||||
|     }, | ||||
|     rssFeedClosed(data) { | ||||
|       if (data.libraryItemId === this.libraryItemId) { | ||||
|       if (data.entityId === this.libraryItemId) { | ||||
|         console.log('RSS Feed Closed', data) | ||||
|         this.rssFeedUrl = null | ||||
|       } | ||||
|  | ||||
							
								
								
									
										29
									
								
								server/Db.js
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								server/Db.js
									
									
									
									
									
								
							| @ -11,6 +11,7 @@ const Author = require('./objects/entities/Author') | ||||
| const Series = require('./objects/entities/Series') | ||||
| const ServerSettings = require('./objects/settings/ServerSettings') | ||||
| const PlaybackSession = require('./objects/PlaybackSession') | ||||
| const Feed = require('./objects/Feed') | ||||
| 
 | ||||
| class Db { | ||||
|   constructor() { | ||||
| @ -22,6 +23,7 @@ class Db { | ||||
|     this.CollectionsPath = Path.join(global.ConfigPath, 'collections') | ||||
|     this.AuthorsPath = Path.join(global.ConfigPath, 'authors') | ||||
|     this.SeriesPath = Path.join(global.ConfigPath, 'series') | ||||
|     this.FeedsPath = Path.join(global.ConfigPath, 'feeds') | ||||
| 
 | ||||
|     this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath) | ||||
|     this.usersDb = new njodb.Database(this.UsersPath) | ||||
| @ -31,6 +33,7 @@ class Db { | ||||
|     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 }) | ||||
|     this.authorsDb = new njodb.Database(this.AuthorsPath) | ||||
|     this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 }) | ||||
|     this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 }) | ||||
| 
 | ||||
|     this.libraryItems = [] | ||||
|     this.users = [] | ||||
| @ -59,6 +62,7 @@ class Db { | ||||
|     else if (entityName === 'collection') return this.collectionsDb | ||||
|     else if (entityName === 'author') return this.authorsDb | ||||
|     else if (entityName === 'series') return this.seriesDb | ||||
|     else if (entityName === 'feed') return this.feedsDb | ||||
|     return null | ||||
|   } | ||||
| 
 | ||||
| @ -71,6 +75,7 @@ class Db { | ||||
|     else if (entityName === 'collection') return 'collections' | ||||
|     else if (entityName === 'author') return 'authors' | ||||
|     else if (entityName === 'series') return 'series' | ||||
|     else if (entityName === 'feed') return 'feeds' | ||||
|     return null | ||||
|   } | ||||
| 
 | ||||
| @ -83,6 +88,7 @@ class Db { | ||||
|     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 }) | ||||
|     this.authorsDb = new njodb.Database(this.AuthorsPath) | ||||
|     this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 }) | ||||
|     this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 }) | ||||
|     return this.init() | ||||
|   } | ||||
| 
 | ||||
| @ -116,21 +122,6 @@ class Db { | ||||
|   async init() { | ||||
|     await this.load() | ||||
| 
 | ||||
|     // Insert Defaults
 | ||||
|     // var rootUser = this.users.find(u => u.type === 'root')
 | ||||
|     // if (!rootUser) {
 | ||||
|     //   var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET)
 | ||||
|     //   Logger.debug('Generated default token', token)
 | ||||
|     //   Logger.info('[Db] Root user created')
 | ||||
|     //   await this.insertEntity('user', this.getDefaultUser(token))
 | ||||
|     // } else {
 | ||||
|     //   Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`)
 | ||||
|     // }
 | ||||
| 
 | ||||
|     // if (!this.libraries.length) {
 | ||||
|     //   await this.insertEntity('library', this.getDefaultLibrary())
 | ||||
|     // }
 | ||||
| 
 | ||||
|     if (!this.serverSettings) { | ||||
|       this.serverSettings = new ServerSettings() | ||||
|       await this.insertEntity('settings', this.serverSettings) | ||||
| @ -278,6 +269,14 @@ class Db { | ||||
|     return this.updateEntity('settings', this.serverSettings) | ||||
|   } | ||||
| 
 | ||||
|   getAllEntities(entityName) { | ||||
|     var entityDb = this.getEntityDb(entityName) | ||||
|     return entityDb.select(() => true).then((results) => results.data).catch((error) => { | ||||
|       Logger.error(`[DB] Failed to get all ${entityName}`, error) | ||||
|       return null | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   insertEntities(entityName, entities) { | ||||
|     var entityDb = this.getEntityDb(entityName) | ||||
|     return entityDb.insert(entities).then((results) => { | ||||
|  | ||||
| @ -142,6 +142,7 @@ class Server { | ||||
| 
 | ||||
|     await this.backupManager.init() | ||||
|     await this.logManager.init() | ||||
|     await this.rssFeedManager.init() | ||||
|     this.podcastManager.init() | ||||
| 
 | ||||
|     if (this.db.serverSettings.scannerDisableWatcher) { | ||||
| @ -194,14 +195,14 @@ class Server { | ||||
| 
 | ||||
|     // RSS Feed temp route
 | ||||
|     app.get('/feed/:id', (req, res) => { | ||||
|       Logger.info(`[Server] requesting rss feed ${req.params.id}`) | ||||
|       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}`) | ||||
|     app.get('/feed/:id/item/:episodeId/*', (req, res) => { | ||||
|       Logger.debug(`[Server] Requesting rss feed episode ${req.params.id}/${req.params.episodeId}`) | ||||
|       this.rssFeedManager.getFeedItem(req, res) | ||||
|     }) | ||||
| 
 | ||||
|  | ||||
| @ -378,7 +378,7 @@ class LibraryItemController { | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
| 
 | ||||
|     const feedData = this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body) | ||||
|     const feedData = await this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body) | ||||
|     if (feedData.error) { | ||||
|       return res.json({ | ||||
|         success: false, | ||||
| @ -398,7 +398,7 @@ class LibraryItemController { | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
| 
 | ||||
|     this.rssFeedManager.closeFeedForItem(req.params.id) | ||||
|     await this.rssFeedManager.closeFeedForItem(req.params.id) | ||||
| 
 | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
|  | ||||
| @ -2,6 +2,7 @@ const Path = require('path') | ||||
| const fs = require('fs-extra') | ||||
| const date = require('date-and-time') | ||||
| const { Podcast } = require('podcast') | ||||
| const Feed = require('../objects/Feed') | ||||
| const Logger = require('../Logger') | ||||
| 
 | ||||
| // Not functional at the moment
 | ||||
| @ -12,137 +13,73 @@ class RssFeedManager { | ||||
|     this.feeds = {} | ||||
|   } | ||||
| 
 | ||||
|   async init() { | ||||
|     var feedObjects = await this.db.getAllEntities('feed') | ||||
|     if (feedObjects && feedObjects.length) { | ||||
|       feedObjects.forEach((feedObj) => { | ||||
|         var feed = new Feed(feedObj) | ||||
|         this.feeds[feed.id] = feed | ||||
|         Logger.info(`[RssFeedManager] Opened rss feed ${feed.feedUrl}`) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   findFeedForItem(libraryItemId) { | ||||
|     return Object.values(this.feeds).find(feed => feed.libraryItemId === libraryItemId) | ||||
|     return Object.values(this.feeds).find(feed => feed.entityId === libraryItemId) | ||||
|   } | ||||
| 
 | ||||
|   getFeed(req, res) { | ||||
|     var feedData = this.feeds[req.params.id] | ||||
|     if (!feedData) { | ||||
|     var feed = this.feeds[req.params.id] | ||||
|     if (!feed) { | ||||
|       Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`) | ||||
|       res.sendStatus(404) | ||||
|       return | ||||
|     } | ||||
|     var xml = feedData.feed.buildXml() | ||||
|     // var xml = feedData.feed.buildXml()
 | ||||
|     var xml = feed.buildXml() | ||||
|     res.set('Content-Type', 'text/xml') | ||||
|     res.send(xml) | ||||
|   } | ||||
| 
 | ||||
|   getFeedItem(req, res) { | ||||
|     var feedData = this.feeds[req.params.id] | ||||
|     if (!feedData) { | ||||
|     var feed = this.feeds[req.params.id] | ||||
|     if (!feed) { | ||||
|       Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`) | ||||
|       res.sendStatus(404) | ||||
|       return | ||||
|     } | ||||
|     var remainingPath = req.params['0'] | ||||
|     var fullPath = Path.join(feedData.libraryItemPath, remainingPath) | ||||
|     res.sendFile(fullPath) | ||||
|     var episodePath = feed.getEpisodePath(req.params.episodeId) | ||||
|     if (!episodePath) { | ||||
|       Logger.error(`[RssFeedManager] Feed episode not found ${req.params.episodeId}`) | ||||
|       res.sendStatus(404) | ||||
|       return | ||||
|     } | ||||
|     res.sendFile(episodePath) | ||||
|     // var remainingPath = req.params['0']
 | ||||
|     // var fullPath = Path.join(feedData.libraryItemPath, remainingPath)
 | ||||
|     // res.sendFile(fullPath)
 | ||||
|   } | ||||
| 
 | ||||
|   getFeedCover(req, res) { | ||||
|     var feedData = this.feeds[req.params.id] | ||||
|     if (!feedData) { | ||||
|     var feed = this.feeds[req.params.id] | ||||
|     if (!feed) { | ||||
|       Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`) | ||||
|       res.sendStatus(404) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     if (!feedData.mediaCoverPath) { | ||||
|     if (!feed.coverPath) { | ||||
|       res.sendStatus(404) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     const extname = Path.extname(feedData.mediaCoverPath).toLowerCase().slice(1) | ||||
|     const extname = Path.extname(feedData.coverPath).toLowerCase().slice(1) | ||||
|     res.type(`image/${extname}`) | ||||
|     var readStream = fs.createReadStream(feedData.mediaCoverPath) | ||||
|     var readStream = fs.createReadStream(feedData.coverPath) | ||||
|     readStream.pipe(res) | ||||
|   } | ||||
| 
 | ||||
|   openFeed(userId, slug, libraryItem, serverAddress) { | ||||
|     const media = libraryItem.media | ||||
|     const mediaMetadata = media.metadata | ||||
|     const isPodcast = libraryItem.mediaType === 'podcast' | ||||
| 
 | ||||
|     const feedUrl = `${serverAddress}/feed/${slug}` | ||||
|     const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName | ||||
| 
 | ||||
|     const feed = new Podcast({ | ||||
|       title: mediaMetadata.title, | ||||
|       description: mediaMetadata.description, | ||||
|       feedUrl, | ||||
|       siteUrl: `${serverAddress}/items/${libraryItem.id}`, | ||||
|       imageUrl: media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`, | ||||
|       author: author || 'advplyr', | ||||
|       language: 'en' | ||||
|     }) | ||||
| 
 | ||||
|     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}`, | ||||
|           author: 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 = { | ||||
|       id: slug, | ||||
|       slug, | ||||
|       userId, | ||||
|       libraryItemId: libraryItem.id, | ||||
|       libraryItemPath: libraryItem.path, | ||||
|       mediaCoverPath: media.coverPath, | ||||
|       serverAddress: serverAddress, | ||||
|       feedUrl, | ||||
|       feed | ||||
|     } | ||||
|     this.feeds[slug] = feedData | ||||
|     return feedData | ||||
|   } | ||||
| 
 | ||||
|   openFeedForItem(user, libraryItem, options) { | ||||
|   async openFeedForItem(user, libraryItem, options) { | ||||
|     const serverAddress = options.serverAddress | ||||
|     const slug = options.slug | ||||
| 
 | ||||
| @ -153,24 +90,29 @@ class RssFeedManager { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const feedData = this.openFeed(user.id, slug, libraryItem, serverAddress) | ||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed ${feedData.feedUrl}`) | ||||
|     this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl }) | ||||
|     return feedData | ||||
|     const feed = new Feed() | ||||
|     feed.setFromItem(user.id, slug, libraryItem, serverAddress) | ||||
|     this.feeds[feed.id] = feed | ||||
| 
 | ||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed ${feed.feedUrl}`) | ||||
|     await this.db.insertEntity('feed', feed) | ||||
|     this.emitter('rss_feed_open', { entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl }) | ||||
|     return feed | ||||
|   } | ||||
| 
 | ||||
|   closeFeedForItem(libraryItemId) { | ||||
|     var feed = this.findFeedForItem(libraryItemId) | ||||
|     if (!feed) return | ||||
|     this.closeRssFeed(feed.id) | ||||
|     return this.closeRssFeed(feed.id) | ||||
|   } | ||||
| 
 | ||||
|   closeRssFeed(id) { | ||||
|   async closeRssFeed(id) { | ||||
|     if (!this.feeds[id]) return | ||||
|     var feedData = this.feeds[id] | ||||
|     this.emitter('rss_feed_closed', { libraryItemId: feedData.libraryItemId, feedUrl: feedData.feedUrl }) | ||||
|     var feed = this.feeds[id] | ||||
|     await this.db.removeEntity('feed', id) | ||||
|     this.emitter('rss_feed_closed', { entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl }) | ||||
|     delete this.feeds[id] | ||||
|     Logger.info(`[RssFeedManager] Closed RSS feed "${feedData.feedUrl}"`) | ||||
|     Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`) | ||||
|   } | ||||
| } | ||||
| module.exports = RssFeedManager | ||||
							
								
								
									
										118
									
								
								server/objects/Feed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								server/objects/Feed.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | ||||
| const FeedMeta = require('./FeedMeta') | ||||
| const FeedEpisode = require('./FeedEpisode') | ||||
| const { Podcast } = require('podcast') | ||||
| 
 | ||||
| class Feed { | ||||
|   constructor(feed) { | ||||
|     this.id = null | ||||
|     this.slug = null | ||||
|     this.userId = null | ||||
|     this.entityType = null | ||||
|     this.entityId = null | ||||
| 
 | ||||
|     this.coverPath = null | ||||
|     this.serverAddress = null | ||||
|     this.feedUrl = null | ||||
| 
 | ||||
|     this.meta = null | ||||
|     this.episodes = null | ||||
| 
 | ||||
|     this.createdAt = null | ||||
|     this.updatedAt = null | ||||
| 
 | ||||
|     if (feed) { | ||||
|       this.construct(feed) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   construct(feed) { | ||||
|     this.id = feed.id | ||||
|     this.slug = feed.slug | ||||
|     this.userId = feed.userId | ||||
|     this.entityType = feed.entityType | ||||
|     this.entityId = feed.entityId | ||||
|     this.coverPath = feed.coverPath | ||||
|     this.serverAddress = feed.serverAddress | ||||
|     this.feedUrl = feed.feedUrl | ||||
|     this.meta = new FeedMeta(feed.meta) | ||||
|     this.episodes = feed.episodes.map(ep => new FeedEpisode(ep)) | ||||
|     this.createdAt = feed.createdAt | ||||
|     this.updatedAt = feed.updatedAt | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       slug: this.slug, | ||||
|       userId: this.userId, | ||||
|       entityType: this.entityType, | ||||
|       entityId: this.entityId, | ||||
|       coverPath: this.coverPath, | ||||
|       serverAddress: this.serverAddress, | ||||
|       feedUrl: this.feedUrl, | ||||
|       meta: this.meta.toJSON(), | ||||
|       episodes: this.episodes.map(ep => ep.toJSON()), | ||||
|       createdAt: this.createdAt, | ||||
|       updatedAt: this.updatedAt | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getEpisodePath(id) { | ||||
|     var episode = this.episodes.find(ep => ep.id === id) | ||||
|     if (!episode) return null | ||||
|     return episode.fullPath | ||||
|   } | ||||
| 
 | ||||
|   setFromItem(userId, slug, libraryItem, serverAddress) { | ||||
|     const media = libraryItem.media | ||||
|     const mediaMetadata = media.metadata | ||||
|     const isPodcast = libraryItem.mediaType === 'podcast' | ||||
| 
 | ||||
|     const feedUrl = `${serverAddress}/feed/${slug}` | ||||
|     const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName | ||||
| 
 | ||||
|     this.id = slug | ||||
|     this.slug = slug | ||||
|     this.userId = userId | ||||
|     this.entityType = 'item' | ||||
|     this.entityId = libraryItem.id | ||||
|     this.coverPath = media.coverPath || null | ||||
|     this.serverAddress = serverAddress | ||||
|     this.feedUrl = feedUrl | ||||
| 
 | ||||
|     this.meta = new FeedMeta() | ||||
|     this.meta.title = mediaMetadata.title | ||||
|     this.meta.description = mediaMetadata.description | ||||
|     this.meta.author = author | ||||
|     this.meta.imageUrl = media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png` | ||||
|     this.meta.feedUrl = feedUrl | ||||
|     this.meta.link = `${serverAddress}/items/${libraryItem.id}` | ||||
| 
 | ||||
|     this.episodes = [] | ||||
|     if (isPodcast) { // PODCAST EPISODES
 | ||||
|       media.episodes.forEach((episode) => { | ||||
|         var feedEpisode = new FeedEpisode() | ||||
|         feedEpisode.setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, this.meta) | ||||
|         this.episodes.push(feedEpisode) | ||||
|       }) | ||||
|     } else { // AUDIOBOOK EPISODES
 | ||||
|       media.tracks.forEach((audioTrack) => { | ||||
|         var feedEpisode = new FeedEpisode() | ||||
|         feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta) | ||||
|         this.episodes.push(feedEpisode) | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     this.createdAt = Date.now() | ||||
|     this.updatedAt = Date.now() | ||||
|   } | ||||
| 
 | ||||
|   buildXml() { | ||||
|     const pod = new Podcast(this.meta.getPodcastMeta()) | ||||
|     this.episodes.forEach((ep) => { | ||||
|       pod.addItem(ep.getPodcastEpisode()) | ||||
|     }) | ||||
|     return pod.buildXml() | ||||
|   } | ||||
| } | ||||
| module.exports = Feed | ||||
							
								
								
									
										130
									
								
								server/objects/FeedEpisode.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								server/objects/FeedEpisode.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,130 @@ | ||||
| const Path = require('path') | ||||
| const date = require('date-and-time') | ||||
| 
 | ||||
| class FeedEpisode { | ||||
|   constructor(episode) { | ||||
|     this.id = null | ||||
| 
 | ||||
|     this.title = null | ||||
|     this.description = null | ||||
|     this.enclosure = null | ||||
|     this.pubDate = null | ||||
|     this.link = null | ||||
|     this.author = null | ||||
|     this.explicit = null | ||||
|     this.duration = null | ||||
| 
 | ||||
|     this.libraryItemId = null | ||||
|     this.episodeId = null | ||||
|     this.trackIndex = null | ||||
|     this.fullPath = null | ||||
| 
 | ||||
|     if (episode) { | ||||
|       this.construct(episode) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   construct(episode) { | ||||
|     this.id = episode.id | ||||
|     this.title = episode.title | ||||
|     this.description = episode.description | ||||
|     this.enclosure = episode.enclosure ? { ...episode.enclosure } : null | ||||
|     this.pubDate = episode.pubDate | ||||
|     this.link = episode.link | ||||
|     this.author = episode.author | ||||
|     this.explicit = episode.explicit | ||||
|     this.duration = episode.duration | ||||
|     this.libraryItemId = episode.libraryItemId | ||||
|     this.episodeId = episode.episodeId || null | ||||
|     this.trackIndex = episode.trackIndex || 0 | ||||
|     this.fullPath = episode.fullPath | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       title: this.title, | ||||
|       description: this.description, | ||||
|       enclosure: this.enclosure ? { ...this.enclosure } : null, | ||||
|       pubDate: this.pubDate, | ||||
|       link: this.link, | ||||
|       author: this.author, | ||||
|       explicit: this.explicit, | ||||
|       duration: this.duration, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       episodeId: this.episodeId, | ||||
|       trackIndex: this.trackIndex, | ||||
|       fullPath: this.fullPath | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, meta) { | ||||
|     const contentUrl = `/feed/${slug}/item/${episode.id}/${episode.audioFile.metadata.filename}` | ||||
|     const media = libraryItem.media | ||||
|     const mediaMetadata = media.metadata | ||||
| 
 | ||||
|     this.id = episode.id | ||||
|     this.title = episode.title | ||||
|     this.description = episode.description || '' | ||||
|     this.enclosure = { | ||||
|       url: `${serverAddress}${contentUrl}`, | ||||
|       type: episode.audioTrack.mimeType, | ||||
|       size: episode.size | ||||
|     } | ||||
|     this.pubDate = episode.pubDate | ||||
|     this.link = meta.link | ||||
|     this.author = meta.author | ||||
|     this.explicit = mediaMetadata.explicit | ||||
|     this.duration = episode.duration | ||||
|     this.libraryItemId = libraryItem.id | ||||
|     this.episodeId = episode.id | ||||
|     this.trackIndex = 0 | ||||
|     this.fullPath = episode.audioFile.metadata.path | ||||
|   } | ||||
| 
 | ||||
|   setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) { | ||||
|     // 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]') | ||||
| 
 | ||||
|     const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}` | ||||
|     const media = libraryItem.media | ||||
|     const mediaMetadata = media.metadata | ||||
| 
 | ||||
|     var title = audioTrack.title | ||||
|     if (libraryItem.media.chapters.length) { | ||||
|       // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
 | ||||
|       var matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1) | ||||
|       if (matchingChapter && matchingChapter.title) title = matchingChapter.title | ||||
|     } | ||||
| 
 | ||||
|     this.id = String(audioTrack.index) | ||||
|     this.title = title | ||||
|     this.description = mediaMetadata.description || '' | ||||
|     this.enclosure = { | ||||
|       url: `${serverAddress}${contentUrl}`, | ||||
|       type: audioTrack.mimeType, | ||||
|       size: audioTrack.metadata.size | ||||
|     } | ||||
|     this.pubDate = audiobookPubDate | ||||
|     this.link = meta.link | ||||
|     this.author = meta.author | ||||
|     this.explicit = mediaMetadata.explicit | ||||
|     this.duration = audioTrack.duration | ||||
|     this.libraryItemId = libraryItem.id | ||||
|     this.episodeId = null | ||||
|     this.trackIndex = audioTrack.index | ||||
|     this.fullPath = audioTrack.metadata.path | ||||
|   } | ||||
| 
 | ||||
|   getPodcastEpisode() { | ||||
|     return { | ||||
|       title: this.title, | ||||
|       description: this.description || '', | ||||
|       enclosure: this.enclosure, | ||||
|       date: this.pubDate || '', | ||||
|       url: this.link, | ||||
|       author: this.author | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = FeedEpisode | ||||
							
								
								
									
										47
									
								
								server/objects/FeedMeta.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								server/objects/FeedMeta.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| class FeedMeta { | ||||
|   constructor(meta) { | ||||
|     this.title = null | ||||
|     this.description = null | ||||
|     this.author = null | ||||
|     this.imageUrl = null | ||||
|     this.feedUrl = null | ||||
|     this.link = null | ||||
| 
 | ||||
|     if (meta) { | ||||
|       this.construct(meta) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   construct(meta) { | ||||
|     this.title = meta.title | ||||
|     this.description = meta.description | ||||
|     this.author = meta.author | ||||
|     this.imageUrl = meta.imageUrl | ||||
|     this.feedUrl = meta.feedUrl | ||||
|     this.link = meta.link | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
|     return { | ||||
|       title: this.title, | ||||
|       description: this.description, | ||||
|       author: this.author, | ||||
|       imageUrl: this.imageUrl, | ||||
|       feedUrl: this.feedUrl, | ||||
|       link: this.link | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getPodcastMeta() { | ||||
|     return { | ||||
|       title: this.title, | ||||
|       description: this.description, | ||||
|       feedUrl: this.feedUrl, | ||||
|       siteUrl: this.link, | ||||
|       imageUrl: this.imageUrl, | ||||
|       author: this.author || 'advplyr', | ||||
|       language: 'en' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = FeedMeta | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user