diff --git a/server/Server.js b/server/Server.js index eb8045f2..def6ed4b 100644 --- a/server/Server.js +++ b/server/Server.js @@ -14,7 +14,6 @@ const Logger = require('./Logger') const Auth = require('./Auth') const Watcher = require('./Watcher') -const Scanner = require('./scanner/Scanner') const Database = require('./Database') const SocketAuthority = require('./SocketAuthority') @@ -23,7 +22,6 @@ const HlsRouter = require('./routers/HlsRouter') const NotificationManager = require('./managers/NotificationManager') const EmailManager = require('./managers/EmailManager') -const CoverManager = require('./managers/CoverManager') const AbMergeManager = require('./managers/AbMergeManager') const CacheManager = require('./managers/CacheManager') const LogManager = require('./managers/LogManager') @@ -65,15 +63,11 @@ class Server { this.emailManager = new EmailManager() this.backupManager = new BackupManager() this.logManager = new LogManager() - this.cacheManager = new CacheManager() this.abMergeManager = new AbMergeManager(this.taskManager) this.playbackSessionManager = new PlaybackSessionManager() - this.coverManager = new CoverManager(this.cacheManager) this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager) this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager) this.rssFeedManager = new RssFeedManager() - - this.scanner = new Scanner(this.coverManager) this.cronManager = new CronManager(this.podcastManager) // Routers @@ -110,7 +104,7 @@ class Server { } await this.cleanUserData() // Remove invalid user item progress - await this.cacheManager.ensureCachePaths() + await CacheManager.ensureCachePaths() await this.backupManager.init() await this.logManager.init() diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index f7ab98e1..1c1454f8 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -1,6 +1,7 @@ const SocketIO = require('socket.io') const Logger = require('./Logger') const Database = require('./Database') +const LibraryScanner = require('./scanner/LibraryScanner') class SocketAuthority { constructor() { @@ -180,7 +181,7 @@ class SocketAuthority { const initialPayload = { userId: client.user.id, username: client.user.username, - librariesScanning: this.Server.scanner.librariesScanning + librariesScanning: LibraryScanner.librariesScanning } if (user.isAdminOrUp) { initialPayload.usersOnline = this.getUsersOnline() diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index f6f62de1..531d2b42 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -5,6 +5,8 @@ const { createNewSortInstance } = require('../libs/fastSort') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +const CacheManager = require('../managers/CacheManager') +const CoverManager = require('../managers/CoverManager') const AuthorFinder = require('../finders/AuthorFinder') const { reqSupportsWebp } = require('../utils/index') @@ -68,13 +70,13 @@ class AuthorController { // Updating/removing cover image if (payload.imagePath !== undefined && payload.imagePath !== req.author.imagePath) { if (!payload.imagePath && req.author.imagePath) { // If removing image then remove file - await this.cacheManager.purgeImageCache(req.author.id) // Purge cache - await this.coverManager.removeFile(req.author.imagePath) + await CacheManager.purgeImageCache(req.author.id) // Purge cache + await CoverManager.removeFile(req.author.imagePath) } else if (payload.imagePath.startsWith('http')) { // Check if image path is a url const imageData = await AuthorFinder.saveAuthorImage(req.author.id, payload.imagePath) if (imageData) { if (req.author.imagePath) { - await this.cacheManager.purgeImageCache(req.author.id) // Purge cache + await CacheManager.purgeImageCache(req.author.id) // Purge cache } payload.imagePath = imageData.path hasUpdated = true @@ -86,7 +88,7 @@ class AuthorController { } if (req.author.imagePath) { - await this.cacheManager.purgeImageCache(req.author.id) // Purge cache + await CacheManager.purgeImageCache(req.author.id) // Purge cache } } } @@ -186,7 +188,7 @@ class AuthorController { // Only updates image if there was no image before or the author ASIN was updated if (authorData.image && (!req.author.imagePath || hasUpdates)) { - this.cacheManager.purgeImageCache(req.author.id) + await CacheManager.purgeImageCache(req.author.id) const imageData = await AuthorFinder.saveAuthorImage(req.author.id, authorData.image) if (imageData) { @@ -232,7 +234,7 @@ class AuthorController { height: height ? parseInt(height) : null, width: width ? parseInt(width) : null } - return this.cacheManager.handleAuthorCache(res, author, options) + return CacheManager.handleAuthorCache(res, author, options) } async middleware(req, res, next) { diff --git a/server/controllers/CacheController.js b/server/controllers/CacheController.js index 815af44d..95c5fe0c 100644 --- a/server/controllers/CacheController.js +++ b/server/controllers/CacheController.js @@ -1,4 +1,4 @@ -const Logger = require('../Logger') +const CacheManager = require('../managers/CacheManager') class CacheController { constructor() { } @@ -8,7 +8,7 @@ class CacheController { if (!req.user.isAdminOrUp) { return res.sendStatus(403) } - await this.cacheManager.purgeAll() + await CacheManager.purgeAll() res.sendStatus(200) } @@ -17,7 +17,7 @@ class CacheController { if (!req.user.isAdminOrUp) { return res.sendStatus(403) } - await this.cacheManager.purgeItems() + await CacheManager.purgeItems() res.sendStatus(200) } } diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index ae36686a..185c3bd9 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -15,6 +15,7 @@ const naturalSort = createNewSortInstance({ }) const LibraryScanner = require('../scanner/LibraryScanner') +const Scanner = require('../scanner/Scanner') const Database = require('../Database') const libraryFilters = require('../utils/queries/libraryFilters') const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters') @@ -772,7 +773,7 @@ class LibraryController { Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user) return res.sendStatus(403) } - this.scanner.matchLibraryItems(req.library) + Scanner.matchLibraryItems(req.library) res.sendStatus(200) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 89654b1c..4def9c04 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -10,6 +10,9 @@ const { ScanResult } = require('../utils/constants') const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') const LibraryItemScanner = require('../scanner/LibraryItemScanner') const AudioFileScanner = require('../scanner/AudioFileScanner') +const Scanner = require('../scanner/Scanner') +const CacheManager = require('../managers/CacheManager') +const CoverManager = require('../managers/CoverManager') class LibraryItemController { constructor() { } @@ -56,7 +59,7 @@ class LibraryItemController { var libraryItem = req.libraryItem // Item has cover and update is removing cover so purge it from cache if (libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) { - await this.cacheManager.purgeCoverCache(libraryItem.id) + await CacheManager.purgeCoverCache(libraryItem.id) } const hasUpdates = libraryItem.update(req.body) @@ -104,7 +107,7 @@ class LibraryItemController { // Item has cover and update is removing cover so purge it from cache if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) { - await this.cacheManager.purgeCoverCache(libraryItem.id) + await CacheManager.purgeCoverCache(libraryItem.id) } // Book specific @@ -165,10 +168,10 @@ class LibraryItemController { var result = null if (req.body && req.body.url) { Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`) - result = await this.coverManager.downloadCoverFromUrl(libraryItem, req.body.url) + result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url) } else if (req.files && req.files.cover) { Logger.debug(`[LibraryItemController] Handling uploaded cover`) - result = await this.coverManager.uploadCover(libraryItem, req.files.cover) + result = await CoverManager.uploadCover(libraryItem, req.files.cover) } else { return res.status(400).send('Invalid request no file or url') } @@ -194,7 +197,7 @@ class LibraryItemController { return res.status(400).send('Invalid request no cover path') } - const validationResult = await this.coverManager.validateCoverPath(req.body.cover, libraryItem) + const validationResult = await CoverManager.validateCoverPath(req.body.cover, libraryItem) if (validationResult.error) { return res.status(500).send(validationResult.error) } @@ -214,7 +217,7 @@ class LibraryItemController { if (libraryItem.media.coverPath) { libraryItem.updateMediaCover('') - await this.cacheManager.purgeCoverCache(libraryItem.id) + await CacheManager.purgeCoverCache(libraryItem.id) await Database.updateLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) } @@ -243,7 +246,7 @@ class LibraryItemController { height: height ? parseInt(height) : null, width: width ? parseInt(width) : null } - return this.cacheManager.handleCoverCache(res, libraryItem, options) + return CacheManager.handleCoverCache(res, libraryItem, options) } // GET: api/items/:id/stream @@ -297,7 +300,7 @@ class LibraryItemController { var libraryItem = req.libraryItem var options = req.body || {} - var matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options) + var matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options) res.json(matchResult) } @@ -421,7 +424,7 @@ class LibraryItemController { res.sendStatus(200) for (const libraryItem of libraryItems) { - const matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options) + const matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options) if (matchResult.updated) { itemsUpdated++ } else if (matchResult.warning) { diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 1447dc03..0e1ebcd3 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -7,6 +7,9 @@ const fs = require('../libs/fsExtra') const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils') const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils') +const Scanner = require('../scanner/Scanner') +const CoverManager = require('../managers/CoverManager') + const LibraryItem = require('../objects/LibraryItem') class PodcastController { @@ -73,7 +76,7 @@ class PodcastController { if (payload.media.metadata.imageUrl) { // TODO: Scan cover image to library files // Podcast cover will always go into library item folder - const coverResponse = await this.coverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl, true) + const coverResponse = await CoverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl, true) if (coverResponse) { if (coverResponse.error) { Logger.error(`[PodcastController] Download cover error from "${payload.media.metadata.imageUrl}": ${coverResponse.error}`) @@ -200,7 +203,7 @@ class PodcastController { } const overrideDetails = req.query.override === '1' - const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails }) + const episodesUpdated = await Scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails }) if (episodesUpdated) { await Database.updateLibraryItem(req.libraryItem) SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) diff --git a/server/managers/CacheManager.js b/server/managers/CacheManager.js index 7ce9ca66..fb0cd26c 100644 --- a/server/managers/CacheManager.js +++ b/server/managers/CacheManager.js @@ -6,13 +6,21 @@ const { resizeImage } = require('../utils/ffmpegHelpers') class CacheManager { constructor() { + this.CachePath = null + this.CoverCachePath = null + this.ImageCachePath = null + this.ItemCachePath = null + } + + /** + * Create cache directory paths if they dont exist + */ + async ensureCachePaths() { // Creates cache paths if necessary and sets owner and permissions this.CachePath = Path.join(global.MetadataPath, 'cache') this.CoverCachePath = Path.join(this.CachePath, 'covers') this.ImageCachePath = Path.join(this.CachePath, 'images') this.ItemCachePath = Path.join(this.CachePath, 'items') - } - async ensureCachePaths() { // Creates cache paths if necessary and sets owner and permissions if (!(await fs.pathExists(this.CachePath))) { await fs.mkdir(this.CachePath) } @@ -151,4 +159,4 @@ class CacheManager { readStream.pipe(res) } } -module.exports = CacheManager \ No newline at end of file +module.exports = new CacheManager() \ No newline at end of file diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index f2545985..1a83b7cc 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -7,19 +7,16 @@ const imageType = require('../libs/imageType') const globals = require('../utils/globals') const { downloadFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils') const { extractCoverArt } = require('../utils/ffmpegHelpers') +const CacheManager = require('../managers/CacheManager') class CoverManager { - constructor(cacheManager) { - this.cacheManager = cacheManager - - this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items') - } + constructor() { } getCoverDirectory(libraryItem) { - if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) { + if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile) { return libraryItem.path } else { - return Path.posix.join(this.ItemMetadataPath, libraryItem.id) + return Path.posix.join(Path.posix.join(global.MetadataPath, 'items'), libraryItem.id) } } @@ -106,7 +103,7 @@ class CoverManager { } await this.removeOldCovers(coverDirPath, extname) - await this.cacheManager.purgeCoverCache(libraryItem.id) + await CacheManager.purgeCoverCache(libraryItem.id) Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`) @@ -144,7 +141,7 @@ class CoverManager { await fs.rename(temppath, coverFullPath) await this.removeOldCovers(coverDirPath, '.' + imgtype.ext) - await this.cacheManager.purgeCoverCache(libraryItem.id) + await CacheManager.purgeCoverCache(libraryItem.id) Logger.info(`[CoverManager] Downloaded libraryItem cover "${coverFullPath}" from url "${url}" for "${libraryItem.media.metadata.title}"`) libraryItem.updateMediaCover(coverFullPath) @@ -223,7 +220,7 @@ class CoverManager { coverPath = newCoverPath } - await this.cacheManager.purgeCoverCache(libraryItem.id) + await CacheManager.purgeCoverCache(libraryItem.id) libraryItem.updateMediaCover(coverPath) return { @@ -264,7 +261,14 @@ class CoverManager { return false } - static async saveEmbeddedCoverArtNew(audioFiles, libraryItemId, libraryItemPath) { + /** + * Extract cover art from audio file and save for library item + * @param {import('../models/Book').AudioFileObject[]} audioFiles + * @param {string} libraryItemId + * @param {string} [libraryItemPath] null for isFile library items + * @returns {Promise} returns cover path + */ + async saveEmbeddedCoverArtNew(audioFiles, libraryItemId, libraryItemPath) { let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt) if (!audioFileWithCover) return null @@ -291,5 +295,57 @@ class CoverManager { } return null } + + /** + * + * @param {string} url + * @param {string} libraryItemId + * @param {string} [libraryItemPath] null if library item isFile or is from adding new podcast + * @returns {Promise<{error:string}|{cover:string}>} + */ + async downloadCoverFromUrlNew(url, libraryItemId, libraryItemPath) { + try { + let coverDirPath = null + if (global.ServerSettings.storeCoverWithItem && libraryItemPath) { + coverDirPath = libraryItemPath + } else { + coverDirPath = Path.posix.join(global.MetadataPath, 'items', libraryItemId) + } + + await fs.ensureDir(coverDirPath) + + const temppath = Path.posix.join(coverDirPath, 'cover') + const success = await downloadFile(url, temppath).then(() => true).catch((err) => { + Logger.error(`[CoverManager] Download image file failed for "${url}"`, err) + return false + }) + if (!success) { + return { + error: 'Failed to download image from url' + } + } + + const imgtype = await this.checkFileIsValidImage(temppath, true) + if (imgtype.error) { + return imgtype + } + + const coverFullPath = Path.posix.join(coverDirPath, `cover.${imgtype.ext}`) + await fs.rename(temppath, coverFullPath) + + await this.removeOldCovers(coverDirPath, '.' + imgtype.ext) + await CacheManager.purgeCoverCache(libraryItemId) + + Logger.info(`[CoverManager] Downloaded libraryItem cover "${coverFullPath}" from url "${url}"`) + return { + cover: coverFullPath + } + } catch (error) { + Logger.error(`[CoverManager] Fetch cover image from url "${url}" failed`, error) + return { + error: 'Failed to fetch image from url' + } + } + } } -module.exports = CoverManager \ No newline at end of file +module.exports = new CoverManager() \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 18eef019..d01a7322 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -9,6 +9,8 @@ const SocketAuthority = require('../SocketAuthority') const fs = require('../libs/fsExtra') const date = require('../libs/dateAndTime') +const CacheManager = require('../managers/CacheManager') + const LibraryController = require('../controllers/LibraryController') const UserController = require('../controllers/UserController') const CollectionController = require('../controllers/CollectionController') @@ -35,13 +37,10 @@ const Series = require('../objects/entities/Series') class ApiRouter { constructor(Server) { this.auth = Server.auth - this.scanner = Server.scanner this.playbackSessionManager = Server.playbackSessionManager this.abMergeManager = Server.abMergeManager this.backupManager = Server.backupManager - this.coverManager = Server.coverManager this.watcher = Server.watcher - this.cacheManager = Server.cacheManager this.podcastManager = Server.podcastManager this.audioMetadataManager = Server.audioMetadataManager this.rssFeedManager = Server.rssFeedManager @@ -414,7 +413,7 @@ class ApiRouter { await this.rssFeedManager.closeFeedForEntityId(libraryItemId) // purge cover cache - await this.cacheManager.purgeCoverCache(libraryItemId) + await CacheManager.purgeCoverCache(libraryItemId) const itemMetadataPath = Path.join(global.MetadataPath, 'items', libraryItemId) if (await fs.pathExists(itemMetadataPath)) { diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index b19e8bf6..190bd69f 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -16,7 +16,8 @@ const CoverManager = require('../managers/CoverManager') const LibraryFile = require('../objects/files/LibraryFile') const SocketAuthority = require('../SocketAuthority') const fsExtra = require("../libs/fsExtra") -// const BookFinder = require('../finders/BookFinder') +const LibraryScan = require("./LibraryScan") +const BookFinder = require('../finders/BookFinder') /** * Metadata for books pulled from files @@ -48,7 +49,7 @@ class BookScanner { * @param {import('../models/LibraryItem')} existingLibraryItem * @param {import('./LibraryItemScanData')} libraryItemData * @param {import('../models/Library').LibrarySettingsObject} librarySettings - * @param {import('./LibraryScan')} libraryScan + * @param {LibraryScan} libraryScan * @returns {Promise} */ async rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) { @@ -291,17 +292,6 @@ class BookScanner { } } - // If no cover then extract cover from audio file if available - if (!media.coverPath && media.audioFiles.length) { - const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path - const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(media.audioFiles, existingLibraryItem.id, libraryItemDir) - if (extractedCoverPath) { - libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) - media.coverPath = extractedCoverPath - hasMediaChanges = true - } - } - // Load authors/series again if updated (for sending back to client) if (authorsUpdated) { media.authors = await media.getAuthors({ @@ -320,6 +310,24 @@ class BookScanner { }) } + // If no cover then extract cover from audio file if available OR search for cover if enabled in server settings + if (!media.coverPath) { + const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path + const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(media.audioFiles, existingLibraryItem.id, libraryItemDir) + if (extractedCoverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) + media.coverPath = extractedCoverPath + hasMediaChanges = true + } else if (Database.serverSettings.scannerFindCovers) { + const authorName = media.authors.map(au => au.name).filter(au => au).join(', ') + const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan) + if (coverPath) { + media.coverPath = coverPath + hasMediaChanges = true + } + } + } + existingLibraryItem.media = media let libraryItemUpdated = false @@ -360,7 +368,7 @@ class BookScanner { * * @param {import('./LibraryItemScanData')} libraryItemData * @param {import('../models/Library').LibrarySettingsObject} librarySettings - * @param {import('./LibraryScan')} libraryScan + * @param {LibraryScan} libraryScan * @returns {Promise} */ async scanNewBookLibraryItem(libraryItemData, librarySettings, libraryScan) { @@ -449,11 +457,17 @@ class BookScanner { } } - // If cover was not found in folder then check embedded covers in audio files - if (!bookObject.coverPath && scannedAudioFiles.length) { + // If cover was not found in folder then check embedded covers in audio files OR search for cover + if (!bookObject.coverPath) { const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path // Extract and save embedded cover art - bookObject.coverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemDir) + const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemDir) + if (extractedCoverPath) { + bookObject.coverPath = extractedCoverPath + } else if (Database.serverSettings.scannerFindCovers) { + const authorName = bookMetadata.authors.join(', ') + bookObject.coverPath = await this.searchForCover(libraryItemObj.id, libraryItemDir, bookObject.title, authorName, libraryScan) + } } libraryItemObj.book = bookObject @@ -533,7 +547,7 @@ class BookScanner { * * @param {import('../models/Book').AudioFileObject[]} audioFiles * @param {import('./LibraryItemScanData')} libraryItemData - * @param {import('./LibraryScan')} libraryScan + * @param {LibraryScan} libraryScan * @returns {Promise} */ async getBookMetadataFromScanData(audioFiles, libraryItemData, libraryScan) { @@ -766,7 +780,7 @@ class BookScanner { /** * @param {string} bookTitle * @param {AudioFile[]} audioFiles - * @param {import('./LibraryScan')} libraryScan + * @param {LibraryScan} libraryScan * @returns {import('../models/Book').ChapterObject[]} */ getChaptersFromAudioFiles(bookTitle, audioFiles, libraryScan) { @@ -847,7 +861,7 @@ class BookScanner { /** * * @param {import('../models/LibraryItem')} libraryItem - * @param {import('./LibraryScan')} libraryScan + * @param {LibraryScan} libraryScan * @returns {Promise} */ async saveMetadataFile(libraryItem, libraryScan) { @@ -1051,31 +1065,38 @@ class BookScanner { } } - // async searchForCover(libraryItem, libraryScan = null) { - // const options = { - // titleDistance: 2, - // authorDistance: 2 - // } - // const scannerCoverProvider = Database.serverSettings.scannerCoverProvider - // const results = await BookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options) - // if (results.length) { - // if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`) - // else Logger.debug(`[Scanner] Found best cover for "${libraryItem.media.metadata.title}"`) + /** + * Search cover provider for matching cover + * @param {string} libraryItemId + * @param {string} libraryItemPath null if book isFile + * @param {string} title + * @param {string} author + * @param {LibraryScan} libraryScan + * @returns {Promise} path to downloaded cover or null if no cover found + */ + async searchForCover(libraryItemId, libraryItemPath, title, author, libraryScan) { + const options = { + titleDistance: 2, + authorDistance: 2 + } + const results = await BookFinder.findCovers(Database.serverSettings.scannerCoverProvider, title, author, options) + if (results.length) { + libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${title}"`) - // // If the first cover result fails, attempt to download the second - // for (let i = 0; i < results.length && i < 2; i++) { + // If the first cover result fails, attempt to download the second + for (let i = 0; i < results.length && i < 2; i++) { - // // Downloads and updates the book cover - // const result = await this.coverManager.downloadCoverFromUrl(libraryItem, results[i]) + // Downloads and updates the book cover + const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath) - // if (result.error) { - // Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error) - // } else { - // return true - // } - // } - // } - // return false - // } + if (result.error) { + Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error) + } else if (result.cover) { + return result.cover + } + } + } + return null + } } module.exports = new BookScanner() \ No newline at end of file diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index a83aaf61..cae2c70e 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -170,6 +170,11 @@ class LibraryItemScanData { existingLibraryItem.ctime = this.ctimeMs this.hasChanges = true } + if (existingLibraryItem.isMissing) { + libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.relPath}" was missing but now found`) + existingLibraryItem.isMissing = false + this.hasChanges = true + } this.libraryFilesRemoved = [] this.libraryFilesModified = [] diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index e53560f5..ea89905a 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -490,7 +490,6 @@ class LibraryScanner { // Scan library item for updates Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) - // itemGroupingResults[itemDir] = await this.scanLibraryItem(library, folder, existingLibraryItem) itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id) continue } else if (library.settings.audiobooksOnly && !fileUpdateGroup[itemDir].some?.(scanUtils.checkFilepathIsAudioFile)) { diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 0ecad33e..fc130f48 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -13,9 +13,7 @@ const Series = require('../objects/entities/Series') const LibraryScanner = require('./LibraryScanner') class Scanner { - constructor(coverManager) { - this.coverManager = coverManager - } + constructor() { } async quickMatchLibraryItem(libraryItem, options = {}) { var provider = options.provider || 'google' @@ -48,7 +46,7 @@ class Scanner { // Update cover if not set OR overrideCover flag if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`) - var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover) + var coverResult = await CoverManager.downloadCoverFromUrl(libraryItem, matchData.cover) if (!coverResult || coverResult.error || !coverResult.cover) { Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`) } else { @@ -69,7 +67,7 @@ class Scanner { // Update cover if not set OR overrideCover flag if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`) - var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover) + var coverResult = await CoverManager.downloadCoverFromUrl(libraryItem, matchData.cover) if (!coverResult || coverResult.error || !coverResult.cover) { Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`) } else { @@ -358,4 +356,4 @@ class Scanner { SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) } } -module.exports = Scanner +module.exports = new Scanner()