2023-08-26 23:33:27 +02:00
const packageJson = require ( '../../package.json' )
const { LogLevel } = require ( '../utils/constants' )
const LibraryItem = require ( '../models/LibraryItem' )
2023-08-28 00:19:57 +02:00
const globals = require ( '../utils/globals' )
2023-08-26 23:33:27 +02:00
class LibraryItemScanData {
constructor ( data ) {
/** @type {string} */
this . libraryFolderId = data . libraryFolderId
/** @type {string} */
this . libraryId = data . libraryId
/** @type {string} */
2023-09-02 01:01:17 +02:00
this . mediaType = data . mediaType
/** @type {string} */
2023-08-26 23:33:27 +02:00
this . ino = data . ino
/** @type {number} */
this . mtimeMs = data . mtimeMs
/** @type {number} */
this . ctimeMs = data . ctimeMs
/** @type {number} */
this . birthtimeMs = data . birthtimeMs
/** @type {string} */
this . path = data . path
/** @type {string} */
this . relPath = data . relPath
/** @type {boolean} */
this . isFile = data . isFile
2023-10-09 00:10:43 +02:00
/** @type {import('../utils/scandir').LibraryItemFilenameMetadata} */
2023-08-26 23:33:27 +02:00
this . mediaMetadata = data . mediaMetadata
/** @type {import('../objects/files/LibraryFile')[]} */
this . libraryFiles = data . libraryFiles
// Set after check
/** @type {boolean} */
this . hasChanges
/** @type {boolean} */
this . hasPathChange
/** @type {LibraryItem.LibraryFileObject[]} */
2023-08-28 00:19:57 +02:00
this . libraryFilesRemoved = [ ]
2023-08-26 23:33:27 +02:00
/** @type {LibraryItem.LibraryFileObject[]} */
2023-08-28 00:19:57 +02:00
this . libraryFilesAdded = [ ]
2023-08-26 23:33:27 +02:00
/** @type {LibraryItem.LibraryFileObject[]} */
2023-08-28 00:19:57 +02:00
this . libraryFilesModified = [ ]
}
2023-09-02 01:01:17 +02:00
/ * *
* Used to create a library item
* /
get libraryItemObject ( ) {
let size = 0
this . libraryFiles . forEach ( ( lf ) => size += ( ! isNaN ( lf . metadata . size ) ? Number ( lf . metadata . size ) : 0 ) )
return {
ino : this . ino ,
path : this . path ,
relPath : this . relPath ,
mediaType : this . mediaType ,
isFile : this . isFile ,
mtime : this . mtimeMs ,
2023-09-03 00:49:28 +02:00
ctime : this . ctimeMs ,
2023-09-02 01:01:17 +02:00
birthtime : this . birthtimeMs ,
lastScan : Date . now ( ) ,
lastScanVersion : packageJson . version ,
libraryFiles : this . libraryFiles ,
libraryId : this . libraryId ,
libraryFolderId : this . libraryFolderId ,
size
}
}
2023-08-28 00:19:57 +02:00
/** @type {boolean} */
get hasLibraryFileChanges ( ) {
return this . libraryFilesRemoved . length + this . libraryFilesModified . length + this . libraryFilesAdded . length
}
/** @type {boolean} */
get hasAudioFileChanges ( ) {
2023-09-03 00:49:28 +02:00
return ( this . audioLibraryFilesRemoved . length + this . audioLibraryFilesAdded . length + this . audioLibraryFilesModified . length ) > 0
2023-08-28 00:19:57 +02:00
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFilesModified ( ) {
return this . libraryFilesModified . filter ( lf => globals . SupportedAudioTypes . includes ( lf . metadata . ext ? . slice ( 1 ) . toLowerCase ( ) || '' ) )
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFilesRemoved ( ) {
return this . libraryFilesRemoved . filter ( lf => globals . SupportedAudioTypes . includes ( lf . metadata . ext ? . slice ( 1 ) . toLowerCase ( ) || '' ) )
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFilesAdded ( ) {
return this . libraryFilesAdded . filter ( lf => globals . SupportedAudioTypes . includes ( lf . metadata . ext ? . slice ( 1 ) . toLowerCase ( ) || '' ) )
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFiles ( ) {
return this . libraryFiles . filter ( lf => globals . SupportedAudioTypes . includes ( lf . metadata . ext ? . slice ( 1 ) . toLowerCase ( ) || '' ) )
2023-08-26 23:33:27 +02:00
}
2023-08-29 00:50:21 +02:00
/** @type {LibraryItem.LibraryFileObject[]} */
get imageLibraryFiles ( ) {
return this . libraryFiles . filter ( lf => globals . SupportedImageTypes . includes ( lf . metadata . ext ? . slice ( 1 ) . toLowerCase ( ) || '' ) )
}
2023-09-19 22:42:38 +02:00
/** @type {import('../objects/files/LibraryFile')[]} */
2023-08-29 00:50:21 +02:00
get ebookLibraryFiles ( ) {
return this . libraryFiles . filter ( lf => globals . SupportedEbookTypes . includes ( lf . metadata . ext ? . slice ( 1 ) . toLowerCase ( ) || '' ) )
}
2023-09-02 01:01:17 +02:00
/** @type {LibraryItem.LibraryFileObject} */
get descTxtLibraryFile ( ) {
return this . libraryFiles . find ( lf => lf . metadata . filename === 'desc.txt' )
}
/** @type {LibraryItem.LibraryFileObject} */
get readerTxtLibraryFile ( ) {
return this . libraryFiles . find ( lf => lf . metadata . filename === 'reader.txt' )
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataAbsLibraryFile ( ) {
return this . libraryFiles . find ( lf => lf . metadata . filename === 'metadata.abs' )
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataJsonLibraryFile ( ) {
return this . libraryFiles . find ( lf => lf . metadata . filename === 'metadata.json' )
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataOpfLibraryFile ( ) {
return this . libraryFiles . find ( lf => lf . metadata . ext . toLowerCase ( ) === '.opf' )
}
2023-11-12 14:30:23 +01:00
/** @type {LibraryItem.LibraryFileObject} */
get metadataNfoLibraryFile ( ) {
return this . libraryFiles . find ( lf => lf . metadata . ext . toLowerCase ( ) === '.nfo' )
}
2023-08-26 23:33:27 +02:00
/ * *
*
* @ param { LibraryItem } existingLibraryItem
* @ param { import ( './LibraryScan' ) } libraryScan
2023-09-03 00:49:28 +02:00
* @ returns { boolean } true if changes found
2023-08-26 23:33:27 +02:00
* /
async checkLibraryItemData ( existingLibraryItem , libraryScan ) {
2023-08-28 00:19:57 +02:00
const keysToCompare = [ 'libraryFolderId' , 'ino' , 'path' , 'relPath' , 'isFile' ]
2023-08-26 23:33:27 +02:00
this . hasChanges = false
this . hasPathChange = false
for ( const key of keysToCompare ) {
if ( existingLibraryItem [ key ] !== this [ key ] ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Library item " ${ existingLibraryItem . relPath } " key " ${ key } " changed from " ${ existingLibraryItem [ key ] } " to " ${ this [ key ] } " ` )
existingLibraryItem [ key ] = this [ key ]
this . hasChanges = true
if ( key === 'relPath' ) {
this . hasPathChange = true
}
}
}
2023-08-28 00:19:57 +02:00
// Check mtime, ctime and birthtime
2023-09-03 00:49:28 +02:00
if ( existingLibraryItem . mtime ? . valueOf ( ) !== this . mtimeMs ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Library item " ${ existingLibraryItem . relPath } " key "mtime" changed from " ${ existingLibraryItem . mtime ? . valueOf ( ) } " to " ${ this . mtimeMs } " ` )
2023-08-28 00:19:57 +02:00
existingLibraryItem . mtime = this . mtimeMs
this . hasChanges = true
}
2023-09-03 00:49:28 +02:00
if ( existingLibraryItem . birthtime ? . valueOf ( ) !== this . birthtimeMs ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Library item " ${ existingLibraryItem . relPath } " key "birthtime" changed from " ${ existingLibraryItem . birthtime ? . valueOf ( ) } " to " ${ this . birthtimeMs } " ` )
2023-08-28 00:19:57 +02:00
existingLibraryItem . birthtime = this . birthtimeMs
this . hasChanges = true
}
2023-09-03 00:49:28 +02:00
if ( existingLibraryItem . ctime ? . valueOf ( ) !== this . ctimeMs ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Library item " ${ existingLibraryItem . relPath } " key "ctime" changed from " ${ existingLibraryItem . ctime ? . valueOf ( ) } " to " ${ this . ctimeMs } " ` )
2023-08-28 00:19:57 +02:00
existingLibraryItem . ctime = this . ctimeMs
this . hasChanges = true
}
2023-09-07 00:48:50 +02:00
if ( existingLibraryItem . isMissing ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Library item " ${ existingLibraryItem . relPath } " was missing but now found ` )
existingLibraryItem . isMissing = false
this . hasChanges = true
}
2023-08-28 00:19:57 +02:00
2023-08-26 23:33:27 +02:00
this . libraryFilesRemoved = [ ]
this . libraryFilesModified = [ ]
let libraryFilesAdded = this . libraryFiles . map ( lf => lf )
for ( const existingLibraryFile of existingLibraryItem . libraryFiles ) {
// Find matching library file using path first and fallback to using inode value
let matchingLibraryFile = this . libraryFiles . find ( lf => lf . metadata . path === existingLibraryFile . metadata . path )
if ( ! matchingLibraryFile ) {
matchingLibraryFile = this . libraryFiles . find ( lf => lf . ino === existingLibraryFile . ino )
if ( matchingLibraryFile ) {
libraryScan . addLog ( LogLevel . INFO , ` Library file with path " ${ existingLibraryFile . metadata . path } " not found, but found file with matching inode value " ${ existingLibraryFile . ino } " at path " ${ matchingLibraryFile . metadata . path } " ` )
}
}
if ( ! matchingLibraryFile ) { // Library file removed
2023-08-29 00:50:21 +02:00
libraryScan . addLog ( LogLevel . INFO , ` Library file " ${ existingLibraryFile . metadata . path } " was removed from library item " ${ existingLibraryItem . relPath } " ` )
2023-08-26 23:33:27 +02:00
this . libraryFilesRemoved . push ( existingLibraryFile )
existingLibraryItem . libraryFiles = existingLibraryItem . libraryFiles . filter ( lf => lf !== existingLibraryFile )
this . hasChanges = true
} else {
libraryFilesAdded = libraryFilesAdded . filter ( lf => lf !== matchingLibraryFile )
if ( this . compareUpdateLibraryFile ( existingLibraryItem . path , existingLibraryFile , matchingLibraryFile , libraryScan ) ) {
this . libraryFilesModified . push ( existingLibraryFile )
this . hasChanges = true
}
}
}
// Log new library files found
if ( libraryFilesAdded . length ) {
this . hasChanges = true
for ( const libraryFile of libraryFilesAdded ) {
2023-08-29 00:50:21 +02:00
libraryScan . addLog ( LogLevel . INFO , ` New library file found with path " ${ libraryFile . metadata . path } " for library item " ${ existingLibraryItem . relPath } " ` )
if ( libraryFile . isEBookFile ) {
// Set all new ebook files as supplementary
libraryFile . isSupplementary = true
}
2023-08-26 23:33:27 +02:00
existingLibraryItem . libraryFiles . push ( libraryFile . toJSON ( ) )
}
}
2023-08-28 00:19:57 +02:00
this . libraryFilesAdded = libraryFilesAdded
2023-08-26 23:33:27 +02:00
if ( this . hasChanges ) {
2023-08-28 00:19:57 +02:00
existingLibraryItem . size = 0
existingLibraryItem . libraryFiles . forEach ( ( lf ) => existingLibraryItem . size += lf . metadata . size )
2023-08-26 23:33:27 +02:00
existingLibraryItem . lastScan = Date . now ( )
existingLibraryItem . lastScanVersion = packageJson . version
2023-08-28 00:19:57 +02:00
2023-08-29 00:50:21 +02:00
libraryScan . addLog ( LogLevel . DEBUG , ` Library item " ${ existingLibraryItem . relPath } " changed: [ ${ existingLibraryItem . changed ( ) ? . join ( ',' ) || '' } ] ` )
2023-08-28 00:19:57 +02:00
if ( this . hasLibraryFileChanges ) {
existingLibraryItem . changed ( 'libraryFiles' , true )
}
2023-08-26 23:33:27 +02:00
await existingLibraryItem . save ( )
2023-09-03 00:49:28 +02:00
return true
2023-08-26 23:33:27 +02:00
}
2023-10-09 00:10:43 +02:00
return false
2023-08-26 23:33:27 +02:00
}
/ * *
* Update existing library file with scanned in library file data
* @ param { string } libraryItemPath
* @ param { LibraryItem . LibraryFileObject } existingLibraryFile
* @ param { import ( '../objects/files/LibraryFile' ) } scannedLibraryFile
* @ param { import ( './LibraryScan' ) } libraryScan
* @ returns { boolean } false if no changes
* /
compareUpdateLibraryFile ( libraryItemPath , existingLibraryFile , scannedLibraryFile , libraryScan ) {
let hasChanges = false
if ( existingLibraryFile . ino !== scannedLibraryFile . ino ) {
existingLibraryFile . ino = scannedLibraryFile . ino
hasChanges = true
}
for ( const key in existingLibraryFile . metadata ) {
if ( existingLibraryFile . metadata [ key ] !== scannedLibraryFile . metadata [ key ] ) {
if ( key !== 'path' && key !== 'relPath' ) {
2023-09-03 22:14:58 +02:00
libraryScan . addLog ( LogLevel . DEBUG , ` Library file " ${ existingLibraryFile . metadata . relPath } " for library item " ${ libraryItemPath } " key " ${ key } " changed from " ${ existingLibraryFile . metadata [ key ] } " to " ${ scannedLibraryFile . metadata [ key ] } " ` )
2023-08-26 23:33:27 +02:00
} else {
libraryScan . addLog ( LogLevel . DEBUG , ` Library file for library item " ${ libraryItemPath } " key " ${ key } " changed from " ${ existingLibraryFile . metadata [ key ] } " to " ${ scannedLibraryFile . metadata [ key ] } " ` )
}
existingLibraryFile . metadata [ key ] = scannedLibraryFile . metadata [ key ]
hasChanges = true
}
}
if ( hasChanges ) {
existingLibraryFile . updatedAt = Date . now ( )
}
return hasChanges
}
2023-08-28 00:19:57 +02:00
/ * *
* Check if existing audio file on Book was removed
* @ param { import ( '../models/Book' ) . AudioFileObject } existingAudioFile
* @ returns { boolean } true if audio file was removed
* /
checkAudioFileRemoved ( existingAudioFile ) {
if ( ! this . audioLibraryFilesRemoved . length ) return false
// First check exact path
if ( this . audioLibraryFilesRemoved . some ( af => af . metadata . path === existingAudioFile . metadata . path ) ) {
return true
}
// Fallback to check inode value
return this . audioLibraryFilesRemoved . some ( af => af . ino === existingAudioFile . ino )
}
2023-08-29 00:50:21 +02:00
/ * *
* Check if existing ebook file on Book was removed
* @ param { import ( '../models/Book' ) . EBookFileObject } ebookFile
* @ returns { boolean } true if ebook file was removed
* /
checkEbookFileRemoved ( ebookFile ) {
if ( ! this . ebookLibraryFiles . length ) return true
if ( this . ebookLibraryFiles . some ( lf => lf . metadata . path === ebookFile . metadata . path ) ) {
return false
}
return ! this . ebookLibraryFiles . some ( lf => lf . ino === ebookFile . ino )
}
2023-10-09 00:10:43 +02:00
/ * *
* Set data parsed from filenames
*
* @ param { Object } bookMetadata
* /
setBookMetadataFromFilenames ( bookMetadata ) {
2023-10-14 22:04:16 +02:00
const keysToMap = [ 'title' , 'subtitle' , 'publishedYear' , 'asin' ]
2023-10-09 00:10:43 +02:00
for ( const key in this . mediaMetadata ) {
if ( keysToMap . includes ( key ) && this . mediaMetadata [ key ] ) {
bookMetadata [ key ] = this . mediaMetadata [ key ]
}
}
if ( this . mediaMetadata . authors ? . length ) {
bookMetadata . authors = this . mediaMetadata . authors
}
if ( this . mediaMetadata . narrators ? . length ) {
bookMetadata . narrators = this . mediaMetadata . narrators
}
if ( this . mediaMetadata . seriesName ) {
bookMetadata . series = [
{
name : this . mediaMetadata . seriesName ,
sequence : this . mediaMetadata . seriesSequence || null
}
]
}
}
2023-08-26 23:33:27 +02:00
}
module . exports = LibraryItemScanData