From f8f94f2a6d9de0035d364bdfc259dce1332f583c Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 28 Aug 2023 17:50:21 -0500 Subject: [PATCH] Update new library scanner to check for cover images and ebooks --- server/Database.js | 3 ++ server/scanner/LibraryItemScanData.js | 38 ++++++++++++-- server/scanner/LibraryScan.js | 3 +- server/scanner/LibraryScanner.js | 76 ++++++++++++++++++++++++++- 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/server/Database.js b/server/Database.js index 2ae3585e45..4bad018d4b 100644 --- a/server/Database.js +++ b/server/Database.js @@ -25,8 +25,11 @@ class Database { // Cached library filter data this.libraryFilterData = {} + /** @type {import('./objects/settings/ServerSettings')} */ this.serverSettings = null + /** @type {import('./objects/settings/NotificationSettings')} */ this.notificationSettings = null + /** @type {import('./objects/settings/EmailSettings')} */ this.emailSettings = null } diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index 1b40452437..88af6a7d18 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -71,6 +71,16 @@ class LibraryItemScanData { return this.libraryFiles.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } + /** @type {LibraryItem.LibraryFileObject[]} */ + get imageLibraryFiles() { + return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get ebookLibraryFiles() { + return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + /** * * @param {LibraryItem} existingLibraryItem @@ -124,7 +134,7 @@ class LibraryItemScanData { } if (!matchingLibraryFile) { // Library file removed - libraryScan.addLog(LogLevel.INFO, `Library file "${existingLibraryFile.metadata.path}" was removed from library item "${existingLibraryItem.path}"`) + libraryScan.addLog(LogLevel.INFO, `Library file "${existingLibraryFile.metadata.path}" was removed from library item "${existingLibraryItem.relPath}"`) this.libraryFilesRemoved.push(existingLibraryFile) existingLibraryItem.libraryFiles = existingLibraryItem.libraryFiles.filter(lf => lf !== existingLibraryFile) this.hasChanges = true @@ -141,7 +151,11 @@ class LibraryItemScanData { if (libraryFilesAdded.length) { this.hasChanges = true for (const libraryFile of libraryFilesAdded) { - libraryScan.addLog(LogLevel.INFO, `New library file found with path "${libraryFile.metadata.path}" for library item "${existingLibraryItem.path}"`) + 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 + } existingLibraryItem.libraryFiles.push(libraryFile.toJSON()) } } @@ -155,14 +169,15 @@ class LibraryItemScanData { existingLibraryItem.lastScan = Date.now() existingLibraryItem.lastScanVersion = packageJson.version - libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.path}" changed: [${existingLibraryItem.changed()?.join(',') || ''}]`) + libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.relPath}" changed: [${existingLibraryItem.changed()?.join(',') || ''}]`) + libraryScan.resultsUpdated++ if (this.hasLibraryFileChanges) { existingLibraryItem.changed('libraryFiles', true) } await existingLibraryItem.save() } else { - libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.path}" is up-to-date`) + libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.relPath}" is up-to-date`) } } @@ -219,5 +234,20 @@ class LibraryItemScanData { // Fallback to check inode value return this.audioLibraryFilesRemoved.some(af => af.ino === existingAudioFile.ino) } + + /** + * 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) + } } module.exports = LibraryItemScanData \ No newline at end of file diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js index 8e97a41a9c..1be4cc663e 100644 --- a/server/scanner/LibraryScan.js +++ b/server/scanner/LibraryScan.js @@ -13,6 +13,7 @@ class LibraryScan { constructor() { this.id = null this.type = null + /** @type {import('../objects/Library')} */ this.library = null this.verbose = false @@ -117,7 +118,7 @@ class LibraryScan { } if (this.verbose) { - Logger.debug(`[LibraryScan] "${this.libraryName}":`, args) + Logger.debug(`[LibraryScan] "${this.libraryName}":`, ...args) } this.logs.push(logObj) } diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 5cccfaa44c..19a642f225 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -7,6 +7,7 @@ const fs = require('../libs/fsExtra') const fileUtils = require('../utils/fileUtils') const scanUtils = require('../utils/scandir') const { ScanResult, LogLevel } = require('../utils/constants') +const globals = require('../utils/globals') const AudioFileScanner = require('./AudioFileScanner') const ScanOptions = require('./ScanOptions') const LibraryScan = require('./LibraryScan') @@ -128,11 +129,13 @@ class LibraryScanner { libraryScan.addLog(LogLevel.INFO, `Library item "${existingLibraryItem.relPath}" folder exists but has no episodes`) } else { libraryScan.addLog(LogLevel.WARN, `Library Item "${existingLibraryItem.path}" (inode: ${existingLibraryItem.ino}) is missing`) + libraryScan.resultsMissing++ if (!existingLibraryItem.isMissing) { libraryItemIdsMissing.push(existingLibraryItem.id) } } } else { + libraryItemDataFound = libraryItemDataFound.filter(lidf => lidf !== libraryItemData) await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan) if (libraryItemData.hasLibraryFileChanges || libraryItemData.hasPathChange) { await this.rescanLibraryItem(existingLibraryItem, libraryItemData, libraryScan) @@ -153,6 +156,11 @@ class LibraryScanner { } }) } + + // Add new library items + if (libraryItemDataFound.length) { + + } } /** @@ -230,7 +238,6 @@ class LibraryScanner { * @param {LibraryScan} libraryScan */ async rescanLibraryItem(existingLibraryItem, libraryItemData, libraryScan) { - if (existingLibraryItem.mediaType === 'book') { /** @type {Book} */ const media = await existingLibraryItem.getMedia({ @@ -309,10 +316,77 @@ class LibraryScanner { media.changed('audioFiles', true) } + // Check if cover was removed + if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath)) { + media.coverPath = null + hasMediaChanges = true + } + + // Check if cover is not set and image files were found + if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { + // Prefer using a cover image with the name "cover" otherwise use the first image + const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) + media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path + hasMediaChanges = true + } + + // Check if ebook was removed + if (media.ebookFile && (libraryScan.library.settings.audiobooksOnly || libraryItemData.checkEbookFileRemoved(media.ebookFile))) { + media.ebookFile = null + hasMediaChanges = true + } + + // Check if ebook is not set and ebooks were found + if (!media.ebookFile && !libraryScan.library.settings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) { + // Prefer to use an epub ebook then fallback to the first ebook found + let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') + if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0] + // Ebook file is the same as library file except for additional `ebookFormat` + ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase() + media.ebookFile = ebookLibraryFile + media.changed('ebookFile', true) + hasMediaChanges = true + } + + // Check/update the isSupplementary flag on libraryFiles for the LibraryItem + let libraryItemUpdated = false + for (const libraryFile of existingLibraryItem.libraryFiles) { + if (globals.SupportedEbookTypes.includes(libraryFile.metadata.ext.slice(1).toLowerCase())) { + if (media.ebookFile && libraryFile.ino === media.ebookFile.ino) { + if (libraryFile.isSupplementary !== false) { + libraryFile.isSupplementary = false + libraryItemUpdated = true + } + } else if (libraryFile.isSupplementary !== true) { + libraryFile.isSupplementary = true + libraryItemUpdated = true + } + } + } + if (libraryItemUpdated) { + existingLibraryItem.changed('libraryFiles', true) + await existingLibraryItem.save() + } + + // TODO: Update chapters & metadata + if (hasMediaChanges) { await media.save() } } } + + /** + * + * @param {LibraryItemScanData} libraryItemData + * @param {LibraryScan} libraryScan + */ + async scanNewLibraryItem(libraryItemData, libraryScan) { + + if (libraryScan.libraryMediaType === 'book') { + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(libraryScan.libraryMediaType, libraryItemData, libraryItemData.audioLibraryFiles) + // TODO: Create new book + } + } } module.exports = LibraryScanner \ No newline at end of file