mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-04 03:17:00 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			380 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			380 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const { DataTypes, Model } = require('sequelize')
 | 
						|
const Logger = require('../Logger')
 | 
						|
const oldLibraryItem = require('../objects/LibraryItem')
 | 
						|
const { areEquivalent } = require('../utils/index')
 | 
						|
 | 
						|
module.exports = (sequelize) => {
 | 
						|
  class LibraryItem extends Model {
 | 
						|
    static async getAllOldLibraryItems() {
 | 
						|
      let libraryItems = await this.findAll({
 | 
						|
        include: [
 | 
						|
          {
 | 
						|
            model: sequelize.models.book,
 | 
						|
            include: [
 | 
						|
              {
 | 
						|
                model: sequelize.models.author,
 | 
						|
                through: {
 | 
						|
                  attributes: []
 | 
						|
                }
 | 
						|
              },
 | 
						|
              {
 | 
						|
                model: sequelize.models.series,
 | 
						|
                through: {
 | 
						|
                  attributes: ['sequence']
 | 
						|
                }
 | 
						|
              }
 | 
						|
            ]
 | 
						|
          },
 | 
						|
          {
 | 
						|
            model: sequelize.models.podcast,
 | 
						|
            include: [
 | 
						|
              {
 | 
						|
                model: sequelize.models.podcastEpisode
 | 
						|
              }
 | 
						|
            ]
 | 
						|
          }
 | 
						|
        ]
 | 
						|
      })
 | 
						|
      return libraryItems.map(ti => this.getOldLibraryItem(ti))
 | 
						|
    }
 | 
						|
 | 
						|
    static getOldLibraryItem(libraryItemExpanded) {
 | 
						|
      let media = null
 | 
						|
      if (libraryItemExpanded.mediaType === 'book') {
 | 
						|
        media = sequelize.models.book.getOldBook(libraryItemExpanded)
 | 
						|
      } else if (libraryItemExpanded.mediaType === 'podcast') {
 | 
						|
        media = sequelize.models.podcast.getOldPodcast(libraryItemExpanded)
 | 
						|
      }
 | 
						|
 | 
						|
      return new oldLibraryItem({
 | 
						|
        id: libraryItemExpanded.id,
 | 
						|
        ino: libraryItemExpanded.ino,
 | 
						|
        libraryId: libraryItemExpanded.libraryId,
 | 
						|
        folderId: libraryItemExpanded.libraryFolderId,
 | 
						|
        path: libraryItemExpanded.path,
 | 
						|
        relPath: libraryItemExpanded.relPath,
 | 
						|
        isFile: libraryItemExpanded.isFile,
 | 
						|
        mtimeMs: libraryItemExpanded.mtime?.valueOf(),
 | 
						|
        ctimeMs: libraryItemExpanded.ctime?.valueOf(),
 | 
						|
        birthtimeMs: libraryItemExpanded.birthtime?.valueOf(),
 | 
						|
        addedAt: libraryItemExpanded.createdAt.valueOf(),
 | 
						|
        updatedAt: libraryItemExpanded.updatedAt.valueOf(),
 | 
						|
        lastScan: libraryItemExpanded.lastScan?.valueOf(),
 | 
						|
        scanVersion: libraryItemExpanded.lastScanVersion,
 | 
						|
        isMissing: !!libraryItemExpanded.isMissing,
 | 
						|
        isInvalid: !!libraryItemExpanded.isInvalid,
 | 
						|
        mediaType: libraryItemExpanded.mediaType,
 | 
						|
        media,
 | 
						|
        libraryFiles: libraryItemExpanded.libraryFiles
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    static async fullCreateFromOld(oldLibraryItem) {
 | 
						|
      const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem))
 | 
						|
 | 
						|
      if (oldLibraryItem.mediaType === 'book') {
 | 
						|
        const bookObj = sequelize.models.book.getFromOld(oldLibraryItem.media)
 | 
						|
        bookObj.libraryItemId = newLibraryItem.id
 | 
						|
        const newBook = await sequelize.models.book.create(bookObj)
 | 
						|
 | 
						|
        const oldBookAuthors = oldLibraryItem.media.metadata.authors || []
 | 
						|
        const oldBookSeriesAll = oldLibraryItem.media.metadata.series || []
 | 
						|
 | 
						|
        for (const oldBookAuthor of oldBookAuthors) {
 | 
						|
          await sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id })
 | 
						|
        }
 | 
						|
        for (const oldSeries of oldBookSeriesAll) {
 | 
						|
          await sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence })
 | 
						|
        }
 | 
						|
      } else if (oldLibraryItem.mediaType === 'podcast') {
 | 
						|
        const podcastObj = sequelize.models.podcast.getFromOld(oldLibraryItem.media)
 | 
						|
        podcastObj.libraryItemId = newLibraryItem.id
 | 
						|
        const newPodcast = await sequelize.models.podcast.create(podcastObj)
 | 
						|
 | 
						|
        const oldEpisodes = oldLibraryItem.media.episodes || []
 | 
						|
        for (const oldEpisode of oldEpisodes) {
 | 
						|
          const episodeObj = sequelize.models.podcastEpisode.getFromOld(oldEpisode)
 | 
						|
          episodeObj.libraryItemId = newLibraryItem.id
 | 
						|
          episodeObj.podcastId = newPodcast.id
 | 
						|
          await sequelize.models.podcastEpisode.create(episodeObj)
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      return newLibraryItem
 | 
						|
    }
 | 
						|
 | 
						|
    static async fullUpdateFromOld(oldLibraryItem) {
 | 
						|
      const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
 | 
						|
        include: [
 | 
						|
          {
 | 
						|
            model: sequelize.models.book,
 | 
						|
            include: [
 | 
						|
              {
 | 
						|
                model: sequelize.models.author,
 | 
						|
                through: {
 | 
						|
                  attributes: []
 | 
						|
                }
 | 
						|
              },
 | 
						|
              {
 | 
						|
                model: sequelize.models.series,
 | 
						|
                through: {
 | 
						|
                  attributes: ['sequence']
 | 
						|
                }
 | 
						|
              }
 | 
						|
            ]
 | 
						|
          },
 | 
						|
          {
 | 
						|
            model: sequelize.models.podcast,
 | 
						|
            include: [
 | 
						|
              {
 | 
						|
                model: sequelize.models.podcastEpisode
 | 
						|
              }
 | 
						|
            ]
 | 
						|
          }
 | 
						|
        ]
 | 
						|
      })
 | 
						|
      if (!libraryItemExpanded) return false
 | 
						|
 | 
						|
      let hasUpdates = false
 | 
						|
 | 
						|
      // Check update Book/Podcast
 | 
						|
      if (libraryItemExpanded.media) {
 | 
						|
        let updatedMedia = null
 | 
						|
        if (libraryItemExpanded.mediaType === 'podcast') {
 | 
						|
          updatedMedia = sequelize.models.podcast.getFromOld(oldLibraryItem.media)
 | 
						|
 | 
						|
          const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || []
 | 
						|
          const updatedPodcastEpisodes = oldLibraryItem.media.episodes || []
 | 
						|
 | 
						|
          for (const existingPodcastEpisode of existingPodcastEpisodes) {
 | 
						|
            // Episode was removed
 | 
						|
            if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) {
 | 
						|
              Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`)
 | 
						|
              await existingPodcastEpisode.destroy()
 | 
						|
              hasUpdates = true
 | 
						|
            }
 | 
						|
          }
 | 
						|
          for (const updatedPodcastEpisode of updatedPodcastEpisodes) {
 | 
						|
            const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id)
 | 
						|
            if (!existingEpisodeMatch) {
 | 
						|
              Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`)
 | 
						|
              await sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode)
 | 
						|
              hasUpdates = true
 | 
						|
            } else {
 | 
						|
              const updatedEpisodeCleaned = sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode)
 | 
						|
              let episodeHasUpdates = false
 | 
						|
              for (const key in updatedEpisodeCleaned) {
 | 
						|
                let existingValue = existingEpisodeMatch[key]
 | 
						|
                if (existingValue instanceof Date) existingValue = existingValue.valueOf()
 | 
						|
 | 
						|
                if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) {
 | 
						|
                  Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`)
 | 
						|
                  episodeHasUpdates = true
 | 
						|
                }
 | 
						|
              }
 | 
						|
              if (episodeHasUpdates) {
 | 
						|
                await existingEpisodeMatch.update(updatedEpisodeCleaned)
 | 
						|
                hasUpdates = true
 | 
						|
              }
 | 
						|
            }
 | 
						|
          }
 | 
						|
        } else if (libraryItemExpanded.mediaType === 'book') {
 | 
						|
          updatedMedia = sequelize.models.book.getFromOld(oldLibraryItem.media)
 | 
						|
 | 
						|
          const existingAuthors = libraryItemExpanded.media.authors || []
 | 
						|
          const existingSeriesAll = libraryItemExpanded.media.series || []
 | 
						|
          const updatedAuthors = oldLibraryItem.media.metadata.authors || []
 | 
						|
          const updatedSeriesAll = oldLibraryItem.media.metadata.series || []
 | 
						|
 | 
						|
          for (const existingAuthor of existingAuthors) {
 | 
						|
            // Author was removed from Book
 | 
						|
            if (!updatedAuthors.some(au => au.id === existingAuthor.id)) {
 | 
						|
              Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
 | 
						|
              await sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
 | 
						|
              hasUpdates = true
 | 
						|
            }
 | 
						|
          }
 | 
						|
          for (const updatedAuthor of updatedAuthors) {
 | 
						|
            // Author was added
 | 
						|
            if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
 | 
						|
              Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
 | 
						|
              await sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id })
 | 
						|
              hasUpdates = true
 | 
						|
            }
 | 
						|
          }
 | 
						|
          for (const existingSeries of existingSeriesAll) {
 | 
						|
            // Series was removed
 | 
						|
            if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) {
 | 
						|
              Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`)
 | 
						|
              await sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id)
 | 
						|
              hasUpdates = true
 | 
						|
            }
 | 
						|
          }
 | 
						|
          for (const updatedSeries of updatedSeriesAll) {
 | 
						|
            // Series was added/updated
 | 
						|
            const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id)
 | 
						|
            if (!existingSeriesMatch) {
 | 
						|
              Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`)
 | 
						|
              await sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence })
 | 
						|
              hasUpdates = true
 | 
						|
            } else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) {
 | 
						|
              Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`)
 | 
						|
              await existingSeriesMatch.bookSeries.update({ sequence: updatedSeries.sequence })
 | 
						|
              hasUpdates = true
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        let hasMediaUpdates = false
 | 
						|
        for (const key in updatedMedia) {
 | 
						|
          let existingValue = libraryItemExpanded.media[key]
 | 
						|
          if (existingValue instanceof Date) existingValue = existingValue.valueOf()
 | 
						|
 | 
						|
          if (!areEquivalent(updatedMedia[key], existingValue, true)) {
 | 
						|
            Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`)
 | 
						|
            hasMediaUpdates = true
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if (hasMediaUpdates && updatedMedia) {
 | 
						|
          await libraryItemExpanded.media.update(updatedMedia)
 | 
						|
          hasUpdates = true
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      const updatedLibraryItem = this.getFromOld(oldLibraryItem)
 | 
						|
      let hasLibraryItemUpdates = false
 | 
						|
      for (const key in updatedLibraryItem) {
 | 
						|
        let existingValue = libraryItemExpanded[key]
 | 
						|
        if (existingValue instanceof Date) existingValue = existingValue.valueOf()
 | 
						|
 | 
						|
        if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) {
 | 
						|
          Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
 | 
						|
          hasLibraryItemUpdates = true
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (hasLibraryItemUpdates) {
 | 
						|
        await libraryItemExpanded.update(updatedLibraryItem)
 | 
						|
        Logger.info(`[LibraryItem] Library item "${libraryItemExpanded.id}" updated`)
 | 
						|
        hasUpdates = true
 | 
						|
      }
 | 
						|
      return hasUpdates
 | 
						|
    }
 | 
						|
 | 
						|
    static getFromOld(oldLibraryItem) {
 | 
						|
      return {
 | 
						|
        id: oldLibraryItem.id,
 | 
						|
        ino: oldLibraryItem.ino,
 | 
						|
        path: oldLibraryItem.path,
 | 
						|
        relPath: oldLibraryItem.relPath,
 | 
						|
        mediaId: oldLibraryItem.media.id,
 | 
						|
        mediaType: oldLibraryItem.mediaType,
 | 
						|
        isFile: !!oldLibraryItem.isFile,
 | 
						|
        isMissing: !!oldLibraryItem.isMissing,
 | 
						|
        isInvalid: !!oldLibraryItem.isInvalid,
 | 
						|
        mtime: oldLibraryItem.mtimeMs,
 | 
						|
        ctime: oldLibraryItem.ctimeMs,
 | 
						|
        birthtime: oldLibraryItem.birthtimeMs,
 | 
						|
        lastScan: oldLibraryItem.lastScan,
 | 
						|
        lastScanVersion: oldLibraryItem.scanVersion,
 | 
						|
        libraryId: oldLibraryItem.libraryId,
 | 
						|
        libraryFolderId: oldLibraryItem.folderId,
 | 
						|
        libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || []
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    static removeById(libraryItemId) {
 | 
						|
      return this.destroy({
 | 
						|
        where: {
 | 
						|
          id: libraryItemId
 | 
						|
        },
 | 
						|
        individualHooks: true
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    getMedia(options) {
 | 
						|
      if (!this.mediaType) return Promise.resolve(null)
 | 
						|
      const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}`
 | 
						|
      return this[mixinMethodName](options)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  LibraryItem.init({
 | 
						|
    id: {
 | 
						|
      type: DataTypes.UUID,
 | 
						|
      defaultValue: DataTypes.UUIDV4,
 | 
						|
      primaryKey: true
 | 
						|
    },
 | 
						|
    ino: DataTypes.STRING,
 | 
						|
    path: DataTypes.STRING,
 | 
						|
    relPath: DataTypes.STRING,
 | 
						|
    mediaId: DataTypes.UUIDV4,
 | 
						|
    mediaType: DataTypes.STRING,
 | 
						|
    isFile: DataTypes.BOOLEAN,
 | 
						|
    isMissing: DataTypes.BOOLEAN,
 | 
						|
    isInvalid: DataTypes.BOOLEAN,
 | 
						|
    mtime: DataTypes.DATE(6),
 | 
						|
    ctime: DataTypes.DATE(6),
 | 
						|
    birthtime: DataTypes.DATE(6),
 | 
						|
    lastScan: DataTypes.DATE,
 | 
						|
    lastScanVersion: DataTypes.STRING,
 | 
						|
    libraryFiles: DataTypes.JSON
 | 
						|
  }, {
 | 
						|
    sequelize,
 | 
						|
    modelName: 'libraryItem'
 | 
						|
  })
 | 
						|
 | 
						|
  const { library, libraryFolder, book, podcast } = sequelize.models
 | 
						|
  library.hasMany(LibraryItem)
 | 
						|
  LibraryItem.belongsTo(library)
 | 
						|
 | 
						|
  libraryFolder.hasMany(LibraryItem)
 | 
						|
  LibraryItem.belongsTo(libraryFolder)
 | 
						|
 | 
						|
  book.hasOne(LibraryItem, {
 | 
						|
    foreignKey: 'mediaId',
 | 
						|
    constraints: false,
 | 
						|
    scope: {
 | 
						|
      mediaType: 'book'
 | 
						|
    }
 | 
						|
  })
 | 
						|
  LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false })
 | 
						|
 | 
						|
  podcast.hasOne(LibraryItem, {
 | 
						|
    foreignKey: 'mediaId',
 | 
						|
    constraints: false,
 | 
						|
    scope: {
 | 
						|
      mediaType: 'podcast'
 | 
						|
    }
 | 
						|
  })
 | 
						|
  LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })
 | 
						|
 | 
						|
  LibraryItem.addHook('afterFind', findResult => {
 | 
						|
    if (!findResult) return
 | 
						|
 | 
						|
    if (!Array.isArray(findResult)) findResult = [findResult]
 | 
						|
    for (const instance of findResult) {
 | 
						|
      if (instance.mediaType === 'book' && instance.book !== undefined) {
 | 
						|
        instance.media = instance.book
 | 
						|
        instance.dataValues.media = instance.dataValues.book
 | 
						|
      } else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) {
 | 
						|
        instance.media = instance.podcast
 | 
						|
        instance.dataValues.media = instance.dataValues.podcast
 | 
						|
      }
 | 
						|
      // To prevent mistakes:
 | 
						|
      delete instance.book
 | 
						|
      delete instance.dataValues.book
 | 
						|
      delete instance.podcast
 | 
						|
      delete instance.dataValues.podcast
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  LibraryItem.addHook('afterDestroy', async instance => {
 | 
						|
    if (!instance) return
 | 
						|
    const media = await instance.getMedia()
 | 
						|
    if (media) {
 | 
						|
      media.destroy()
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  return LibraryItem
 | 
						|
} |