mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add cover finder to new book scanner
This commit is contained in:
		
							parent
							
								
									934c0b9093
								
							
						
					
					
						commit
						efae529fac
					
				| @ -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() | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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()) | ||||
|  | ||||
| @ -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 | ||||
| module.exports = new CacheManager() | ||||
| @ -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<string>} 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 | ||||
| module.exports = new CoverManager() | ||||
| @ -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)) { | ||||
|  | ||||
| @ -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<import('../models/LibraryItem')>} | ||||
|    */ | ||||
|   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<import('../models/LibraryItem')>} | ||||
|    */ | ||||
|   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<BookMetadataObject>} | ||||
|    */ | ||||
|   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<string>} 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() | ||||
| @ -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 = [] | ||||
|  | ||||
| @ -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)) { | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user