mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-30 18:48:55 +01:00
391 lines
15 KiB
JavaScript
391 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.debug(`[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.debug(`[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.debug(`[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.debug(`[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.debug(`[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.debug(`[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.debug(`[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.debug(`[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.debug(`[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.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
|
||
|
hasLibraryItemUpdates = true
|
||
|
}
|
||
|
}
|
||
|
if (hasLibraryItemUpdates) {
|
||
|
await libraryItemExpanded.update(updatedLibraryItem)
|
||
|
hasUpdates = true
|
||
|
}
|
||
|
return hasUpdates
|
||
|
}
|
||
|
|
||
|
static updateFromOld(oldLibraryItem) {
|
||
|
const libraryItem = this.getFromOld(oldLibraryItem)
|
||
|
return this.update(libraryItem, {
|
||
|
where: {
|
||
|
id: libraryItem.id
|
||
|
}
|
||
|
}).then((result) => result[0] > 0).catch((error) => {
|
||
|
Logger.error(`[LibraryItem] Failed to update libraryItem ${libraryItem.id}`, error)
|
||
|
return false
|
||
|
})
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|