mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			655 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			655 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const Path = require('path')
 | |
| const { DataTypes, Model } = require('sequelize')
 | |
| const Logger = require('../Logger')
 | |
| 
 | |
| const RSS = require('../libs/rss')
 | |
| 
 | |
| /**
 | |
|  * @typedef FeedOptions
 | |
|  * @property {boolean} preventIndexing
 | |
|  * @property {string} ownerName
 | |
|  * @property {string} ownerEmail
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @typedef FeedExpandedProperties
 | |
|  * @property {import('./FeedEpisode')} feedEpisodes
 | |
|  *
 | |
|  * @typedef {Feed & FeedExpandedProperties} FeedExpanded
 | |
|  */
 | |
| 
 | |
| class Feed extends Model {
 | |
|   constructor(values, options) {
 | |
|     super(values, options)
 | |
| 
 | |
|     /** @type {UUIDV4} */
 | |
|     this.id
 | |
|     /** @type {string} */
 | |
|     this.slug
 | |
|     /** @type {string} */
 | |
|     this.entityType
 | |
|     /** @type {UUIDV4} */
 | |
|     this.entityId
 | |
|     /** @type {Date} */
 | |
|     this.entityUpdatedAt
 | |
|     /** @type {string} */
 | |
|     this.serverAddress
 | |
|     /** @type {string} */
 | |
|     this.feedURL
 | |
|     /** @type {string} */
 | |
|     this.imageURL
 | |
|     /** @type {string} */
 | |
|     this.siteURL
 | |
|     /** @type {string} */
 | |
|     this.title
 | |
|     /** @type {string} */
 | |
|     this.description
 | |
|     /** @type {string} */
 | |
|     this.author
 | |
|     /** @type {string} */
 | |
|     this.podcastType
 | |
|     /** @type {string} */
 | |
|     this.language
 | |
|     /** @type {string} */
 | |
|     this.ownerName
 | |
|     /** @type {string} */
 | |
|     this.ownerEmail
 | |
|     /** @type {boolean} */
 | |
|     this.explicit
 | |
|     /** @type {boolean} */
 | |
|     this.preventIndexing
 | |
|     /** @type {string} */
 | |
|     this.coverPath
 | |
|     /** @type {UUIDV4} */
 | |
|     this.userId
 | |
|     /** @type {Date} */
 | |
|     this.createdAt
 | |
|     /** @type {Date} */
 | |
|     this.updatedAt
 | |
| 
 | |
|     // Expanded properties
 | |
| 
 | |
|     /** @type {import('./FeedEpisode')[]} - only set if expanded */
 | |
|     this.feedEpisodes
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {string} feedId
 | |
|    * @returns {Promise<boolean>} - true if feed was removed
 | |
|    */
 | |
|   static async removeById(feedId) {
 | |
|     return (
 | |
|       (await this.destroy({
 | |
|         where: {
 | |
|           id: feedId
 | |
|         }
 | |
|       })) > 0
 | |
|     )
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {import('./LibraryItem').LibraryItemExpanded} libraryItem
 | |
|    * @param {string} slug
 | |
|    * @param {string} serverAddress
 | |
|    * @param {FeedOptions} [feedOptions=null]
 | |
|    *
 | |
|    * @returns {Feed}
 | |
|    */
 | |
|   static getFeedObjForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions = null) {
 | |
|     const media = libraryItem.media
 | |
| 
 | |
|     let entityUpdatedAt = libraryItem.updatedAt
 | |
| 
 | |
|     // Podcast feeds should use the most recent episode updatedAt if more recent
 | |
|     if (libraryItem.mediaType === 'podcast') {
 | |
|       entityUpdatedAt = libraryItem.media.podcastEpisodes.reduce((mostRecent, episode) => {
 | |
|         return episode.updatedAt > mostRecent ? episode.updatedAt : mostRecent
 | |
|       }, entityUpdatedAt)
 | |
|     }
 | |
| 
 | |
|     const feedObj = {
 | |
|       slug,
 | |
|       entityType: 'libraryItem',
 | |
|       entityId: libraryItem.id,
 | |
|       entityUpdatedAt,
 | |
|       serverAddress,
 | |
|       feedURL: `/feed/${slug}`,
 | |
|       imageURL: media.coverPath ? `/feed/${slug}/cover${Path.extname(media.coverPath)}` : `/Logo.png`,
 | |
|       siteURL: `/item/${libraryItem.id}`,
 | |
|       title: media.title,
 | |
|       description: media.description,
 | |
|       author: libraryItem.mediaType === 'podcast' ? media.author : media.authorName,
 | |
|       podcastType: libraryItem.mediaType === 'podcast' ? media.podcastType : 'serial',
 | |
|       language: media.language,
 | |
|       explicit: media.explicit,
 | |
|       coverPath: media.coverPath,
 | |
|       userId
 | |
|     }
 | |
| 
 | |
|     if (feedOptions) {
 | |
|       feedObj.preventIndexing = feedOptions.preventIndexing
 | |
|       feedObj.ownerName = feedOptions.ownerName
 | |
|       feedObj.ownerEmail = feedOptions.ownerEmail
 | |
|     }
 | |
| 
 | |
|     return feedObj
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {import('./LibraryItem').LibraryItemExpanded} libraryItem
 | |
|    * @param {string} slug
 | |
|    * @param {string} serverAddress
 | |
|    * @param {FeedOptions} feedOptions
 | |
|    *
 | |
|    * @returns {Promise<FeedExpanded>}
 | |
|    */
 | |
|   static async createFeedForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions) {
 | |
|     const feedObj = this.getFeedObjForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions)
 | |
| 
 | |
|     /** @type {typeof import('./FeedEpisode')} */
 | |
|     const feedEpisodeModel = this.sequelize.models.feedEpisode
 | |
| 
 | |
|     const transaction = await this.sequelize.transaction()
 | |
|     try {
 | |
|       const feed = await this.create(feedObj, { transaction })
 | |
| 
 | |
|       if (libraryItem.mediaType === 'podcast') {
 | |
|         feed.feedEpisodes = await feedEpisodeModel.createFromPodcastEpisodes(libraryItem, feed, slug, transaction)
 | |
|       } else {
 | |
|         feed.feedEpisodes = await feedEpisodeModel.createFromAudiobookTracks(libraryItem, feed, slug, transaction)
 | |
|       }
 | |
| 
 | |
|       await transaction.commit()
 | |
| 
 | |
|       return feed
 | |
|     } catch (error) {
 | |
|       Logger.error(`[Feed] Error creating feed for library item ${libraryItem.id}`, error)
 | |
|       await transaction.rollback()
 | |
|       return null
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {import('./Collection')} collectionExpanded
 | |
|    * @param {string} slug
 | |
|    * @param {string} serverAddress
 | |
|    * @param {FeedOptions} [feedOptions=null]
 | |
|    *
 | |
|    * @returns {{ feedObj: Feed, booksWithTracks: import('./Book').BookExpandedWithLibraryItem[] }}
 | |
|    */
 | |
|   static getFeedObjForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions = null) {
 | |
|     const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length)
 | |
| 
 | |
|     const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => {
 | |
|       return book.libraryItem.updatedAt > mostRecent ? book.libraryItem.updatedAt : mostRecent
 | |
|     }, collectionExpanded.updatedAt)
 | |
| 
 | |
|     const firstBookWithCover = booksWithTracks.find((book) => book.coverPath)
 | |
| 
 | |
|     const allBookAuthorNames = booksWithTracks.reduce((authorNames, book) => {
 | |
|       const bookAuthorsToAdd = book.authors.filter((author) => !authorNames.includes(author.name)).map((author) => author.name)
 | |
|       return authorNames.concat(bookAuthorsToAdd)
 | |
|     }, [])
 | |
|     let author = allBookAuthorNames.slice(0, 3).join(', ')
 | |
|     if (allBookAuthorNames.length > 3) {
 | |
|       author += ' & more'
 | |
|     }
 | |
| 
 | |
|     const feedObj = {
 | |
|       slug,
 | |
|       entityType: 'collection',
 | |
|       entityId: collectionExpanded.id,
 | |
|       entityUpdatedAt,
 | |
|       serverAddress,
 | |
|       feedURL: `/feed/${slug}`,
 | |
|       imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`,
 | |
|       siteURL: `/collection/${collectionExpanded.id}`,
 | |
|       title: collectionExpanded.name,
 | |
|       description: collectionExpanded.description || '',
 | |
|       author,
 | |
|       podcastType: 'serial',
 | |
|       explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit
 | |
|       coverPath: firstBookWithCover?.coverPath || null,
 | |
|       userId
 | |
|     }
 | |
| 
 | |
|     if (feedOptions) {
 | |
|       feedObj.preventIndexing = feedOptions.preventIndexing
 | |
|       feedObj.ownerName = feedOptions.ownerName
 | |
|       feedObj.ownerEmail = feedOptions.ownerEmail
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       feedObj,
 | |
|       booksWithTracks
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {import('./Collection')} collectionExpanded
 | |
|    * @param {string} slug
 | |
|    * @param {string} serverAddress
 | |
|    * @param {FeedOptions} feedOptions
 | |
|    *
 | |
|    * @returns {Promise<FeedExpanded>}
 | |
|    */
 | |
|   static async createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) {
 | |
|     const { feedObj, booksWithTracks } = this.getFeedObjForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions)
 | |
| 
 | |
|     /** @type {typeof import('./FeedEpisode')} */
 | |
|     const feedEpisodeModel = this.sequelize.models.feedEpisode
 | |
| 
 | |
|     const transaction = await this.sequelize.transaction()
 | |
|     try {
 | |
|       const feed = await this.create(feedObj, { transaction })
 | |
|       feed.feedEpisodes = await feedEpisodeModel.createFromBooks(booksWithTracks, feed, slug, transaction)
 | |
| 
 | |
|       await transaction.commit()
 | |
| 
 | |
|       return feed
 | |
|     } catch (error) {
 | |
|       Logger.error(`[Feed] Error creating feed for collection ${collectionExpanded.id}`, error)
 | |
|       await transaction.rollback()
 | |
|       return null
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {import('./Series')} seriesExpanded
 | |
|    * @param {string} slug
 | |
|    * @param {string} serverAddress
 | |
|    * @param {FeedOptions} [feedOptions=null]
 | |
|    *
 | |
|    * @returns {{ feedObj: Feed, booksWithTracks: import('./Book').BookExpandedWithLibraryItem[] }}
 | |
|    */
 | |
|   static getFeedObjForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions = null) {
 | |
|     const booksWithTracks = seriesExpanded.books.filter((book) => book.includedAudioFiles.length)
 | |
|     const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => {
 | |
|       return book.libraryItem.updatedAt > mostRecent ? book.libraryItem.updatedAt : mostRecent
 | |
|     }, seriesExpanded.updatedAt)
 | |
| 
 | |
|     const firstBookWithCover = booksWithTracks.find((book) => book.coverPath)
 | |
| 
 | |
|     const allBookAuthorNames = booksWithTracks.reduce((authorNames, book) => {
 | |
|       const bookAuthorsToAdd = book.authors.filter((author) => !authorNames.includes(author.name)).map((author) => author.name)
 | |
|       return authorNames.concat(bookAuthorsToAdd)
 | |
|     }, [])
 | |
|     let author = allBookAuthorNames.slice(0, 3).join(', ')
 | |
|     if (allBookAuthorNames.length > 3) {
 | |
|       author += ' & more'
 | |
|     }
 | |
| 
 | |
|     const feedObj = {
 | |
|       slug,
 | |
|       entityType: 'series',
 | |
|       entityId: seriesExpanded.id,
 | |
|       entityUpdatedAt,
 | |
|       serverAddress,
 | |
|       feedURL: `/feed/${slug}`,
 | |
|       imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`,
 | |
|       siteURL: `/library/${booksWithTracks[0].libraryItem.libraryId}/series/${seriesExpanded.id}`,
 | |
|       title: seriesExpanded.name,
 | |
|       description: seriesExpanded.description || '',
 | |
|       author,
 | |
|       podcastType: 'serial',
 | |
|       explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit
 | |
|       coverPath: firstBookWithCover?.coverPath || null,
 | |
|       userId
 | |
|     }
 | |
| 
 | |
|     if (feedOptions) {
 | |
|       feedObj.preventIndexing = feedOptions.preventIndexing
 | |
|       feedObj.ownerName = feedOptions.ownerName
 | |
|       feedObj.ownerEmail = feedOptions.ownerEmail
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       feedObj,
 | |
|       booksWithTracks
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {import('./Series')} seriesExpanded
 | |
|    * @param {string} slug
 | |
|    * @param {string} serverAddress
 | |
|    * @param {FeedOptions} feedOptions
 | |
|    *
 | |
|    * @returns {Promise<FeedExpanded>}
 | |
|    */
 | |
|   static async createFeedForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) {
 | |
|     const { feedObj, booksWithTracks } = this.getFeedObjForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions)
 | |
| 
 | |
|     /** @type {typeof import('./FeedEpisode')} */
 | |
|     const feedEpisodeModel = this.sequelize.models.feedEpisode
 | |
| 
 | |
|     const transaction = await this.sequelize.transaction()
 | |
|     try {
 | |
|       const feed = await this.create(feedObj, { transaction })
 | |
|       feed.feedEpisodes = await feedEpisodeModel.createFromBooks(booksWithTracks, feed, slug, transaction)
 | |
| 
 | |
|       await transaction.commit()
 | |
| 
 | |
|       return feed
 | |
|     } catch (error) {
 | |
|       Logger.error(`[Feed] Error creating feed for series ${seriesExpanded.id}`, error)
 | |
|       await transaction.rollback()
 | |
|       return null
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Initialize model
 | |
|    *
 | |
|    * Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series
 | |
|    * @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
 | |
|    *
 | |
|    * @param {import('../Database').sequelize} sequelize
 | |
|    */
 | |
|   static init(sequelize) {
 | |
|     super.init(
 | |
|       {
 | |
|         id: {
 | |
|           type: DataTypes.UUID,
 | |
|           defaultValue: DataTypes.UUIDV4,
 | |
|           primaryKey: true
 | |
|         },
 | |
|         slug: DataTypes.STRING,
 | |
|         entityType: DataTypes.STRING,
 | |
|         entityId: DataTypes.UUID,
 | |
|         entityUpdatedAt: DataTypes.DATE,
 | |
|         serverAddress: DataTypes.STRING,
 | |
|         feedURL: DataTypes.STRING,
 | |
|         imageURL: DataTypes.STRING,
 | |
|         siteURL: DataTypes.STRING,
 | |
|         title: DataTypes.STRING,
 | |
|         description: DataTypes.TEXT,
 | |
|         author: DataTypes.STRING,
 | |
|         podcastType: DataTypes.STRING,
 | |
|         language: DataTypes.STRING,
 | |
|         ownerName: DataTypes.STRING,
 | |
|         ownerEmail: DataTypes.STRING,
 | |
|         explicit: DataTypes.BOOLEAN,
 | |
|         preventIndexing: DataTypes.BOOLEAN,
 | |
|         coverPath: DataTypes.STRING
 | |
|       },
 | |
|       {
 | |
|         sequelize,
 | |
|         modelName: 'feed'
 | |
|       }
 | |
|     )
 | |
| 
 | |
|     const { user, libraryItem, collection, series, playlist } = sequelize.models
 | |
| 
 | |
|     user.hasMany(Feed)
 | |
|     Feed.belongsTo(user)
 | |
| 
 | |
|     libraryItem.hasMany(Feed, {
 | |
|       foreignKey: 'entityId',
 | |
|       constraints: false,
 | |
|       scope: {
 | |
|         entityType: 'libraryItem'
 | |
|       }
 | |
|     })
 | |
|     Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false })
 | |
| 
 | |
|     collection.hasMany(Feed, {
 | |
|       foreignKey: 'entityId',
 | |
|       constraints: false,
 | |
|       scope: {
 | |
|         entityType: 'collection'
 | |
|       }
 | |
|     })
 | |
|     Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false })
 | |
| 
 | |
|     series.hasMany(Feed, {
 | |
|       foreignKey: 'entityId',
 | |
|       constraints: false,
 | |
|       scope: {
 | |
|         entityType: 'series'
 | |
|       }
 | |
|     })
 | |
|     Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false })
 | |
| 
 | |
|     playlist.hasMany(Feed, {
 | |
|       foreignKey: 'entityId',
 | |
|       constraints: false,
 | |
|       scope: {
 | |
|         entityType: 'playlist'
 | |
|       }
 | |
|     })
 | |
|     Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })
 | |
| 
 | |
|     Feed.addHook('afterFind', (findResult) => {
 | |
|       if (!findResult) return
 | |
| 
 | |
|       if (!Array.isArray(findResult)) findResult = [findResult]
 | |
|       for (const instance of findResult) {
 | |
|         if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) {
 | |
|           instance.entity = instance.libraryItem
 | |
|           instance.dataValues.entity = instance.dataValues.libraryItem
 | |
|         } else if (instance.entityType === 'collection' && instance.collection !== undefined) {
 | |
|           instance.entity = instance.collection
 | |
|           instance.dataValues.entity = instance.dataValues.collection
 | |
|         } else if (instance.entityType === 'series' && instance.series !== undefined) {
 | |
|           instance.entity = instance.series
 | |
|           instance.dataValues.entity = instance.dataValues.series
 | |
|         } else if (instance.entityType === 'playlist' && instance.playlist !== undefined) {
 | |
|           instance.entity = instance.playlist
 | |
|           instance.dataValues.entity = instance.dataValues.playlist
 | |
|         }
 | |
| 
 | |
|         // To prevent mistakes:
 | |
|         delete instance.libraryItem
 | |
|         delete instance.dataValues.libraryItem
 | |
|         delete instance.collection
 | |
|         delete instance.dataValues.collection
 | |
|         delete instance.series
 | |
|         delete instance.dataValues.series
 | |
|         delete instance.playlist
 | |
|         delete instance.dataValues.playlist
 | |
|       }
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @returns {Promise<FeedExpanded>}
 | |
|    */
 | |
|   async updateFeedForEntity() {
 | |
|     /** @type {typeof import('./FeedEpisode')} */
 | |
|     const feedEpisodeModel = this.sequelize.models.feedEpisode
 | |
| 
 | |
|     let feedObj = null
 | |
|     let feedEpisodeCreateFunc = null
 | |
|     let feedEpisodeCreateFuncEntity = null
 | |
| 
 | |
|     if (this.entityType === 'libraryItem') {
 | |
|       /** @type {typeof import('./LibraryItem')} */
 | |
|       const libraryItemModel = this.sequelize.models.libraryItem
 | |
| 
 | |
|       const itemExpanded = await libraryItemModel.getExpandedById(this.entityId)
 | |
|       feedObj = Feed.getFeedObjForLibraryItem(this.userId, itemExpanded, this.slug, this.serverAddress)
 | |
| 
 | |
|       feedEpisodeCreateFuncEntity = itemExpanded
 | |
|       if (itemExpanded.mediaType === 'podcast') {
 | |
|         feedEpisodeCreateFunc = feedEpisodeModel.createFromPodcastEpisodes.bind(feedEpisodeModel)
 | |
|       } else {
 | |
|         feedEpisodeCreateFunc = feedEpisodeModel.createFromAudiobookTracks.bind(feedEpisodeModel)
 | |
|       }
 | |
|     } else if (this.entityType === 'collection') {
 | |
|       /** @type {typeof import('./Collection')} */
 | |
|       const collectionModel = this.sequelize.models.collection
 | |
| 
 | |
|       const collectionExpanded = await collectionModel.getExpandedById(this.entityId)
 | |
|       const feedObjData = Feed.getFeedObjForCollection(this.userId, collectionExpanded, this.slug, this.serverAddress)
 | |
|       feedObj = feedObjData.feedObj
 | |
|       feedEpisodeCreateFuncEntity = feedObjData.booksWithTracks
 | |
|       feedEpisodeCreateFunc = feedEpisodeModel.createFromBooks.bind(feedEpisodeModel)
 | |
|     } else if (this.entityType === 'series') {
 | |
|       /** @type {typeof import('./Series')} */
 | |
|       const seriesModel = this.sequelize.models.series
 | |
| 
 | |
|       const seriesExpanded = await seriesModel.getExpandedById(this.entityId)
 | |
|       const feedObjData = Feed.getFeedObjForSeries(this.userId, seriesExpanded, this.slug, this.serverAddress)
 | |
|       feedObj = feedObjData.feedObj
 | |
|       feedEpisodeCreateFuncEntity = feedObjData.booksWithTracks
 | |
|       feedEpisodeCreateFunc = feedEpisodeModel.createFromBooks.bind(feedEpisodeModel)
 | |
|     } else {
 | |
|       Logger.error(`[Feed] Invalid entity type ${this.entityType} for feed ${this.id}`)
 | |
|       return null
 | |
|     }
 | |
| 
 | |
|     const transaction = await this.sequelize.transaction()
 | |
|     try {
 | |
|       const updatedFeed = await this.update(feedObj, { transaction })
 | |
| 
 | |
|       // Remove existing feed episodes
 | |
|       await feedEpisodeModel.destroy({
 | |
|         where: {
 | |
|           feedId: this.id
 | |
|         },
 | |
|         transaction
 | |
|       })
 | |
| 
 | |
|       // Create new feed episodes
 | |
|       updatedFeed.feedEpisodes = await feedEpisodeCreateFunc(feedEpisodeCreateFuncEntity, updatedFeed, this.slug, transaction)
 | |
| 
 | |
|       await transaction.commit()
 | |
| 
 | |
|       return updatedFeed
 | |
|     } catch (error) {
 | |
|       Logger.error(`[Feed] Error updating feed ${this.entityId}`, error)
 | |
|       await transaction.rollback()
 | |
| 
 | |
|       return null
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   getEntity(options) {
 | |
|     if (!this.entityType) return Promise.resolve(null)
 | |
|     const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}`
 | |
|     return this[mixinMethodName](options)
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} hostPrefix
 | |
|    */
 | |
|   buildXml(hostPrefix) {
 | |
|     const blockTags = [{ 'itunes:block': 'yes' }, { 'googleplay:block': 'yes' }]
 | |
|     const rssData = {
 | |
|       title: this.title,
 | |
|       description: this.description || '',
 | |
|       generator: 'Audiobookshelf',
 | |
|       feed_url: `${hostPrefix}${this.feedURL}`,
 | |
|       site_url: `${hostPrefix}${this.siteURL}`,
 | |
|       image_url: `${hostPrefix}${this.imageURL}`,
 | |
|       custom_namespaces: {
 | |
|         itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd',
 | |
|         psc: 'http://podlove.org/simple-chapters',
 | |
|         podcast: 'https://podcastindex.org/namespace/1.0',
 | |
|         googleplay: 'http://www.google.com/schemas/play-podcasts/1.0'
 | |
|       },
 | |
|       custom_elements: [
 | |
|         { language: this.language || 'en' },
 | |
|         { author: this.author || 'advplyr' },
 | |
|         { 'itunes:author': this.author || 'advplyr' },
 | |
|         { 'itunes:summary': this.description || '' },
 | |
|         { 'itunes:type': this.podcastType },
 | |
|         {
 | |
|           'itunes:image': {
 | |
|             _attr: {
 | |
|               href: `${hostPrefix}${this.imageURL}`
 | |
|             }
 | |
|           }
 | |
|         },
 | |
|         {
 | |
|           'itunes:owner': [{ 'itunes:name': this.ownerName || this.author || '' }, { 'itunes:email': this.ownerEmail || '' }]
 | |
|         },
 | |
|         { 'itunes:explicit': !!this.explicit },
 | |
|         ...(this.preventIndexing ? blockTags : [])
 | |
|       ]
 | |
|     }
 | |
| 
 | |
|     const rssfeed = new RSS(rssData)
 | |
|     this.feedEpisodes.forEach((ep) => {
 | |
|       rssfeed.item(ep.getRSSData(hostPrefix))
 | |
|     })
 | |
|     return rssfeed.xml()
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} id
 | |
|    * @returns {string}
 | |
|    */
 | |
|   getEpisodePath(id) {
 | |
|     const episode = this.feedEpisodes.find((ep) => ep.id === id)
 | |
|     if (!episode) return null
 | |
|     return episode.filePath
 | |
|   }
 | |
| 
 | |
|   toOldJSON() {
 | |
|     const episodes = this.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode())
 | |
|     return {
 | |
|       id: this.id,
 | |
|       slug: this.slug,
 | |
|       userId: this.userId,
 | |
|       entityType: this.entityType,
 | |
|       entityId: this.entityId,
 | |
|       entityUpdatedAt: this.entityUpdatedAt?.valueOf() || null,
 | |
|       coverPath: this.coverPath || null,
 | |
|       meta: {
 | |
|         title: this.title,
 | |
|         description: this.description,
 | |
|         author: this.author,
 | |
|         imageUrl: this.imageURL,
 | |
|         feedUrl: this.feedURL,
 | |
|         link: this.siteURL,
 | |
|         explicit: this.explicit,
 | |
|         type: this.podcastType,
 | |
|         language: this.language,
 | |
|         preventIndexing: this.preventIndexing,
 | |
|         ownerName: this.ownerName,
 | |
|         ownerEmail: this.ownerEmail
 | |
|       },
 | |
|       serverAddress: this.serverAddress,
 | |
|       feedUrl: this.feedURL,
 | |
|       episodes: episodes || [],
 | |
|       createdAt: this.createdAt.valueOf(),
 | |
|       updatedAt: this.updatedAt.valueOf()
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   toOldJSONMinified() {
 | |
|     return {
 | |
|       id: this.id,
 | |
|       entityType: this.entityType,
 | |
|       entityId: this.entityId,
 | |
|       feedUrl: this.feedURL,
 | |
|       meta: {
 | |
|         title: this.title,
 | |
|         description: this.description,
 | |
|         preventIndexing: this.preventIndexing,
 | |
|         ownerName: this.ownerName,
 | |
|         ownerEmail: this.ownerEmail
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = Feed
 |