2023-07-05 01:14:44 +02:00
const { DataTypes , Model } = require ( 'sequelize' )
const Logger = require ( '../Logger' )
const oldLibraryItem = require ( '../objects/LibraryItem' )
2023-07-29 01:03:31 +02:00
const libraryFilters = require ( '../utils/queries/libraryFilters' )
2023-07-05 01:14:44 +02:00
const { areEquivalent } = require ( '../utils/index' )
module . exports = ( sequelize ) => {
class LibraryItem extends Model {
2023-07-19 22:36:18 +02:00
/ * *
* Loads all podcast episodes , all library items in chunks of 500 , then maps them to old library items
* @ todo this is a temporary solution until we can use the sqlite without loading all the library items on init
*
* @ returns { Promise < objects . LibraryItem [ ] > } old library items
* /
static async loadAllLibraryItems ( ) {
let start = Date . now ( )
Logger . info ( ` [LibraryItem] Loading podcast episodes... ` )
const podcastEpisodes = await sequelize . models . podcastEpisode . findAll ( )
Logger . info ( ` [LibraryItem] Finished loading ${ podcastEpisodes . length } podcast episodes in ${ ( ( Date . now ( ) - start ) / 1000 ) . toFixed ( 2 ) } s ` )
start = Date . now ( )
Logger . info ( ` [LibraryItem] Loading library items... ` )
let libraryItems = await this . getAllOldLibraryItemsIncremental ( )
Logger . info ( ` [LibraryItem] Finished loading ${ libraryItems . length } library items in ${ ( ( Date . now ( ) - start ) / 1000 ) . toFixed ( 2 ) } s ` )
// Map LibraryItem to old library item
libraryItems = libraryItems . map ( li => {
if ( li . mediaType === 'podcast' ) {
li . media . podcastEpisodes = podcastEpisodes . filter ( pe => pe . podcastId === li . media . id )
}
return this . getOldLibraryItem ( li )
} )
return libraryItems
}
/ * *
* Loads all LibraryItem in batches of 500
* @ todo temporary solution
*
* @ param { Model < LibraryItem > [ ] } libraryItems
* @ param { number } offset
* @ returns { Promise < Model < LibraryItem > [ ] > }
* /
static async getAllOldLibraryItemsIncremental ( libraryItems = [ ] , offset = 0 ) {
const limit = 500
const rows = await this . getLibraryItemsIncrement ( offset , limit )
libraryItems . push ( ... rows )
if ( ! rows . length || rows . length < limit ) {
return libraryItems
}
Logger . info ( ` [LibraryItem] Loaded ${ rows . length } library items. ${ libraryItems . length } loaded so far. ` )
return this . getAllOldLibraryItemsIncremental ( libraryItems , offset + rows . length )
}
/ * *
* Gets library items partially expanded , not including podcast episodes
* @ todo temporary solution
*
* @ param { number } offset
* @ param { number } limit
* @ returns { Promise < Model < LibraryItem > [ ] > } LibraryItem
* /
static getLibraryItemsIncrement ( offset , limit ) {
return 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
}
] ,
offset ,
limit
} )
}
/ * *
* Currently unused because this is too slow and uses too much mem
*
* @ returns { Array < objects . LibraryItem > } old library items
* /
2023-07-05 01:14:44 +02:00
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 ) )
}
2023-07-19 22:36:18 +02:00
/ * *
* Convert an expanded LibraryItem into an old library item
*
* @ param { Model < LibraryItem > } libraryItemExpanded
* @ returns { oldLibraryItem }
* /
2023-07-05 01:14:44 +02:00
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 ,
2023-07-16 22:05:51 +02:00
oldLibraryItemId : libraryItemExpanded . extraData ? . oldLibraryItemId || null ,
2023-07-05 01:14:44 +02:00
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 : {
2023-07-17 15:09:08 +02:00
attributes : [ 'id' , 'sequence' ]
2023-07-05 01:14:44 +02:00
}
}
]
} ,
{
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 ) ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " episode " ${ existingPodcastEpisode . title } " was removed ` )
2023-07-05 01:14:44 +02:00
await existingPodcastEpisode . destroy ( )
hasUpdates = true
}
}
for ( const updatedPodcastEpisode of updatedPodcastEpisodes ) {
const existingEpisodeMatch = existingPodcastEpisodes . find ( ep => ep . id === updatedPodcastEpisode . id )
if ( ! existingEpisodeMatch ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " episode " ${ updatedPodcastEpisode . title } " was added ` )
2023-07-05 01:14:44 +02:00
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 ) ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " episode " ${ existingEpisodeMatch . title } " ${ key } was updated from " ${ existingValue } " to " ${ updatedEpisodeCleaned [ key ] } " ` )
2023-07-05 01:14:44 +02:00
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 ) ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " author " ${ existingAuthor . name } " was removed ` )
2023-07-05 01:14:44 +02:00
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 ) ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " author " ${ updatedAuthor . name } " was added ` )
2023-07-05 01:14:44 +02:00
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 ) ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " series " ${ existingSeries . name } " was removed ` )
2023-07-05 01:14:44 +02:00
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 ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " series " ${ updatedSeries . name } " was added ` )
2023-07-05 01:14:44 +02:00
await sequelize . models . bookSeries . create ( { seriesId : updatedSeries . id , bookId : libraryItemExpanded . media . id , sequence : updatedSeries . sequence } )
hasUpdates = true
} else if ( existingSeriesMatch . bookSeries . sequence !== updatedSeries . sequence ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " series " ${ updatedSeries . name } " sequence was updated from " ${ existingSeriesMatch . bookSeries . sequence } " to " ${ updatedSeries . sequence } " ` )
2023-07-17 15:09:08 +02:00
await existingSeriesMatch . bookSeries . update ( { id : updatedSeries . id , sequence : updatedSeries . sequence } )
2023-07-05 01:14:44 +02:00
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 ) ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " ${ libraryItemExpanded . mediaType } . ${ key } updated from ${ existingValue } to ${ updatedMedia [ key ] } ` )
2023-07-05 01:14:44 +02:00
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 ) ) {
2023-07-14 21:50:37 +02:00
Logger . dev ( ` [LibraryItem] " ${ libraryItemExpanded . media . title } " ${ key } updated from ${ existingValue } to ${ updatedLibraryItem [ key ] } ` )
2023-07-05 01:14:44 +02:00
hasLibraryItemUpdates = true
}
}
if ( hasLibraryItemUpdates ) {
await libraryItemExpanded . update ( updatedLibraryItem )
2023-07-14 21:50:37 +02:00
Logger . info ( ` [LibraryItem] Library item " ${ libraryItemExpanded . id } " updated ` )
2023-07-05 01:14:44 +02:00
hasUpdates = true
}
return hasUpdates
}
static getFromOld ( oldLibraryItem ) {
2023-07-16 22:05:51 +02:00
const extraData = { }
if ( oldLibraryItem . oldLibraryItemId ) {
extraData . oldLibraryItemId = oldLibraryItem . oldLibraryItemId
}
2023-07-05 01:14:44 +02:00
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 ,
2023-07-29 01:03:31 +02:00
size : oldLibraryItem . size ,
2023-07-05 01:14:44 +02:00
lastScan : oldLibraryItem . lastScan ,
lastScanVersion : oldLibraryItem . scanVersion ,
libraryId : oldLibraryItem . libraryId ,
libraryFolderId : oldLibraryItem . folderId ,
2023-07-16 22:05:51 +02:00
libraryFiles : oldLibraryItem . libraryFiles ? . map ( lf => lf . toJSON ( ) ) || [ ] ,
extraData
2023-07-05 01:14:44 +02:00
}
}
static removeById ( libraryItemId ) {
return this . destroy ( {
where : {
id : libraryItemId
} ,
individualHooks : true
} )
}
2023-07-29 01:03:31 +02:00
static async getByFilterAndSort ( libraryId , userId , { filterBy , sortBy , sortDesc , limit , offset } ) {
const { libraryItems , count } = await libraryFilters . getFilteredLibraryItems ( libraryId , filterBy , sortBy , sortDesc , limit , offset , userId )
return {
libraryItems : libraryItems . map ( ti => this . getOldLibraryItem ( ti ) ) ,
count
}
}
2023-07-05 01:14:44 +02:00
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 ) ,
2023-07-29 01:03:31 +02:00
size : DataTypes . BIGINT ,
2023-07-05 01:14:44 +02:00
lastScan : DataTypes . DATE ,
lastScanVersion : DataTypes . STRING ,
2023-07-16 22:05:51 +02:00
libraryFiles : DataTypes . JSON ,
extraData : DataTypes . JSON
2023-07-05 01:14:44 +02:00
} , {
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
}