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