mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			375 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			375 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const { DataTypes, Model } = require('sequelize')
 | 
						|
const oldFeed = require('../objects/Feed')
 | 
						|
const areEquivalent = require('../utils/areEquivalent')
 | 
						|
 | 
						|
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
 | 
						|
  }
 | 
						|
 | 
						|
  static async getOldFeeds() {
 | 
						|
    const feeds = await this.findAll({
 | 
						|
      include: {
 | 
						|
        model: this.sequelize.models.feedEpisode
 | 
						|
      }
 | 
						|
    })
 | 
						|
    return feeds.map((f) => this.getOldFeed(f))
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get old feed from Feed and optionally Feed with FeedEpisodes
 | 
						|
   * @param {Feed} feedExpanded
 | 
						|
   * @returns {oldFeed}
 | 
						|
   */
 | 
						|
  static getOldFeed(feedExpanded) {
 | 
						|
    const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode())
 | 
						|
    return new oldFeed({
 | 
						|
      id: feedExpanded.id,
 | 
						|
      slug: feedExpanded.slug,
 | 
						|
      userId: feedExpanded.userId,
 | 
						|
      entityType: feedExpanded.entityType,
 | 
						|
      entityId: feedExpanded.entityId,
 | 
						|
      entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null,
 | 
						|
      coverPath: feedExpanded.coverPath || null,
 | 
						|
      meta: {
 | 
						|
        title: feedExpanded.title,
 | 
						|
        description: feedExpanded.description,
 | 
						|
        author: feedExpanded.author,
 | 
						|
        imageUrl: feedExpanded.imageURL,
 | 
						|
        feedUrl: feedExpanded.feedURL,
 | 
						|
        link: feedExpanded.siteURL,
 | 
						|
        explicit: feedExpanded.explicit,
 | 
						|
        type: feedExpanded.podcastType,
 | 
						|
        language: feedExpanded.language,
 | 
						|
        preventIndexing: feedExpanded.preventIndexing,
 | 
						|
        ownerName: feedExpanded.ownerName,
 | 
						|
        ownerEmail: feedExpanded.ownerEmail
 | 
						|
      },
 | 
						|
      serverAddress: feedExpanded.serverAddress,
 | 
						|
      feedUrl: feedExpanded.feedURL,
 | 
						|
      episodes: episodes || [],
 | 
						|
      createdAt: feedExpanded.createdAt.valueOf(),
 | 
						|
      updatedAt: feedExpanded.updatedAt.valueOf()
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  static removeById(feedId) {
 | 
						|
    return this.destroy({
 | 
						|
      where: {
 | 
						|
        id: feedId
 | 
						|
      }
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Find all library item ids that have an open feed (used in library filter)
 | 
						|
   * @returns {Promise<string[]>} array of library item ids
 | 
						|
   */
 | 
						|
  static async findAllLibraryItemIds() {
 | 
						|
    const feeds = await this.findAll({
 | 
						|
      attributes: ['entityId'],
 | 
						|
      where: {
 | 
						|
        entityType: 'libraryItem'
 | 
						|
      }
 | 
						|
    })
 | 
						|
    return feeds.map((f) => f.entityId).filter((f) => f) || []
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Find feed where and return oldFeed
 | 
						|
   * @param {Object} where sequelize where object
 | 
						|
   * @returns {Promise<oldFeed>} oldFeed
 | 
						|
   */
 | 
						|
  static async findOneOld(where) {
 | 
						|
    if (!where) return null
 | 
						|
    const feedExpanded = await this.findOne({
 | 
						|
      where,
 | 
						|
      include: {
 | 
						|
        model: this.sequelize.models.feedEpisode
 | 
						|
      }
 | 
						|
    })
 | 
						|
    if (!feedExpanded) return null
 | 
						|
    return this.getOldFeed(feedExpanded)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Find feed and return oldFeed
 | 
						|
   * @param {string} id
 | 
						|
   * @returns {Promise<oldFeed>} oldFeed
 | 
						|
   */
 | 
						|
  static async findByPkOld(id) {
 | 
						|
    if (!id) return null
 | 
						|
    const feedExpanded = await this.findByPk(id, {
 | 
						|
      include: {
 | 
						|
        model: this.sequelize.models.feedEpisode
 | 
						|
      }
 | 
						|
    })
 | 
						|
    if (!feedExpanded) return null
 | 
						|
    return this.getOldFeed(feedExpanded)
 | 
						|
  }
 | 
						|
 | 
						|
  static async fullCreateFromOld(oldFeed) {
 | 
						|
    const feedObj = this.getFromOld(oldFeed)
 | 
						|
    const newFeed = await this.create(feedObj)
 | 
						|
 | 
						|
    if (oldFeed.episodes?.length) {
 | 
						|
      for (const oldFeedEpisode of oldFeed.episodes) {
 | 
						|
        const feedEpisode = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
 | 
						|
        feedEpisode.feedId = newFeed.id
 | 
						|
        await this.sequelize.models.feedEpisode.create(feedEpisode)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  static async fullUpdateFromOld(oldFeed) {
 | 
						|
    const oldFeedEpisodes = oldFeed.episodes || []
 | 
						|
    const feedObj = this.getFromOld(oldFeed)
 | 
						|
 | 
						|
    const existingFeed = await this.findByPk(feedObj.id, {
 | 
						|
      include: this.sequelize.models.feedEpisode
 | 
						|
    })
 | 
						|
    if (!existingFeed) return false
 | 
						|
 | 
						|
    let hasUpdates = false
 | 
						|
 | 
						|
    // Remove and update existing feed episodes
 | 
						|
    for (const feedEpisode of existingFeed.feedEpisodes) {
 | 
						|
      const oldFeedEpisode = oldFeedEpisodes.find((ep) => ep.id === feedEpisode.id)
 | 
						|
      // Episode removed
 | 
						|
      if (!oldFeedEpisode) {
 | 
						|
        feedEpisode.destroy()
 | 
						|
      } else {
 | 
						|
        let episodeHasUpdates = false
 | 
						|
        const oldFeedEpisodeCleaned = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
 | 
						|
        for (const key in oldFeedEpisodeCleaned) {
 | 
						|
          if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) {
 | 
						|
            episodeHasUpdates = true
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if (episodeHasUpdates) {
 | 
						|
          await feedEpisode.update(oldFeedEpisodeCleaned)
 | 
						|
          hasUpdates = true
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Add new feed episodes
 | 
						|
    for (const episode of oldFeedEpisodes) {
 | 
						|
      if (!existingFeed.feedEpisodes.some((fe) => fe.id === episode.id)) {
 | 
						|
        await this.sequelize.models.feedEpisode.createFromOld(feedObj.id, episode)
 | 
						|
        hasUpdates = true
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let feedHasUpdates = false
 | 
						|
    for (const key in feedObj) {
 | 
						|
      let existingValue = existingFeed[key]
 | 
						|
      if (existingValue instanceof Date) existingValue = existingValue.valueOf()
 | 
						|
 | 
						|
      if (!areEquivalent(existingValue, feedObj[key])) {
 | 
						|
        feedHasUpdates = true
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (feedHasUpdates) {
 | 
						|
      await existingFeed.update(feedObj)
 | 
						|
      hasUpdates = true
 | 
						|
    }
 | 
						|
 | 
						|
    return hasUpdates
 | 
						|
  }
 | 
						|
 | 
						|
  static getFromOld(oldFeed) {
 | 
						|
    const oldFeedMeta = oldFeed.meta || {}
 | 
						|
    return {
 | 
						|
      id: oldFeed.id,
 | 
						|
      slug: oldFeed.slug,
 | 
						|
      entityType: oldFeed.entityType,
 | 
						|
      entityId: oldFeed.entityId,
 | 
						|
      entityUpdatedAt: oldFeed.entityUpdatedAt,
 | 
						|
      serverAddress: oldFeed.serverAddress,
 | 
						|
      feedURL: oldFeed.feedUrl,
 | 
						|
      coverPath: oldFeed.coverPath || null,
 | 
						|
      imageURL: oldFeedMeta.imageUrl,
 | 
						|
      siteURL: oldFeedMeta.link,
 | 
						|
      title: oldFeedMeta.title,
 | 
						|
      description: oldFeedMeta.description,
 | 
						|
      author: oldFeedMeta.author,
 | 
						|
      podcastType: oldFeedMeta.type || null,
 | 
						|
      language: oldFeedMeta.language || null,
 | 
						|
      ownerName: oldFeedMeta.ownerName || null,
 | 
						|
      ownerEmail: oldFeedMeta.ownerEmail || null,
 | 
						|
      explicit: !!oldFeedMeta.explicit,
 | 
						|
      preventIndexing: !!oldFeedMeta.preventIndexing,
 | 
						|
      userId: oldFeed.userId
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  getEntity(options) {
 | 
						|
    if (!this.entityType) return Promise.resolve(null)
 | 
						|
    const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}`
 | 
						|
    return this[mixinMethodName](options)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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.UUIDV4,
 | 
						|
        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
 | 
						|
      }
 | 
						|
    })
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
module.exports = Feed
 |