mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			241 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			241 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const { DataTypes, Model } = require('sequelize')
 | 
						|
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
 | 
						|
/**
 | 
						|
 * @typedef ChapterObject
 | 
						|
 * @property {number} id
 | 
						|
 * @property {number} start
 | 
						|
 * @property {number} end
 | 
						|
 * @property {string} title
 | 
						|
 */
 | 
						|
 | 
						|
class PodcastEpisode extends Model {
 | 
						|
  constructor(values, options) {
 | 
						|
    super(values, options)
 | 
						|
 | 
						|
    /** @type {string} */
 | 
						|
    this.id
 | 
						|
    /** @type {number} */
 | 
						|
    this.index
 | 
						|
    /** @type {string} */
 | 
						|
    this.season
 | 
						|
    /** @type {string} */
 | 
						|
    this.episode
 | 
						|
    /** @type {string} */
 | 
						|
    this.episodeType
 | 
						|
    /** @type {string} */
 | 
						|
    this.title
 | 
						|
    /** @type {string} */
 | 
						|
    this.subtitle
 | 
						|
    /** @type {string} */
 | 
						|
    this.description
 | 
						|
    /** @type {string} */
 | 
						|
    this.pubDate
 | 
						|
    /** @type {string} */
 | 
						|
    this.enclosureURL
 | 
						|
    /** @type {BigInt} */
 | 
						|
    this.enclosureSize
 | 
						|
    /** @type {string} */
 | 
						|
    this.enclosureType
 | 
						|
    /** @type {Date} */
 | 
						|
    this.publishedAt
 | 
						|
    /** @type {import('./Book').AudioFileObject} */
 | 
						|
    this.audioFile
 | 
						|
    /** @type {ChapterObject[]} */
 | 
						|
    this.chapters
 | 
						|
    /** @type {Object} */
 | 
						|
    this.extraData
 | 
						|
    /** @type {string} */
 | 
						|
    this.podcastId
 | 
						|
    /** @type {Date} */
 | 
						|
    this.createdAt
 | 
						|
    /** @type {Date} */
 | 
						|
    this.updatedAt
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   *
 | 
						|
   * @param {import('../utils/podcastUtils').RssPodcastEpisode} rssPodcastEpisode
 | 
						|
   * @param {string} podcastId
 | 
						|
   * @param {import('../objects/files/AudioFile')} audioFile
 | 
						|
   */
 | 
						|
  static async createFromRssPodcastEpisode(rssPodcastEpisode, podcastId, audioFile) {
 | 
						|
    const podcastEpisode = {
 | 
						|
      index: null,
 | 
						|
      season: rssPodcastEpisode.season,
 | 
						|
      episode: rssPodcastEpisode.episode,
 | 
						|
      episodeType: rssPodcastEpisode.episodeType,
 | 
						|
      title: rssPodcastEpisode.title,
 | 
						|
      subtitle: rssPodcastEpisode.subtitle,
 | 
						|
      description: rssPodcastEpisode.description,
 | 
						|
      pubDate: rssPodcastEpisode.pubDate,
 | 
						|
      enclosureURL: rssPodcastEpisode.enclosure?.url || null,
 | 
						|
      enclosureSize: rssPodcastEpisode.enclosure?.length || null,
 | 
						|
      enclosureType: rssPodcastEpisode.enclosure?.type || null,
 | 
						|
      publishedAt: rssPodcastEpisode.publishedAt,
 | 
						|
      podcastId,
 | 
						|
      audioFile: audioFile.toJSON(),
 | 
						|
      chapters: [],
 | 
						|
      extraData: {}
 | 
						|
    }
 | 
						|
    if (rssPodcastEpisode.guid) {
 | 
						|
      podcastEpisode.extraData.guid = rssPodcastEpisode.guid
 | 
						|
    }
 | 
						|
 | 
						|
    if (audioFile.chapters?.length) {
 | 
						|
      podcastEpisode.chapters = audioFile.chapters.map((ch) => ({ ...ch }))
 | 
						|
    } else if (rssPodcastEpisode.chapters?.length) {
 | 
						|
      podcastEpisode.chapters = rssPodcastEpisode.chapters.map((ch) => ({ ...ch }))
 | 
						|
    }
 | 
						|
 | 
						|
    return this.create(podcastEpisode)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initialize model
 | 
						|
   * @param {import('../Database').sequelize} sequelize
 | 
						|
   */
 | 
						|
  static init(sequelize) {
 | 
						|
    super.init(
 | 
						|
      {
 | 
						|
        id: {
 | 
						|
          type: DataTypes.UUID,
 | 
						|
          defaultValue: DataTypes.UUIDV4,
 | 
						|
          primaryKey: true
 | 
						|
        },
 | 
						|
        index: DataTypes.INTEGER,
 | 
						|
        season: DataTypes.STRING,
 | 
						|
        episode: DataTypes.STRING,
 | 
						|
        episodeType: DataTypes.STRING,
 | 
						|
        title: DataTypes.STRING,
 | 
						|
        subtitle: DataTypes.STRING(1000),
 | 
						|
        description: DataTypes.TEXT,
 | 
						|
        pubDate: DataTypes.STRING,
 | 
						|
        enclosureURL: DataTypes.STRING,
 | 
						|
        enclosureSize: DataTypes.BIGINT,
 | 
						|
        enclosureType: DataTypes.STRING,
 | 
						|
        publishedAt: DataTypes.DATE,
 | 
						|
 | 
						|
        audioFile: DataTypes.JSON,
 | 
						|
        chapters: DataTypes.JSON,
 | 
						|
        extraData: DataTypes.JSON
 | 
						|
      },
 | 
						|
      {
 | 
						|
        sequelize,
 | 
						|
        modelName: 'podcastEpisode',
 | 
						|
        indexes: [
 | 
						|
          {
 | 
						|
            name: 'podcastEpisode_createdAt_podcastId',
 | 
						|
            fields: ['createdAt', 'podcastId']
 | 
						|
          },
 | 
						|
          {
 | 
						|
            name: 'podcast_episodes_published_at',
 | 
						|
            fields: ['publishedAt']
 | 
						|
          }
 | 
						|
        ]
 | 
						|
      }
 | 
						|
    )
 | 
						|
 | 
						|
    const { podcast } = sequelize.models
 | 
						|
    podcast.hasMany(PodcastEpisode, {
 | 
						|
      onDelete: 'CASCADE'
 | 
						|
    })
 | 
						|
    PodcastEpisode.belongsTo(podcast)
 | 
						|
 | 
						|
    PodcastEpisode.addHook('afterDestroy', async (instance) => {
 | 
						|
      libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterDestroy')
 | 
						|
    })
 | 
						|
 | 
						|
    PodcastEpisode.addHook('afterCreate', async (instance) => {
 | 
						|
      libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterCreate')
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  get size() {
 | 
						|
    return this.audioFile?.metadata.size || 0
 | 
						|
  }
 | 
						|
 | 
						|
  get duration() {
 | 
						|
    return this.audioFile?.duration || 0
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used for matching the episode with an episode in the RSS feed
 | 
						|
   *
 | 
						|
   * @param {string} guid
 | 
						|
   * @param {string} enclosureURL
 | 
						|
   * @returns {boolean}
 | 
						|
   */
 | 
						|
  checkMatchesGuidOrEnclosureUrl(guid, enclosureURL) {
 | 
						|
    if (this.extraData?.guid && this.extraData.guid === guid) {
 | 
						|
      return true
 | 
						|
    }
 | 
						|
    if (this.enclosureURL && this.enclosureURL === enclosureURL) {
 | 
						|
      return true
 | 
						|
    }
 | 
						|
    return false
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used in client players
 | 
						|
   *
 | 
						|
   * @param {string} libraryItemId
 | 
						|
   * @returns {import('./Book').AudioTrack}
 | 
						|
   */
 | 
						|
  getAudioTrack(libraryItemId) {
 | 
						|
    const track = structuredClone(this.audioFile)
 | 
						|
    track.startOffset = 0
 | 
						|
    track.title = this.audioFile.metadata.filename
 | 
						|
    track.contentUrl = `/api/items/${libraryItemId}/file/${track.ino}`
 | 
						|
    return track
 | 
						|
  }
 | 
						|
 | 
						|
  toOldJSON(libraryItemId) {
 | 
						|
    if (!libraryItemId) {
 | 
						|
      throw new Error(`[PodcastEpisode] Cannot convert to old JSON because libraryItemId is not provided`)
 | 
						|
    }
 | 
						|
 | 
						|
    let enclosure = null
 | 
						|
    if (this.enclosureURL) {
 | 
						|
      enclosure = {
 | 
						|
        url: this.enclosureURL,
 | 
						|
        type: this.enclosureType,
 | 
						|
        length: this.enclosureSize !== null ? String(this.enclosureSize) : null
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      libraryItemId: libraryItemId,
 | 
						|
      podcastId: this.podcastId,
 | 
						|
      id: this.id,
 | 
						|
      oldEpisodeId: this.extraData?.oldEpisodeId || null,
 | 
						|
      index: this.index,
 | 
						|
      season: this.season,
 | 
						|
      episode: this.episode,
 | 
						|
      episodeType: this.episodeType,
 | 
						|
      title: this.title,
 | 
						|
      subtitle: this.subtitle,
 | 
						|
      description: this.description,
 | 
						|
      enclosure,
 | 
						|
      guid: this.extraData?.guid || null,
 | 
						|
      pubDate: this.pubDate,
 | 
						|
      chapters: structuredClone(this.chapters),
 | 
						|
      audioFile: structuredClone(this.audioFile),
 | 
						|
      publishedAt: this.publishedAt?.valueOf() || null,
 | 
						|
      addedAt: this.createdAt.valueOf(),
 | 
						|
      updatedAt: this.updatedAt.valueOf()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  toOldJSONExpanded(libraryItemId) {
 | 
						|
    const json = this.toOldJSON(libraryItemId)
 | 
						|
 | 
						|
    json.audioTrack = this.getAudioTrack(libraryItemId)
 | 
						|
    json.size = this.size
 | 
						|
    json.duration = this.duration
 | 
						|
 | 
						|
    return json
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
module.exports = PodcastEpisode
 |