mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 09:38:56 +01:00
387 lines
15 KiB
JavaScript
387 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,
|
|
oldLibraryItemId: libraryItemExpanded.extraData?.oldLibraryItemId || null,
|
|
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) {
|
|
const extraData = {}
|
|
if (oldLibraryItem.oldLibraryItemId) {
|
|
extraData.oldLibraryItemId = oldLibraryItem.oldLibraryItemId
|
|
}
|
|
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()) || [],
|
|
extraData
|
|
}
|
|
}
|
|
|
|
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,
|
|
extraData: 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
|
|
} |