mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 18:37:00 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			249 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const Path = require('path')
 | |
| 
 | |
| const Logger = require('../Logger')
 | |
| const SocketAuthority = require('../SocketAuthority')
 | |
| 
 | |
| const fs = require('../libs/fsExtra')
 | |
| const Feed = require('../objects/Feed')
 | |
| 
 | |
| class RssFeedManager {
 | |
|   constructor(db) {
 | |
|     this.db = db
 | |
| 
 | |
|     this.feeds = {}
 | |
|   }
 | |
| 
 | |
|   get feedsArray() {
 | |
|     return Object.values(this.feeds)
 | |
|   }
 | |
| 
 | |
|   validateFeedEntity(feedObj) {
 | |
|     if (feedObj.entityType === 'collection') {
 | |
|       if (!this.db.collections.some(li => li.id === feedObj.entityId)) {
 | |
|         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
 | |
|         return false
 | |
|       }
 | |
|     } else if (feedObj.entityType === 'libraryItem') {
 | |
|       if (!this.db.libraryItems.some(li => li.id === feedObj.entityId)) {
 | |
|         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
 | |
|         return false
 | |
|       }
 | |
|     } else if (feedObj.entityType === 'series') {
 | |
|       const series = this.db.series.find(s => s.id === feedObj.entityId)
 | |
|       const hasSeriesBook = this.db.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
 | |
|       if (!hasSeriesBook) {
 | |
|         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`)
 | |
|         return false
 | |
|       }
 | |
|     } else {
 | |
|       Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Invalid entityType "${feedObj.entityType}"`)
 | |
|       return false
 | |
|     }
 | |
|     return true
 | |
|   }
 | |
| 
 | |
|   async init() {
 | |
|     const feedObjects = await this.db.getAllEntities('feed')
 | |
|     if (!feedObjects || !feedObjects.length) return
 | |
| 
 | |
|     for (const feedObj of feedObjects) {
 | |
|       // Migration: In v2.2.12 entityType "item" was updated to "libraryItem"
 | |
|       if (feedObj.entityType === 'item') {
 | |
|         feedObj.entityType = 'libraryItem'
 | |
|         await this.db.updateEntity('feed', feedObj)
 | |
|       }
 | |
| 
 | |
|       // Remove invalid feeds
 | |
|       if (!this.validateFeedEntity(feedObj)) {
 | |
|         await this.db.removeEntity('feed', feedObj.id)
 | |
|       }
 | |
| 
 | |
|       const feed = new Feed(feedObj)
 | |
|       this.feeds[feed.id] = feed
 | |
|       Logger.info(`[RssFeedManager] Opened rss feed ${feed.feedUrl}`)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   findFeedForEntityId(entityId) {
 | |
|     return Object.values(this.feeds).find(feed => feed.entityId === entityId)
 | |
|   }
 | |
| 
 | |
|   findFeed(feedId) {
 | |
|     return this.feeds[feedId] || null
 | |
|   }
 | |
| 
 | |
|   async getFeed(req, res) {
 | |
|     const feed = this.feeds[req.params.id]
 | |
|     if (!feed) {
 | |
|       Logger.debug(`[RssFeedManager] Feed not found ${req.params.id}`)
 | |
|       res.sendStatus(404)
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     // Check if feed needs to be updated
 | |
|     if (feed.entityType === 'libraryItem') {
 | |
|       const libraryItem = this.db.getLibraryItem(feed.entityId)
 | |
| 
 | |
|       let mostRecentlyUpdatedAt = libraryItem.updatedAt
 | |
|       if (libraryItem.isPodcast) {
 | |
|         libraryItem.media.episodes.forEach((episode) => {
 | |
|           if (episode.updatedAt > mostRecentlyUpdatedAt) mostRecentlyUpdatedAt = episode.updatedAt
 | |
|         })
 | |
|       }
 | |
| 
 | |
|       if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) {
 | |
|         Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
 | |
|         feed.updateFromItem(libraryItem)
 | |
|         await this.db.updateEntity('feed', feed)
 | |
|       }
 | |
|     } else if (feed.entityType === 'collection') {
 | |
|       const collection = this.db.collections.find(c => c.id === feed.entityId)
 | |
|       if (collection) {
 | |
|         const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
 | |
| 
 | |
|         // Find most recently updated item in collection
 | |
|         let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
 | |
|         collectionExpanded.books.forEach((libraryItem) => {
 | |
|           if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) {
 | |
|             mostRecentlyUpdatedAt = libraryItem.updatedAt
 | |
|           }
 | |
|         })
 | |
| 
 | |
|         if (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt) {
 | |
|           Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`)
 | |
| 
 | |
|           feed.updateFromCollection(collectionExpanded)
 | |
|           await this.db.updateEntity('feed', feed)
 | |
|         }
 | |
|       }
 | |
|     } else if (feed.entityType === 'series') {
 | |
|       const series = this.db.series.find(s => s.id === feed.entityId)
 | |
|       if (series) {
 | |
|         const seriesJson = series.toJSON()
 | |
|         // Get books in series that have audio tracks
 | |
|         seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
 | |
| 
 | |
|         // Find most recently updated item in series
 | |
|         let mostRecentlyUpdatedAt = seriesJson.updatedAt
 | |
|         let totalTracks = 0 // Used to detect series items removed
 | |
|         seriesJson.books.forEach((libraryItem) => {
 | |
|           totalTracks += libraryItem.media.tracks.length
 | |
|           if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) {
 | |
|             mostRecentlyUpdatedAt = libraryItem.updatedAt
 | |
|           }
 | |
|         })
 | |
|         if (totalTracks !== feed.episodes.length) {
 | |
|           mostRecentlyUpdatedAt = Date.now()
 | |
|         }
 | |
| 
 | |
|         if (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt) {
 | |
|           Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`)
 | |
| 
 | |
|           feed.updateFromSeries(seriesJson)
 | |
|           await this.db.updateEntity('feed', feed)
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const xml = feed.buildXml()
 | |
|     res.set('Content-Type', 'text/xml')
 | |
|     res.send(xml)
 | |
|   }
 | |
| 
 | |
|   getFeedItem(req, res) {
 | |
|     const feed = this.feeds[req.params.id]
 | |
|     if (!feed) {
 | |
|       Logger.debug(`[RssFeedManager] Feed not found ${req.params.id}`)
 | |
|       res.sendStatus(404)
 | |
|       return
 | |
|     }
 | |
|     const 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)
 | |
|   }
 | |
| 
 | |
|   getFeedCover(req, res) {
 | |
|     const feed = this.feeds[req.params.id]
 | |
|     if (!feed) {
 | |
|       Logger.debug(`[RssFeedManager] Feed not found ${req.params.id}`)
 | |
|       res.sendStatus(404)
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     if (!feed.coverPath) {
 | |
|       res.sendStatus(404)
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
 | |
|     res.type(`image/${extname}`)
 | |
|     const readStream = fs.createReadStream(feed.coverPath)
 | |
|     readStream.pipe(res)
 | |
|   }
 | |
| 
 | |
|   async openFeedForItem(user, libraryItem, options) {
 | |
|     const serverAddress = options.serverAddress
 | |
|     const slug = options.slug
 | |
| 
 | |
|     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)
 | |
|     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
 | |
|     return feed
 | |
|   }
 | |
| 
 | |
|   async openFeedForCollection(user, collectionExpanded, options) {
 | |
|     const serverAddress = options.serverAddress
 | |
|     const slug = options.slug
 | |
| 
 | |
|     const feed = new Feed()
 | |
|     feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress)
 | |
|     this.feeds[feed.id] = feed
 | |
| 
 | |
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
 | |
|     await this.db.insertEntity('feed', feed)
 | |
|     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
 | |
|     return feed
 | |
|   }
 | |
| 
 | |
|   async openFeedForSeries(user, seriesExpanded, options) {
 | |
|     const serverAddress = options.serverAddress
 | |
|     const slug = options.slug
 | |
| 
 | |
|     const feed = new Feed()
 | |
|     feed.setFromSeries(user.id, slug, seriesExpanded, serverAddress)
 | |
|     this.feeds[feed.id] = feed
 | |
| 
 | |
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
 | |
|     await this.db.insertEntity('feed', feed)
 | |
|     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
 | |
|     return feed
 | |
|   }
 | |
| 
 | |
|   async handleCloseFeed(feed) {
 | |
|     if (!feed) return
 | |
|     await this.db.removeEntity('feed', feed.id)
 | |
|     SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified())
 | |
|     delete this.feeds[feed.id]
 | |
|     Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
 | |
|   }
 | |
| 
 | |
|   closeRssFeed(id) {
 | |
|     if (!this.feeds[id]) return
 | |
|     return this.handleCloseFeed(this.feeds[id])
 | |
|   }
 | |
| 
 | |
|   closeFeedForEntityId(entityId) {
 | |
|     const feed = this.findFeedForEntityId(entityId)
 | |
|     if (!feed) return
 | |
|     return this.handleCloseFeed(feed)
 | |
|   }
 | |
| }
 | |
| module.exports = RssFeedManager |