diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index c738c52e..c12441b2 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -20,6 +20,7 @@ const LibraryScan = require("./LibraryScan") const OpfFileScanner = require('./OpfFileScanner') const NfoFileScanner = require('./NfoFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') +const EBookFile = require("../objects/files/EBookFile") /** * Metadata for books pulled from files @@ -84,7 +85,7 @@ class BookScanner { // Update audio files that were modified if (libraryItemData.audioLibraryFilesModified.length) { - let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified) + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) media.audioFiles = media.audioFiles.map((audioFileObj) => { let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path) if (!matchedScannedAudioFile) { @@ -138,11 +139,25 @@ class BookScanner { } // Check if cover was removed - if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { + if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { media.coverPath = null hasMediaChanges = true } + // Update cover if it was modified + if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { + let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) + if (coverMatch) { + const coverPath = coverMatch.new.metadata.path + if (coverPath !== media.coverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating book cover "${media.coverPath}" => "${coverPath}" for book "${media.title}"`) + media.coverPath = coverPath + media.changed('coverPath', true) + 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 @@ -157,6 +172,19 @@ class BookScanner { hasMediaChanges = true } + // Update ebook if it was modified + if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) { + let ebookMatch = libraryItemData.ebookLibraryFilesModified.find(eFile => eFile.old.metadata.path === media.ebookFile.metadata.path) + if (ebookMatch) { + const ebookFile = new EBookFile(ebookMatch.new) + ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase() + libraryScan.addLog(LogLevel.DEBUG, `Updating book ebook file "${media.ebookFile.metadata.path}" => "${ebookFile.metadata.path}" for book "${media.title}"`) + media.ebookFile = ebookFile.toJSON() + media.changed('ebookFile', true) + hasMediaChanges = true + } + } + // Check if ebook is not set and ebooks were found if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) { // Prefer to use an epub ebook then fallback to the first ebook found diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index b604e4d7..d5a4a7a2 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -4,6 +4,12 @@ const LibraryItem = require('../models/LibraryItem') const globals = require('../utils/globals') class LibraryItemScanData { + /** + * @typedef LibraryFileModifiedObject + * @property {LibraryItem.LibraryFileObject} old + * @property {LibraryItem.LibraryFileObject} new + */ + constructor(data) { /** @type {string} */ this.libraryFolderId = data.libraryFolderId @@ -39,7 +45,7 @@ class LibraryItemScanData { this.libraryFilesRemoved = [] /** @type {LibraryItem.LibraryFileObject[]} */ this.libraryFilesAdded = [] - /** @type {LibraryItem.LibraryFileObject[]} */ + /** @type {LibraryFileModifiedObject[]} */ this.libraryFilesModified = [] } @@ -77,9 +83,9 @@ class LibraryItemScanData { return (this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified.length) > 0 } - /** @type {LibraryItem.LibraryFileObject[]} */ + /** @type {LibraryFileModifiedObject[]} */ get audioLibraryFilesModified() { - return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ @@ -97,12 +103,42 @@ class LibraryItemScanData { return this.libraryFiles.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } + /** @type {LibraryFileModifiedObject[]} */ + get imageLibraryFilesModified() { + return this.libraryFilesModified.filter(lf => globals.SupportedImageTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get imageLibraryFilesRemoved() { + return this.libraryFilesRemoved.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get imageLibraryFilesAdded() { + return this.libraryFilesAdded.filter(lf => globals.SupportedImageTypes.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 {import('../objects/files/LibraryFile')[]} */ + /** @type {LibraryFileModifiedObject[]} */ + get ebookLibraryFilesModified() { + return this.libraryFilesModified.filter(lf => globals.SupportedEbookTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get ebookLibraryFilesRemoved() { + return this.libraryFilesRemoved.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get ebookLibraryFilesAdded() { + return this.libraryFilesAdded.filter(lf => globals.SupportedEbookTypes.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() || '')) } @@ -153,7 +189,7 @@ class LibraryItemScanData { existingLibraryItem[key] = this[key] this.hasChanges = true - if (key === 'relPath') { + if (key === 'relPath' || key === 'path') { this.hasPathChange = true } } @@ -202,8 +238,9 @@ class LibraryItemScanData { this.hasChanges = true } else { libraryFilesAdded = libraryFilesAdded.filter(lf => lf !== matchingLibraryFile) + let existingLibraryFileBefore = structuredClone(existingLibraryFile) if (this.compareUpdateLibraryFile(existingLibraryItem.path, existingLibraryFile, matchingLibraryFile, libraryScan)) { - this.libraryFilesModified.push(existingLibraryFile) + this.libraryFilesModified.push({old: existingLibraryFileBefore, new: existingLibraryFile}) this.hasChanges = true } } diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 07dcbb11..4958d5f7 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -71,7 +71,7 @@ class PodcastScanner { // Update audio files that were modified if (libraryItemData.audioLibraryFilesModified.length) { - let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified) + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) for (const podcastEpisode of existingPodcastEpisodes) { let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === podcastEpisode.audioFile.metadata.path) @@ -132,11 +132,25 @@ class PodcastScanner { let hasMediaChanges = false // Check if cover was removed - if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath)) { + if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath)) { media.coverPath = null hasMediaChanges = true } + // Update cover if it was modified + if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { + let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) + if (coverMatch) { + const coverPath = coverMatch.new.metadata.path + if (coverPath !== media.coverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating podcast cover "${media.coverPath}" => "${coverPath}" for podcast "${media.title}"`) + media.coverPath = coverPath + media.changed('coverPath', true) + 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