mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:New scanner and scanner server settings
This commit is contained in:
		
							parent
							
								
									bf11d266dc
								
							
						
					
					
						commit
						a5fc382cad
					
				@ -104,11 +104,23 @@ export default {
 | 
				
			|||||||
      if (payload.serverSettings) {
 | 
					      if (payload.serverSettings) {
 | 
				
			||||||
        this.$store.commit('setServerSettings', payload.serverSettings)
 | 
					        this.$store.commit('setServerSettings', payload.serverSettings)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Start scans currently running
 | 
				
			||||||
      if (payload.librariesScanning) {
 | 
					      if (payload.librariesScanning) {
 | 
				
			||||||
        payload.librariesScanning.forEach((libraryScan) => {
 | 
					        payload.librariesScanning.forEach((libraryScan) => {
 | 
				
			||||||
          this.scanStart(libraryScan)
 | 
					          this.scanStart(libraryScan)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Remove any current scans that are no longer running
 | 
				
			||||||
 | 
					      var currentScans = [...this.$store.state.scanners.libraryScans]
 | 
				
			||||||
 | 
					      currentScans.forEach((ls) => {
 | 
				
			||||||
 | 
					        if (!payload.librariesScanning || !payload.librariesScanning.find((_ls) => _ls.id === ls.id)) {
 | 
				
			||||||
 | 
					          this.$toast.dismiss(ls.toastId)
 | 
				
			||||||
 | 
					          this.$store.commit('scanners/remove', ls)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (payload.backups && payload.backups.length) {
 | 
					      if (payload.backups && payload.backups.length) {
 | 
				
			||||||
        this.$store.commit('setBackups', payload.backups)
 | 
					        this.$store.commit('setBackups', payload.backups)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -152,6 +164,16 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      this.$store.commit('audiobooks/remove', audiobook)
 | 
					      this.$store.commit('audiobooks/remove', audiobook)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    audiobooksAdded(audiobooks) {
 | 
				
			||||||
 | 
					      audiobooks.forEach((ab) => {
 | 
				
			||||||
 | 
					        this.$store.commit('audiobooks/addUpdate', ab)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    audiobooksUpdated(audiobooks) {
 | 
				
			||||||
 | 
					      audiobooks.forEach((ab) => {
 | 
				
			||||||
 | 
					        this.$store.commit('audiobooks/addUpdate', ab)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    libraryAdded(library) {
 | 
					    libraryAdded(library) {
 | 
				
			||||||
      this.$store.commit('libraries/addUpdate', library)
 | 
					      this.$store.commit('libraries/addUpdate', library)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -162,6 +184,8 @@ export default {
 | 
				
			|||||||
      this.$store.commit('libraries/remove', library)
 | 
					      this.$store.commit('libraries/remove', library)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    scanComplete(data) {
 | 
					    scanComplete(data) {
 | 
				
			||||||
 | 
					      console.log('Scan complete received', data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var message = `Scan "${data.name}" complete!`
 | 
					      var message = `Scan "${data.name}" complete!`
 | 
				
			||||||
      if (data.results) {
 | 
					      if (data.results) {
 | 
				
			||||||
        var scanResultMsgs = []
 | 
					        var scanResultMsgs = []
 | 
				
			||||||
@ -337,6 +361,8 @@ export default {
 | 
				
			|||||||
      this.socket.on('audiobook_updated', this.audiobookUpdated)
 | 
					      this.socket.on('audiobook_updated', this.audiobookUpdated)
 | 
				
			||||||
      this.socket.on('audiobook_added', this.audiobookAdded)
 | 
					      this.socket.on('audiobook_added', this.audiobookAdded)
 | 
				
			||||||
      this.socket.on('audiobook_removed', this.audiobookRemoved)
 | 
					      this.socket.on('audiobook_removed', this.audiobookRemoved)
 | 
				
			||||||
 | 
					      this.socket.on('audiobooks_updated', this.audiobooksUpdated)
 | 
				
			||||||
 | 
					      this.socket.on('audiobooks_added', this.audiobooksAdded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Library Listeners
 | 
					      // Library Listeners
 | 
				
			||||||
      this.socket.on('library_updated', this.libraryUpdated)
 | 
					      this.socket.on('library_updated', this.libraryUpdated)
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,20 @@
 | 
				
			|||||||
        </ui-tooltip>
 | 
					        </ui-tooltip>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="flex items-center py-2">
 | 
				
			||||||
 | 
					        <ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="updateScannerPreferAudioMeta" />
 | 
				
			||||||
 | 
					        <ui-tooltip :text="scannerPreferAudioMetaTooltip">
 | 
				
			||||||
 | 
					          <p class="pl-4 text-lg">Scanner prefer audio metadata <span class="material-icons icon-text">info_outlined</span></p>
 | 
				
			||||||
 | 
					        </ui-tooltip>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="flex items-center py-2">
 | 
				
			||||||
 | 
					        <ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="updateScannerPreferOpfMeta" />
 | 
				
			||||||
 | 
					        <ui-tooltip :text="scannerPreferOpfMetaTooltip">
 | 
				
			||||||
 | 
					          <p class="pl-4 text-lg">Scanner prefer OPF metadata <span class="material-icons icon-text">info_outlined</span></p>
 | 
				
			||||||
 | 
					        </ui-tooltip>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="flex items-center py-2">
 | 
					      <div class="flex items-center py-2">
 | 
				
			||||||
        <ui-toggle-switch v-model="storeCoversInAudiobookDir" :disabled="updatingServerSettings" @input="updateCoverStorageDestination" />
 | 
					        <ui-toggle-switch v-model="storeCoversInAudiobookDir" :disabled="updatingServerSettings" @input="updateCoverStorageDestination" />
 | 
				
			||||||
        <ui-tooltip :text="coverDestinationTooltip">
 | 
					        <ui-tooltip :text="coverDestinationTooltip">
 | 
				
			||||||
@ -83,6 +97,12 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    scannerPreferAudioMetaTooltip() {
 | 
				
			||||||
 | 
					      return 'Audio file ID3 meta tags will be used for book details over folder & filenames'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    scannerPreferOpfMetaTooltip() {
 | 
				
			||||||
 | 
					      return 'OPF file metadata will be used for book details over folder & filenames'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    saveMetadataTooltip() {
 | 
					    saveMetadataTooltip() {
 | 
				
			||||||
      return 'This will write a "metadata.nfo" file in all of your audiobook directories.'
 | 
					      return 'This will write a "metadata.nfo" file in all of your audiobook directories.'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -127,6 +147,16 @@ export default {
 | 
				
			|||||||
        scannerParseSubtitle: !!val
 | 
					        scannerParseSubtitle: !!val
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    updateScannerPreferAudioMeta(val) {
 | 
				
			||||||
 | 
					      this.updateServerSettings({
 | 
				
			||||||
 | 
					        scannerPreferAudioMetadata: !!val
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    updateScannerPreferOpfMeta(val) {
 | 
				
			||||||
 | 
					      this.updateServerSettings({
 | 
				
			||||||
 | 
					        scannerPreferOpfMetadata: !!val
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    updateServerSettings(payload) {
 | 
					    updateServerSettings(payload) {
 | 
				
			||||||
      this.updatingServerSettings = true
 | 
					      this.updatingServerSettings = true
 | 
				
			||||||
      this.$store
 | 
					      this.$store
 | 
				
			||||||
@ -144,7 +174,6 @@ export default {
 | 
				
			|||||||
      this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
 | 
					      this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
 | 
					      this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
 | 
				
			||||||
      this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    resetAudiobooks() {
 | 
					    resetAudiobooks() {
 | 
				
			||||||
      if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
 | 
					      if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
 | 
				
			||||||
 | 
				
			|||||||
@ -196,7 +196,7 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Sync other files (all files that are not audio files) - Updates cover path
 | 
					    // Sync other files (all files that are not audio files) - Updates cover path
 | 
				
			||||||
    var hasOtherFileUpdates = false
 | 
					    var hasOtherFileUpdates = false
 | 
				
			||||||
    var otherFilesUpdated = await existingAudiobook.syncOtherFiles(audiobookData.otherFiles, this.MetadataPath, forceAudioFileScan)
 | 
					    var otherFilesUpdated = await existingAudiobook.syncOtherFiles(audiobookData.otherFiles, this.MetadataPath, false, forceAudioFileScan)
 | 
				
			||||||
    if (otherFilesUpdated) {
 | 
					    if (otherFilesUpdated) {
 | 
				
			||||||
      hasOtherFileUpdates = true
 | 
					      hasOtherFileUpdates = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -250,7 +250,7 @@ class Scanner {
 | 
				
			|||||||
    var hasUpdates = hasOtherFileUpdates || hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
 | 
					    var hasUpdates = hasOtherFileUpdates || hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check that audio tracks are in sequential order with no gaps
 | 
					    // Check that audio tracks are in sequential order with no gaps
 | 
				
			||||||
    if (existingAudiobook.checkUpdateMissingParts()) {
 | 
					    if (existingAudiobook.checkUpdateMissingTracks()) {
 | 
				
			||||||
      Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
 | 
					      Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
 | 
				
			||||||
      hasUpdates = true
 | 
					      hasUpdates = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -299,7 +299,7 @@ class Scanner {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Look for desc.txt and reader.txt and update
 | 
					    // Look for desc.txt and reader.txt and update
 | 
				
			||||||
    await audiobook.saveDataFromTextFiles()
 | 
					    await audiobook.saveDataFromTextFiles(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Extract embedded cover art if cover is not already in directory
 | 
					    // Extract embedded cover art if cover is not already in directory
 | 
				
			||||||
    if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
 | 
					    if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
 | 
				
			||||||
@ -314,7 +314,7 @@ class Scanner {
 | 
				
			|||||||
    audiobook.setDetailsFromFileMetadata()
 | 
					    audiobook.setDetailsFromFileMetadata()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check for gaps in track numbers
 | 
					    // Check for gaps in track numbers
 | 
				
			||||||
    audiobook.checkUpdateMissingParts()
 | 
					    audiobook.checkUpdateMissingTracks()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set chapters from audio files
 | 
					    // Set chapters from audio files
 | 
				
			||||||
    audiobook.setChapters()
 | 
					    audiobook.setChapters()
 | 
				
			||||||
@ -671,11 +671,6 @@ class Scanner {
 | 
				
			|||||||
      var folder = library.getFolderById(folderId)
 | 
					      var folder = library.getFolderById(folderId)
 | 
				
			||||||
      if (!folder) {
 | 
					      if (!folder) {
 | 
				
			||||||
        Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`)
 | 
					        Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        Logger.debug(`Looking at folders in library "${library.name}" for folderid ${folderId}`)
 | 
					 | 
				
			||||||
        library.folders.forEach((fold) => {
 | 
					 | 
				
			||||||
          Logger.debug(`Folder "${fold.id}" "${fold.fullPath}"`)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -311,19 +311,18 @@ class Server {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async filesChanged(fileUpdates) {
 | 
					  async filesChanged(fileUpdates) {
 | 
				
			||||||
    Logger.info('[Server]', fileUpdates.length, 'Files Changed')
 | 
					    Logger.info('[Server]', fileUpdates.length, 'Files Changed')
 | 
				
			||||||
    await this.scanner.filesChanged(fileUpdates)
 | 
					    await this.scanner2.scanFilesChanged(fileUpdates)
 | 
				
			||||||
    // Logger.debug('[Server] Files changed result', result)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scan(libraryId, forceAudioFileScan = false) {
 | 
					  async scan(libraryId) {
 | 
				
			||||||
    Logger.info('[Server] Starting Scan')
 | 
					    Logger.info('[Server] Starting Scan')
 | 
				
			||||||
    // await this.scanner2.scan(libraryId)
 | 
					    await this.scanner2.scan(libraryId)
 | 
				
			||||||
    await this.scanner(libraryId, forceAudioFileScan)
 | 
					    // await this.scanner.scan(libraryId)
 | 
				
			||||||
    Logger.info('[Server] Scan complete')
 | 
					    Logger.info('[Server] Scan complete')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scanAudiobook(socket, audiobookId) {
 | 
					  async scanAudiobook(socket, audiobookId) {
 | 
				
			||||||
    var result = await this.scanner.scanAudiobookById(audiobookId)
 | 
					    var result = await this.scanner2.scanAudiobookById(audiobookId)
 | 
				
			||||||
    var scanResultName = ''
 | 
					    var scanResultName = ''
 | 
				
			||||||
    for (const key in ScanResult) {
 | 
					    for (const key in ScanResult) {
 | 
				
			||||||
      if (ScanResult[key] === result) {
 | 
					      if (ScanResult[key] === result) {
 | 
				
			||||||
@ -335,7 +334,7 @@ class Server {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  cancelScan(id) {
 | 
					  cancelScan(id) {
 | 
				
			||||||
    Logger.debug('[Server] Cancel scan', id)
 | 
					    Logger.debug('[Server] Cancel scan', id)
 | 
				
			||||||
    this.scanner.cancelLibraryScan[id] = true
 | 
					    this.scanner2.cancelLibraryScan[id] = true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Generates an NFO metadata file, if no audiobookId is passed then all audiobooks are done
 | 
					  // Generates an NFO metadata file, if no audiobookId is passed then all audiobooks are done
 | 
				
			||||||
@ -624,7 +623,7 @@ class Server {
 | 
				
			|||||||
      configPath: this.ConfigPath,
 | 
					      configPath: this.ConfigPath,
 | 
				
			||||||
      user: client.user.toJSONForBrowser(),
 | 
					      user: client.user.toJSONForBrowser(),
 | 
				
			||||||
      stream: client.stream || null,
 | 
					      stream: client.stream || null,
 | 
				
			||||||
      librariesScanning: this.scanner.librariesScanning,
 | 
					      librariesScanning: this.scanner2.librariesScanning,
 | 
				
			||||||
      backups: (this.backupManager.backups || []).map(b => b.toJSON())
 | 
					      backups: (this.backupManager.backups || []).map(b => b.toJSON())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (user.type === 'root') {
 | 
					    if (user.type === 'root') {
 | 
				
			||||||
 | 
				
			|||||||
@ -24,8 +24,11 @@ class FolderWatcher extends EventEmitter {
 | 
				
			|||||||
      Logger.warn('[Watcher] Already watching library', library.name)
 | 
					      Logger.warn('[Watcher] Already watching library', library.name)
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    Logger.info(`[Watcher] Initializing watcher for "${library.name}"..`)
 | 
					    Logger.info(`[Watcher] Initializing watcher for "${library.name}".`)
 | 
				
			||||||
    var folderPaths = library.folderPaths
 | 
					    var folderPaths = library.folderPaths
 | 
				
			||||||
 | 
					    folderPaths.forEach((fp) => {
 | 
				
			||||||
 | 
					      Logger.debug(`[Watcher] Init watcher for library folder path "${fp}"`)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
    var watcher = new Watcher(folderPaths, {
 | 
					    var watcher = new Watcher(folderPaths, {
 | 
				
			||||||
      ignored: /(^|[\/\\])\../, // ignore dotfiles
 | 
					      ignored: /(^|[\/\\])\../, // ignore dotfiles
 | 
				
			||||||
      renameDetection: true,
 | 
					      renameDetection: true,
 | 
				
			||||||
@ -48,6 +51,8 @@ class FolderWatcher extends EventEmitter {
 | 
				
			|||||||
        Logger.error(`[Watcher] ${error}`)
 | 
					        Logger.error(`[Watcher] ${error}`)
 | 
				
			||||||
      }).on('ready', () => {
 | 
					      }).on('ready', () => {
 | 
				
			||||||
        Logger.info(`[Watcher] "${library.name}" Ready`)
 | 
					        Logger.info(`[Watcher] "${library.name}" Ready`)
 | 
				
			||||||
 | 
					      }).on('close', () => {
 | 
				
			||||||
 | 
					        Logger.debug(`[Watcher] "${library.name}" Closed`)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.libraryWatchers.push({
 | 
					    this.libraryWatchers.push({
 | 
				
			||||||
 | 
				
			|||||||
@ -165,9 +165,9 @@ class AudioFile {
 | 
				
			|||||||
    this.fullPath = fileData.fullPath
 | 
					    this.fullPath = fileData.fullPath
 | 
				
			||||||
    this.addedAt = Date.now()
 | 
					    this.addedAt = Date.now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.trackNumFromMeta = fileData.trackNumFromMeta || null
 | 
					    this.trackNumFromMeta = fileData.trackNumFromMeta
 | 
				
			||||||
    this.trackNumFromFilename = fileData.trackNumFromFilename || null
 | 
					    this.trackNumFromFilename = fileData.trackNumFromFilename
 | 
				
			||||||
    this.cdNumFromFilename = fileData.cdNumFromFilename || null
 | 
					    this.cdNumFromFilename = fileData.cdNumFromFilename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.format = probeData.format
 | 
					    this.format = probeData.format
 | 
				
			||||||
    this.duration = probeData.duration
 | 
					    this.duration = probeData.duration
 | 
				
			||||||
@ -180,15 +180,13 @@ class AudioFile {
 | 
				
			|||||||
    this.channelLayout = probeData.channelLayout
 | 
					    this.channelLayout = probeData.channelLayout
 | 
				
			||||||
    this.chapters = probeData.chapters || []
 | 
					    this.chapters = probeData.chapters || []
 | 
				
			||||||
    this.metadata = probeData.audioFileMetadata
 | 
					    this.metadata = probeData.audioFileMetadata
 | 
				
			||||||
 | 
					    this.embeddedCoverArt = probeData.embeddedCoverArt
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validateTrackIndex(isSingleTrack) {
 | 
					  validateTrackIndex() {
 | 
				
			||||||
    var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta)
 | 
					    var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta)
 | 
				
			||||||
    var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename)
 | 
					    var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isSingleTrack) { // Single audio track audiobook only use metadata tag and default to 1
 | 
					 | 
				
			||||||
      return numFromMeta ? numFromMeta : 1
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (numFromMeta !== null) return numFromMeta
 | 
					    if (numFromMeta !== null) return numFromMeta
 | 
				
			||||||
    if (numFromFilename !== null) return numFromFilename
 | 
					    if (numFromFilename !== null) return numFromFilename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -284,5 +282,33 @@ class AudioFile {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
    return hasUpdates
 | 
					    return hasUpdates
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateFromScan(scannedAudioFile) {
 | 
				
			||||||
 | 
					    var hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var newjson = scannedAudioFile.toJSON()
 | 
				
			||||||
 | 
					    if (this.manuallyVerified) newjson.manuallyVerified = true
 | 
				
			||||||
 | 
					    if (this.exclude) newjson.exclude = true
 | 
				
			||||||
 | 
					    newjson.addedAt = this.addedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const key in newjson) {
 | 
				
			||||||
 | 
					      if (key === 'metadata') {
 | 
				
			||||||
 | 
					        if (!this.metadata || !this.metadata.isEqual(scannedAudioFile.metadata)) {
 | 
				
			||||||
 | 
					          this.metadata = scannedAudioFile.metadata
 | 
				
			||||||
 | 
					          hasUpdated = true
 | 
				
			||||||
 | 
					          // console.log('metadata updated for audio file')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (key === 'chapters') {
 | 
				
			||||||
 | 
					        if (this.syncChapters(newjson.chapters || [])) {
 | 
				
			||||||
 | 
					          hasUpdated = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (this[key] !== newjson[key]) {
 | 
				
			||||||
 | 
					        this[key] = newjson[key]
 | 
				
			||||||
 | 
					        hasUpdated = true
 | 
				
			||||||
 | 
					        // console.log('key', key, 'updated', this[key], newjson[key])
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return hasUpdated
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = AudioFile
 | 
					module.exports = AudioFile
 | 
				
			||||||
@ -101,5 +101,13 @@ class AudioFileMetadata {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return hasUpdates
 | 
					    return hasUpdates
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isEqual(audioFileMetadata) {
 | 
				
			||||||
 | 
					    if (!audioFileMetadata || !audioFileMetadata.toJSON) return false
 | 
				
			||||||
 | 
					    for (const key in audioFileMetadata.toJSON()) {
 | 
				
			||||||
 | 
					      if (audioFileMetadata[key] !== this[key]) return false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = AudioFileMetadata
 | 
					module.exports = AudioFileMetadata
 | 
				
			||||||
@ -353,6 +353,11 @@ class Audiobook {
 | 
				
			|||||||
    this.lastUpdate = Date.now()
 | 
					    this.lastUpdate = Date.now()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setInvalid() {
 | 
				
			||||||
 | 
					    this.isInvalid = true
 | 
				
			||||||
 | 
					    this.lastUpdate = Date.now()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setBook(data) {
 | 
					  setBook(data) {
 | 
				
			||||||
    // Use first image file as cover
 | 
					    // Use first image file as cover
 | 
				
			||||||
    if (this.otherFiles && this.otherFiles.length) {
 | 
					    if (this.otherFiles && this.otherFiles.length) {
 | 
				
			||||||
@ -400,6 +405,11 @@ class Audiobook {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateAudioFile(updatedAudioFile) {
 | 
				
			||||||
 | 
					    var audioFile = this.audioFiles.find(af => af.ino === updatedAudioFile.ino)
 | 
				
			||||||
 | 
					    return audioFile.updateFromScan(updatedAudioFile)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addOtherFile(fileData) {
 | 
					  addOtherFile(fileData) {
 | 
				
			||||||
    var file = new AudiobookFile()
 | 
					    var file = new AudiobookFile()
 | 
				
			||||||
    file.setData(fileData)
 | 
					    file.setData(fileData)
 | 
				
			||||||
@ -437,8 +447,8 @@ class Audiobook {
 | 
				
			|||||||
    return this.book.updateCover(cover, coverFullPath)
 | 
					    return this.book.updateCover(cover, coverFullPath)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  checkHasTrackNum(trackNum) {
 | 
					  checkHasTrackNum(trackNum, excludeIno) {
 | 
				
			||||||
    return this.tracks.find(t => t.index === trackNum)
 | 
					    return this._audioFiles.find(t => t.index === trackNum && t.ino !== excludeIno)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateAudioTracks(orderedFileData) {
 | 
					  updateAudioTracks(orderedFileData) {
 | 
				
			||||||
@ -473,6 +483,7 @@ class Audiobook {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    this.setChapters()
 | 
					    this.setChapters()
 | 
				
			||||||
 | 
					    this.checkUpdateMissingTracks()
 | 
				
			||||||
    this.lastUpdate = Date.now()
 | 
					    this.lastUpdate = Date.now()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -486,7 +497,7 @@ class Audiobook {
 | 
				
			|||||||
    this.audioFiles = this.audioFiles.filter(f => f.ino !== track.ino)
 | 
					    this.audioFiles = this.audioFiles.filter(f => f.ino !== track.ino)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  checkUpdateMissingParts() {
 | 
					  checkUpdateMissingTracks() {
 | 
				
			||||||
    var currMissingParts = (this.missingParts || []).join(',') || ''
 | 
					    var currMissingParts = (this.missingParts || []).join(',') || ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var current_index = 1
 | 
					    var current_index = 1
 | 
				
			||||||
@ -515,13 +526,14 @@ class Audiobook {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // On scan check other files found with other files saved
 | 
					  // On scan check other files found with other files saved
 | 
				
			||||||
  async syncOtherFiles(newOtherFiles, metadataPath, forceRescan = false) {
 | 
					  async syncOtherFiles(newOtherFiles, metadataPath, opfMetadataOverrideDetails, forceRescan = false) {
 | 
				
			||||||
    var hasUpdates = false
 | 
					    var hasUpdates = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var currOtherFileNum = this.otherFiles.length
 | 
					    var currOtherFileNum = this.otherFiles.length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var alreadyHasDescTxt = this.otherFiles.find(of => of.filename === 'desc.txt')
 | 
					    var otherFilenamesAlreadyInBook = this.otherFiles.map(ofile => ofile.filename)
 | 
				
			||||||
    var alreadyHasReaderTxt = this.otherFiles.find(of => of.filename === 'reader.txt')
 | 
					    var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
 | 
				
			||||||
 | 
					    var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var newOtherFilePaths = newOtherFiles.map(f => f.path)
 | 
					    var newOtherFilePaths = newOtherFiles.map(f => f.path)
 | 
				
			||||||
    this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
 | 
					    this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
 | 
				
			||||||
@ -553,21 +565,22 @@ class Audiobook {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If OPF file and was not already there
 | 
				
			||||||
    var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
 | 
					    var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
 | 
				
			||||||
    if (metadataOpf) {
 | 
					    if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
 | 
				
			||||||
      var xmlText = await readTextFile(metadataOpf.fullPath)
 | 
					      var xmlText = await readTextFile(metadataOpf.fullPath)
 | 
				
			||||||
      if (xmlText) {
 | 
					      if (xmlText) {
 | 
				
			||||||
        var opfMetadata = await parseOpfMetadataXML(xmlText)
 | 
					        var opfMetadata = await parseOpfMetadataXML(xmlText)
 | 
				
			||||||
        Logger.debug(`[Audiobook] Sync Other File "${metadataOpf.filename}" parsed:`, opfMetadata)
 | 
					        // Logger.debug(`[Audiobook] Sync Other File "${metadataOpf.filename}" parsed:`, opfMetadata)
 | 
				
			||||||
        if (opfMetadata) {
 | 
					        if (opfMetadata) {
 | 
				
			||||||
          const bookUpdatePayload = {}
 | 
					          const bookUpdatePayload = {}
 | 
				
			||||||
          for (const key in opfMetadata) {
 | 
					          for (const key in opfMetadata) {
 | 
				
			||||||
            // Add genres only if genres are empty
 | 
					            // Add genres only if genres are empty
 | 
				
			||||||
            if (key === 'genres') {
 | 
					            if (key === 'genres') {
 | 
				
			||||||
              if (opfMetadata.genres.length && !this.book._genres.length) {
 | 
					              if (opfMetadata.genres.length && (!this.book._genres.length || opfMetadataOverrideDetails)) {
 | 
				
			||||||
                bookUpdatePayload[key] = opfMetadata.genres
 | 
					                bookUpdatePayload[key] = opfMetadata.genres
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            } else if (opfMetadata[key] && !this.book[key]) {
 | 
					            } else if (opfMetadata[key] && (!this.book[key] || opfMetadataOverrideDetails)) {
 | 
				
			||||||
              bookUpdatePayload[key] = opfMetadata[key]
 | 
					              bookUpdatePayload[key] = opfMetadata[key]
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -789,7 +802,7 @@ class Audiobook {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Look for desc.txt and reader.txt and update details if found
 | 
					  // Look for desc.txt and reader.txt and update details if found
 | 
				
			||||||
  async saveDataFromTextFiles() {
 | 
					  async saveDataFromTextFiles(opfMetadataOverrideDetails) {
 | 
				
			||||||
    var bookUpdatePayload = {}
 | 
					    var bookUpdatePayload = {}
 | 
				
			||||||
    var descriptionText = await this.fetchTextFromTextFile('desc.txt')
 | 
					    var descriptionText = await this.fetchTextFromTextFile('desc.txt')
 | 
				
			||||||
    if (descriptionText) {
 | 
					    if (descriptionText) {
 | 
				
			||||||
@ -807,15 +820,15 @@ class Audiobook {
 | 
				
			|||||||
      var xmlText = await readTextFile(metadataOpf.fullPath)
 | 
					      var xmlText = await readTextFile(metadataOpf.fullPath)
 | 
				
			||||||
      if (xmlText) {
 | 
					      if (xmlText) {
 | 
				
			||||||
        var opfMetadata = await parseOpfMetadataXML(xmlText)
 | 
					        var opfMetadata = await parseOpfMetadataXML(xmlText)
 | 
				
			||||||
        Logger.debug(`[Audiobook] "${this.title}" found "${metadataOpf.filename}" parsed:`, opfMetadata)
 | 
					        // Logger.debug(`[Audiobook] "${this.title}" found "${metadataOpf.filename}" parsed:`, opfMetadata)
 | 
				
			||||||
        if (opfMetadata) {
 | 
					        if (opfMetadata) {
 | 
				
			||||||
          for (const key in opfMetadata) {
 | 
					          for (const key in opfMetadata) {
 | 
				
			||||||
            // Add genres only if genres are empty
 | 
					            // Add genres only if genres are empty
 | 
				
			||||||
            if (key === 'genres') {
 | 
					            if (key === 'genres') {
 | 
				
			||||||
              if (opfMetadata.genres.length && !this.book._genres.length) {
 | 
					              if (opfMetadata.genres.length && (!this.book._genres.length || opfMetadataOverrideDetails)) {
 | 
				
			||||||
                bookUpdatePayload[key] = opfMetadata.genres
 | 
					                bookUpdatePayload[key] = opfMetadata.genres
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            } else if (opfMetadata[key] && !this.book[key] && !bookUpdatePayload[key]) {
 | 
					            } else if (opfMetadata[key] && ((!this.book[key] && !bookUpdatePayload[key]) || opfMetadataOverrideDetails)) {
 | 
				
			||||||
              bookUpdatePayload[key] = opfMetadata[key]
 | 
					              bookUpdatePayload[key] = opfMetadata[key]
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -836,10 +849,10 @@ class Audiobook {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Audio file metadata tags map to book details (will not overwrite)
 | 
					  // Audio file metadata tags map to book details (will not overwrite)
 | 
				
			||||||
  setDetailsFromFileMetadata() {
 | 
					  setDetailsFromFileMetadata(overrideExistingDetails = false) {
 | 
				
			||||||
    if (!this.audioFiles.length) return false
 | 
					    if (!this.audioFiles.length) return false
 | 
				
			||||||
    var audioFile = this.audioFiles[0]
 | 
					    var audioFile = this.audioFiles[0]
 | 
				
			||||||
    return this.book.setDetailsFromFileMetadata(audioFile.metadata)
 | 
					    return this.book.setDetailsFromFileMetadata(audioFile.metadata, overrideExistingDetails)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Returns null if file not found, true if file was updated, false if up to date
 | 
					  // Returns null if file not found, true if file was updated, false if up to date
 | 
				
			||||||
@ -884,9 +897,15 @@ class Audiobook {
 | 
				
			|||||||
    return hasUpdated
 | 
					    return hasUpdated
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  checkScanData(dataFound) {
 | 
					  checkScanData(dataFound, version) {
 | 
				
			||||||
    var hasUpdated = false
 | 
					    var hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.isMissing) {
 | 
				
			||||||
 | 
					      // Audiobook no longer missing
 | 
				
			||||||
 | 
					      this.isMissing = false
 | 
				
			||||||
 | 
					      hasUpdated = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (dataFound.ino !== this.ino) {
 | 
					    if (dataFound.ino !== this.ino) {
 | 
				
			||||||
      this.ino = dataFound.ino
 | 
					      this.ino = dataFound.ino
 | 
				
			||||||
      hasUpdated = true
 | 
					      hasUpdated = true
 | 
				
			||||||
@ -916,7 +935,7 @@ class Audiobook {
 | 
				
			|||||||
      var audioFileFoundCheck = this.checkFileFound(af, true)
 | 
					      var audioFileFoundCheck = this.checkFileFound(af, true)
 | 
				
			||||||
      if (audioFileFoundCheck === null) {
 | 
					      if (audioFileFoundCheck === null) {
 | 
				
			||||||
        newAudioFileData.push(af)
 | 
					        newAudioFileData.push(af)
 | 
				
			||||||
      } else if (audioFileFoundCheck === true) {
 | 
					      } else if (audioFileFoundCheck) {
 | 
				
			||||||
        hasUpdated = true
 | 
					        hasUpdated = true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -925,7 +944,7 @@ class Audiobook {
 | 
				
			|||||||
      var fileFoundCheck = this.checkFileFound(otherFileData, false)
 | 
					      var fileFoundCheck = this.checkFileFound(otherFileData, false)
 | 
				
			||||||
      if (fileFoundCheck === null) {
 | 
					      if (fileFoundCheck === null) {
 | 
				
			||||||
        newOtherFileData.push(otherFileData)
 | 
					        newOtherFileData.push(otherFileData)
 | 
				
			||||||
      } else if (fileFoundCheck === true) {
 | 
					      } else if (fileFoundCheck) {
 | 
				
			||||||
        hasUpdated = true
 | 
					        hasUpdated = true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -933,7 +952,7 @@ class Audiobook {
 | 
				
			|||||||
    const audioFilesRemoved = []
 | 
					    const audioFilesRemoved = []
 | 
				
			||||||
    const otherFilesRemoved = []
 | 
					    const otherFilesRemoved = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // inodes will all be up to date at this point
 | 
					    // Remove audio files not found (inodes will all be up to date at this point)
 | 
				
			||||||
    this.audioFiles = this.audioFiles.filter(af => {
 | 
					    this.audioFiles = this.audioFiles.filter(af => {
 | 
				
			||||||
      if (!dataFound.audioFiles.find(_af => _af.ino === af.ino)) {
 | 
					      if (!dataFound.audioFiles.find(_af => _af.ino === af.ino)) {
 | 
				
			||||||
        audioFilesRemoved.push(af.toJSON())
 | 
					        audioFilesRemoved.push(af.toJSON())
 | 
				
			||||||
@ -946,10 +965,11 @@ class Audiobook {
 | 
				
			|||||||
    if (audioFilesRemoved.length) {
 | 
					    if (audioFilesRemoved.length) {
 | 
				
			||||||
      const audioFilesRemovedInodes = audioFilesRemoved.map(afr => afr.ino)
 | 
					      const audioFilesRemovedInodes = audioFilesRemoved.map(afr => afr.ino)
 | 
				
			||||||
      this.tracks = this.tracks.filter(t => !audioFilesRemovedInodes.includes(t.ino))
 | 
					      this.tracks = this.tracks.filter(t => !audioFilesRemovedInodes.includes(t.ino))
 | 
				
			||||||
      this.checkUpdateMissingParts()
 | 
					      this.checkUpdateMissingTracks()
 | 
				
			||||||
      hasUpdated = true
 | 
					      hasUpdated = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove other files not found
 | 
				
			||||||
    this.otherFiles = this.otherFiles.filter(otherFile => {
 | 
					    this.otherFiles = this.otherFiles.filter(otherFile => {
 | 
				
			||||||
      if (!dataFound.otherFiles.find(_otherFile => _otherFile.ino === otherFile.ino)) {
 | 
					      if (!dataFound.otherFiles.find(_otherFile => _otherFile.ino === otherFile.ino)) {
 | 
				
			||||||
        otherFilesRemoved.push(otherFile.toJSON())
 | 
					        otherFilesRemoved.push(otherFile.toJSON())
 | 
				
			||||||
@ -969,6 +989,15 @@ class Audiobook {
 | 
				
			|||||||
      hasUpdated = true
 | 
					      hasUpdated = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if invalid (has no audio files or ebooks)
 | 
				
			||||||
 | 
					    if (!this.audioFilesToInclude.length && !this.ebooks.length && !newAudioFileData.length && !newOtherFileData.length) {
 | 
				
			||||||
 | 
					      this.isInvalid = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (hasUpdated) {
 | 
				
			||||||
 | 
					      this.setLastScan(version)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      updated: hasUpdated,
 | 
					      updated: hasUpdated,
 | 
				
			||||||
      newAudioFileData,
 | 
					      newAudioFileData,
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ class Book {
 | 
				
			|||||||
  get _genres() { return this.genres || [] }
 | 
					  get _genres() { return this.genres || [] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get shouldSearchForCover() {
 | 
					  get shouldSearchForCover() {
 | 
				
			||||||
 | 
					    if (this.cover) return false
 | 
				
			||||||
    if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
 | 
					    if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
 | 
				
			||||||
    var timeSinceLastSearch = Date.now() - this.lastCoverSearch
 | 
					    var timeSinceLastSearch = Date.now() - this.lastCoverSearch
 | 
				
			||||||
    return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup
 | 
					    return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup
 | 
				
			||||||
@ -297,7 +298,7 @@ class Book {
 | 
				
			|||||||
    return [genreTag]
 | 
					    return [genreTag]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setDetailsFromFileMetadata(audioFileMetadata) {
 | 
					  setDetailsFromFileMetadata(audioFileMetadata, overrideExistingDetails = false) {
 | 
				
			||||||
    const MetadataMapArray = [
 | 
					    const MetadataMapArray = [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        tag: 'tagComposer',
 | 
					        tag: 'tagComposer',
 | 
				
			||||||
@ -319,6 +320,10 @@ class Book {
 | 
				
			|||||||
        tag: 'tagSubtitle',
 | 
					        tag: 'tagSubtitle',
 | 
				
			||||||
        key: 'subtitle'
 | 
					        key: 'subtitle'
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        tag: 'tagAlbum',
 | 
				
			||||||
 | 
					        key: 'title',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        tag: 'tagArtist',
 | 
					        tag: 'tagArtist',
 | 
				
			||||||
        key: 'author'
 | 
					        key: 'author'
 | 
				
			||||||
@ -342,12 +347,12 @@ class Book {
 | 
				
			|||||||
    MetadataMapArray.forEach((mapping) => {
 | 
					    MetadataMapArray.forEach((mapping) => {
 | 
				
			||||||
      if (audioFileMetadata[mapping.tag]) {
 | 
					      if (audioFileMetadata[mapping.tag]) {
 | 
				
			||||||
        // Genres can contain multiple
 | 
					        // Genres can contain multiple
 | 
				
			||||||
        if (mapping.key === 'genres' && (!this[mapping.key].length || !this[mapping.key])) {
 | 
					        if (mapping.key === 'genres' && (!this[mapping.key].length || !this[mapping.key] || overrideExistingDetails)) {
 | 
				
			||||||
          updatePayload[mapping.key] = this.parseGenresTag(audioFileMetadata[mapping.tag])
 | 
					          updatePayload[mapping.key] = this.parseGenresTag(audioFileMetadata[mapping.tag])
 | 
				
			||||||
          Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key].join(',')}`)
 | 
					          // Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key].join(',')}`)
 | 
				
			||||||
        } else if (!this[mapping.key]) {
 | 
					        } else if (!this[mapping.key] || overrideExistingDetails) {
 | 
				
			||||||
          updatePayload[mapping.key] = audioFileMetadata[mapping.tag]
 | 
					          updatePayload[mapping.key] = audioFileMetadata[mapping.tag]
 | 
				
			||||||
          Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key]}`)
 | 
					          // Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key]}`)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
				
			|||||||
@ -46,6 +46,9 @@ class ServerSettings {
 | 
				
			|||||||
    this.newTagExpireDays = settings.newTagExpireDays
 | 
					    this.newTagExpireDays = settings.newTagExpireDays
 | 
				
			||||||
    this.scannerFindCovers = !!settings.scannerFindCovers
 | 
					    this.scannerFindCovers = !!settings.scannerFindCovers
 | 
				
			||||||
    this.scannerParseSubtitle = settings.scannerParseSubtitle
 | 
					    this.scannerParseSubtitle = settings.scannerParseSubtitle
 | 
				
			||||||
 | 
					    this.scannerPreferAudioMetadata = !!settings.scannerPreferAudioMetadata
 | 
				
			||||||
 | 
					    this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.coverDestination = settings.coverDestination || CoverDestination.METADATA
 | 
					    this.coverDestination = settings.coverDestination || CoverDestination.METADATA
 | 
				
			||||||
    this.saveMetadataFile = !!settings.saveMetadataFile
 | 
					    this.saveMetadataFile = !!settings.saveMetadataFile
 | 
				
			||||||
    this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
 | 
					    this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
 | 
				
			||||||
@ -73,6 +76,8 @@ class ServerSettings {
 | 
				
			|||||||
      newTagExpireDays: this.newTagExpireDays,
 | 
					      newTagExpireDays: this.newTagExpireDays,
 | 
				
			||||||
      scannerFindCovers: this.scannerFindCovers,
 | 
					      scannerFindCovers: this.scannerFindCovers,
 | 
				
			||||||
      scannerParseSubtitle: this.scannerParseSubtitle,
 | 
					      scannerParseSubtitle: this.scannerParseSubtitle,
 | 
				
			||||||
 | 
					      scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
 | 
				
			||||||
 | 
					      scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
 | 
				
			||||||
      coverDestination: this.coverDestination,
 | 
					      coverDestination: this.coverDestination,
 | 
				
			||||||
      saveMetadataFile: !!this.saveMetadataFile,
 | 
					      saveMetadataFile: !!this.saveMetadataFile,
 | 
				
			||||||
      rateLimitLoginRequests: this.rateLimitLoginRequests,
 | 
					      rateLimitLoginRequests: this.rateLimitLoginRequests,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ const AudioFile = require('../objects/AudioFile')
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const prober = require('../utils/prober')
 | 
					const prober = require('../utils/prober')
 | 
				
			||||||
const Logger = require('../Logger')
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
const { msToTimestamp } = require('../utils')
 | 
					const { LogLevel } = require('../utils/constants')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AudioFileScanner {
 | 
					class AudioFileScanner {
 | 
				
			||||||
  constructor() { }
 | 
					  constructor() { }
 | 
				
			||||||
@ -80,6 +80,9 @@ class AudioFileScanner {
 | 
				
			|||||||
    audioFileData.trackNumFromFilename = this.getTrackNumberFromFilename(bookScanData, audioFileData.filename)
 | 
					    audioFileData.trackNumFromFilename = this.getTrackNumberFromFilename(bookScanData, audioFileData.filename)
 | 
				
			||||||
    audioFileData.cdNumFromFilename = this.getCdNumberFromFilename(bookScanData, audioFileData.filename)
 | 
					    audioFileData.cdNumFromFilename = this.getCdNumberFromFilename(bookScanData, audioFileData.filename)
 | 
				
			||||||
    audioFile.setDataFromProbe(audioFileData, probeData)
 | 
					    audioFile.setDataFromProbe(audioFileData, probeData)
 | 
				
			||||||
 | 
					    if (audioFile.embeddedCoverArt) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      audioFile,
 | 
					      audioFile,
 | 
				
			||||||
      elapsed: Date.now() - probeStart
 | 
					      elapsed: Date.now() - probeStart
 | 
				
			||||||
@ -87,12 +90,11 @@ class AudioFileScanner {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Returns array of { AudioFile, elapsed } from audio file scan objects
 | 
					  // Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
 | 
				
			||||||
  async scanAudioFiles(audioFileDataArray, bookScanData) {
 | 
					  async executeAudioFileScans(audioFileDataArray, bookScanData) {
 | 
				
			||||||
    var proms = []
 | 
					    var proms = []
 | 
				
			||||||
    for (let i = 0; i < audioFileDataArray.length; i++) {
 | 
					    for (let i = 0; i < audioFileDataArray.length; i++) {
 | 
				
			||||||
      var prom = this.scan(audioFileDataArray[i], bookScanData)
 | 
					      proms.push(this.scan(audioFileDataArray[i], bookScanData))
 | 
				
			||||||
      proms.push(prom)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    var scanStart = Date.now()
 | 
					    var scanStart = Date.now()
 | 
				
			||||||
    var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
 | 
					    var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
 | 
				
			||||||
@ -102,5 +104,62 @@ class AudioFileScanner {
 | 
				
			|||||||
      averageScanDuration: this.getAverageScanDurationMs(results)
 | 
					      averageScanDuration: this.getAverageScanDurationMs(results)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanAudioFiles(audioFileDataArray, bookScanData, audiobook, preferAudioMetadata, libraryScan = null) {
 | 
				
			||||||
 | 
					    var hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var audioScanResult = await this.executeAudioFileScans(audioFileDataArray, bookScanData)
 | 
				
			||||||
 | 
					    if (audioScanResult.audioFiles.length) {
 | 
				
			||||||
 | 
					      if (libraryScan) {
 | 
				
			||||||
 | 
					        libraryScan.addLog(LogLevel.DEBUG, `Book "${bookScanData.path}" Audio file scan took ${audioScanResult.elapsed}ms for ${audioScanResult.audioFiles.length} with average time of ${audioScanResult.averageScanDuration}ms`)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var totalAudioFilesToInclude = audiobook.audioFilesToInclude.filter(af => !audioScanResult.audioFiles.find(_af => _af.ino === af.ino)).length + audioScanResult.audioFiles.length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // validate & add/update audio files to audiobook
 | 
				
			||||||
 | 
					      for (let i = 0; i < audioScanResult.audioFiles.length; i++) {
 | 
				
			||||||
 | 
					        var newAF = audioScanResult.audioFiles[i]
 | 
				
			||||||
 | 
					        var existingAF = audiobook.getAudioFileByIno(newAF.ino)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var trackIndex = null
 | 
				
			||||||
 | 
					        if (totalAudioFilesToInclude === 1) { // Single track audiobooks
 | 
				
			||||||
 | 
					          trackIndex = 1
 | 
				
			||||||
 | 
					        } else if (existingAF && existingAF.manuallyVerified) { // manually verified audio files use existing index
 | 
				
			||||||
 | 
					          trackIndex = existingAF.index
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          trackIndex = newAF.validateTrackIndex()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (trackIndex !== null) {
 | 
				
			||||||
 | 
					          if (audiobook.checkHasTrackNum(trackIndex, newAF.ino)) {
 | 
				
			||||||
 | 
					            newAF.setDuplicateTrackNumber(trackIndex)
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            newAF.index = trackIndex
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (existingAF) {
 | 
				
			||||||
 | 
					          if (audiobook.updateAudioFile(newAF)) {
 | 
				
			||||||
 | 
					            // console.log('update dauido file')
 | 
				
			||||||
 | 
					            hasUpdated = true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          audiobook.addAudioFile(newAF)
 | 
				
			||||||
 | 
					          // console.log('added auido file')
 | 
				
			||||||
 | 
					          hasUpdated = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (hasUpdated) {
 | 
				
			||||||
 | 
					        audiobook.rebuildTracks()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Set book details from audio file ID3 tags, optional prefer
 | 
				
			||||||
 | 
					      if (audiobook.setDetailsFromFileMetadata(preferAudioMetadata)) {
 | 
				
			||||||
 | 
					        hasUpdated = true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return hasUpdated
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = new AudioFileScanner()
 | 
					module.exports = new AudioFileScanner()
 | 
				
			||||||
@ -35,7 +35,6 @@ class AudioProbeData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  setData(data) {
 | 
					  setData(data) {
 | 
				
			||||||
    var audioStream = this.getDefaultAudioStream(data.audio_streams)
 | 
					    var audioStream = this.getDefaultAudioStream(data.audio_streams)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.embeddedCoverArt = data.video_stream ? this.getEmbeddedCoverArt(data.video_stream) : false
 | 
					    this.embeddedCoverArt = data.video_stream ? this.getEmbeddedCoverArt(data.video_stream) : false
 | 
				
			||||||
    this.format = data.format
 | 
					    this.format = data.format
 | 
				
			||||||
    this.duration = data.duration
 | 
					    this.duration = data.duration
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,10 @@
 | 
				
			|||||||
const Folder = require('../objects/Folder')
 | 
					const Path = require('path')
 | 
				
			||||||
const Constants = require('../utils/constants')
 | 
					const fs = require('fs-extra')
 | 
				
			||||||
 | 
					const date = require('date-and-time')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
 | 
					const Folder = require('../objects/Folder')
 | 
				
			||||||
 | 
					const { LogLevel } = require('../utils/constants')
 | 
				
			||||||
const { getId, secondsToTimestamp } = require('../utils/index')
 | 
					const { getId, secondsToTimestamp } = require('../utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibraryScan {
 | 
					class LibraryScan {
 | 
				
			||||||
@ -9,6 +13,7 @@ class LibraryScan {
 | 
				
			|||||||
    this.libraryId = null
 | 
					    this.libraryId = null
 | 
				
			||||||
    this.libraryName = null
 | 
					    this.libraryName = null
 | 
				
			||||||
    this.folders = null
 | 
					    this.folders = null
 | 
				
			||||||
 | 
					    this.verbose = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.scanOptions = null
 | 
					    this.scanOptions = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -16,14 +21,21 @@ class LibraryScan {
 | 
				
			|||||||
    this.finishedAt = null
 | 
					    this.finishedAt = null
 | 
				
			||||||
    this.elapsed = null
 | 
					    this.elapsed = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.status = Constants.ScanStatus.NOTHING
 | 
					 | 
				
			||||||
    this.resultsMissing = 0
 | 
					    this.resultsMissing = 0
 | 
				
			||||||
    this.resultsAdded = 0
 | 
					    this.resultsAdded = 0
 | 
				
			||||||
    this.resultsUpdated = 0
 | 
					    this.resultsUpdated = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.logs = []
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get _scanOptions() { return this.scanOptions || {} }
 | 
					  get _scanOptions() { return this.scanOptions || {} }
 | 
				
			||||||
  get forceRescan() { return !!this._scanOptions.forceRescan }
 | 
					  get forceRescan() { return !!this._scanOptions.forceRescan }
 | 
				
			||||||
 | 
					  get preferAudioMetadata() { return !!this._scanOptions.preferAudioMetadata }
 | 
				
			||||||
 | 
					  get preferOpfMetadata() { return !!this._scanOptions.preferOpfMetadata }
 | 
				
			||||||
 | 
					  get findCovers() { return !!this._scanOptions.findCovers }
 | 
				
			||||||
 | 
					  get timestamp() {
 | 
				
			||||||
 | 
					    return (new Date()).toISOString()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get resultStats() {
 | 
					  get resultStats() {
 | 
				
			||||||
    return `${this.resultsAdded} Added | ${this.resultsUpdated} Updated | ${this.resultsMissing} Missing`
 | 
					    return `${this.resultsAdded} Added | ${this.resultsUpdated} Updated | ${this.resultsMissing} Missing`
 | 
				
			||||||
@ -42,6 +54,28 @@ class LibraryScan {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  get totalResults() {
 | 
				
			||||||
 | 
					    return this.resultsAdded + this.resultsUpdated + this.resultsMissing
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get getLogFilename() {
 | 
				
			||||||
 | 
					    return date.format(new Date(), 'YYYY-MM-DD') + '_' + this.id + '.txt'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSON() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      libraryId: this.libraryId,
 | 
				
			||||||
 | 
					      libraryName: this.libraryName,
 | 
				
			||||||
 | 
					      folders: this.folders.map(f => f.toJSON()),
 | 
				
			||||||
 | 
					      scanOptions: this.scanOptions.toJSON(),
 | 
				
			||||||
 | 
					      startedAt: this.startedAt,
 | 
				
			||||||
 | 
					      finishedAt: this.finishedAt,
 | 
				
			||||||
 | 
					      elapsed: this.elapsed,
 | 
				
			||||||
 | 
					      resultsAdded: this.resultsAdded,
 | 
				
			||||||
 | 
					      resultsUpdated: this.resultsUpdated,
 | 
				
			||||||
 | 
					      resultsMissing: this.resultsMissing
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setData(library, scanOptions) {
 | 
					  setData(library, scanOptions) {
 | 
				
			||||||
    this.id = getId('lscan')
 | 
					    this.id = getId('lscan')
 | 
				
			||||||
@ -58,5 +92,39 @@ class LibraryScan {
 | 
				
			|||||||
    this.finishedAt = Date.now()
 | 
					    this.finishedAt = Date.now()
 | 
				
			||||||
    this.elapsed = this.finishedAt - this.startedAt
 | 
					    this.elapsed = this.finishedAt - this.startedAt
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getLogLevelString(level) {
 | 
				
			||||||
 | 
					    for (const key in LogLevel) {
 | 
				
			||||||
 | 
					      if (LogLevel[key] === level) {
 | 
				
			||||||
 | 
					        return key
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return 'UNKNOWN'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addLog(level, ...args) {
 | 
				
			||||||
 | 
					    const logObj = {
 | 
				
			||||||
 | 
					      timestamp: this.timestamp,
 | 
				
			||||||
 | 
					      message: args.join(' '),
 | 
				
			||||||
 | 
					      levelName: this.getLogLevelString(level),
 | 
				
			||||||
 | 
					      level
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.verbose) {
 | 
				
			||||||
 | 
					      Logger.debug(`[LibraryScan] "${this.libraryName}":`, args)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.logs.push(logObj)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async saveLog(logDir) {
 | 
				
			||||||
 | 
					    await fs.ensureDir(logDir)
 | 
				
			||||||
 | 
					    var outputPath = Path.join(logDir, this.getLogFilename)
 | 
				
			||||||
 | 
					    var logLines = [JSON.stringify(this.toJSON())]
 | 
				
			||||||
 | 
					    this.logs.forEach(l => {
 | 
				
			||||||
 | 
					      logLines.push(JSON.stringify(l))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    await fs.writeFile(outputPath, logLines.join('\n') + '\n')
 | 
				
			||||||
 | 
					    Logger.info(`[LibraryScan] Scan log saved "${outputPath}"`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = LibraryScan
 | 
					module.exports = LibraryScan
 | 
				
			||||||
@ -4,29 +4,6 @@ class ScanOptions {
 | 
				
			|||||||
  constructor(options) {
 | 
					  constructor(options) {
 | 
				
			||||||
    this.forceRescan = false
 | 
					    this.forceRescan = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // this.metadataPrecedence = [
 | 
					 | 
				
			||||||
    //   {
 | 
					 | 
				
			||||||
    //     id: 'directory',
 | 
					 | 
				
			||||||
    //     include: true
 | 
					 | 
				
			||||||
    //   },
 | 
					 | 
				
			||||||
    //   {
 | 
					 | 
				
			||||||
    //     id: 'reader-desc-txt',
 | 
					 | 
				
			||||||
    //     include: true
 | 
					 | 
				
			||||||
    //   },
 | 
					 | 
				
			||||||
    //   {
 | 
					 | 
				
			||||||
    //     id: 'audio-file-metadata',
 | 
					 | 
				
			||||||
    //     include: true
 | 
					 | 
				
			||||||
    //   },
 | 
					 | 
				
			||||||
    //   {
 | 
					 | 
				
			||||||
    //     id: 'metadata-opf',
 | 
					 | 
				
			||||||
    //     include: true
 | 
					 | 
				
			||||||
    //   },
 | 
					 | 
				
			||||||
    //   {
 | 
					 | 
				
			||||||
    //     id: 'external-source',
 | 
					 | 
				
			||||||
    //     include: false
 | 
					 | 
				
			||||||
    //   }
 | 
					 | 
				
			||||||
    // ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Server settings
 | 
					    // Server settings
 | 
				
			||||||
    this.parseSubtitles = false
 | 
					    this.parseSubtitles = false
 | 
				
			||||||
    this.findCovers = false
 | 
					    this.findCovers = false
 | 
				
			||||||
 | 
				
			|||||||
@ -4,10 +4,9 @@ const Path = require('path')
 | 
				
			|||||||
// Utils
 | 
					// Utils
 | 
				
			||||||
const Logger = require('../Logger')
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
const { version } = require('../../package.json')
 | 
					const { version } = require('../../package.json')
 | 
				
			||||||
const audioFileScanner = require('../utils/audioFileScanner')
 | 
					 | 
				
			||||||
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir')
 | 
					const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir')
 | 
				
			||||||
const { comparePaths, getIno, getId, msToTimestamp } = require('../utils/index')
 | 
					const { comparePaths, getIno, getId, msToTimestamp } = require('../utils/index')
 | 
				
			||||||
const { ScanResult, CoverDestination } = require('../utils/constants')
 | 
					const { ScanResult, CoverDestination, LogLevel } = require('../utils/constants')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AudioFileScanner = require('./AudioFileScanner')
 | 
					const AudioFileScanner = require('./AudioFileScanner')
 | 
				
			||||||
const BookFinder = require('../BookFinder')
 | 
					const BookFinder = require('../BookFinder')
 | 
				
			||||||
@ -20,6 +19,8 @@ class Scanner {
 | 
				
			|||||||
    this.AudiobookPath = AUDIOBOOK_PATH
 | 
					    this.AudiobookPath = AUDIOBOOK_PATH
 | 
				
			||||||
    this.MetadataPath = METADATA_PATH
 | 
					    this.MetadataPath = METADATA_PATH
 | 
				
			||||||
    this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books')
 | 
					    this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books')
 | 
				
			||||||
 | 
					    var LogDirPath = Path.join(this.MetadataPath, 'logs')
 | 
				
			||||||
 | 
					    this.ScanLogPath = Path.join(LogDirPath, 'scans')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.db = db
 | 
					    this.db = db
 | 
				
			||||||
    this.coverController = coverController
 | 
					    this.coverController = coverController
 | 
				
			||||||
@ -46,8 +47,82 @@ class Scanner {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLibraryScanning(libraryId) {
 | 
				
			||||||
 | 
					    return this.librariesScanning.find(ls => ls.id === libraryId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanAudiobookById(audiobookId) {
 | 
				
			||||||
 | 
					    var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
 | 
				
			||||||
 | 
					    if (!audiobook) {
 | 
				
			||||||
 | 
					      Logger.error(`[Scanner] Scan audiobook by id not found ${audiobookId}`)
 | 
				
			||||||
 | 
					      return ScanResult.NOTHING
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const library = this.db.libraries.find(lib => lib.id === audiobook.libraryId)
 | 
				
			||||||
 | 
					    if (!library) {
 | 
				
			||||||
 | 
					      Logger.error(`[Scanner] Scan audiobook by id library not found "${audiobook.libraryId}"`)
 | 
				
			||||||
 | 
					      return ScanResult.NOTHING
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const folder = library.folders.find(f => f.id === audiobook.folderId)
 | 
				
			||||||
 | 
					    if (!folder) {
 | 
				
			||||||
 | 
					      Logger.error(`[Scanner] Scan audiobook by id folder not found "${audiobook.folderId}" in library "${library.name}"`)
 | 
				
			||||||
 | 
					      return ScanResult.NOTHING
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Logger.info(`[Scanner] Scanning Audiobook "${audiobook.title}"`)
 | 
				
			||||||
 | 
					    return this.scanAudiobook(folder, audiobook)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanAudiobook(folder, audiobook) {
 | 
				
			||||||
 | 
					    var audiobookData = await getAudiobookFileData(folder, audiobook.fullPath, this.db.serverSettings)
 | 
				
			||||||
 | 
					    if (!audiobookData) {
 | 
				
			||||||
 | 
					      return ScanResult.NOTHING
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var checkRes = audiobook.checkScanData(audiobookData, version)
 | 
				
			||||||
 | 
					    if (checkRes.updated) hasUpdated = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync other files first so that local images are used as cover art
 | 
				
			||||||
 | 
					    // TODO: Cleanup other file sync
 | 
				
			||||||
 | 
					    var allOtherFiles = checkRes.newOtherFileData.concat(audiobook._otherFiles)
 | 
				
			||||||
 | 
					    if (await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, this.db.serverSettings.scannerPreferOpfMetadata)) {
 | 
				
			||||||
 | 
					      hasUpdated = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scan all audio files
 | 
				
			||||||
 | 
					    if (audiobookData.audioFiles.length) {
 | 
				
			||||||
 | 
					      if (await AudioFileScanner.scanAudioFiles(audiobookData.audioFiles, audiobookData, audiobook, this.db.serverSettings.scannerPreferAudioMetadata)) {
 | 
				
			||||||
 | 
					        hasUpdated = true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Extract embedded cover art if cover is not already in directory
 | 
				
			||||||
 | 
					      if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
 | 
				
			||||||
 | 
					        var outputCoverDirs = this.getCoverDirectory(audiobook)
 | 
				
			||||||
 | 
					        var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
 | 
				
			||||||
 | 
					        if (relativeDir) {
 | 
				
			||||||
 | 
					          Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`)
 | 
				
			||||||
 | 
					          hasUpdated = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!audiobook.audioFilesToInclude.length && !audiobook.ebooks.length) { // Audiobook is invalid
 | 
				
			||||||
 | 
					      audiobook.setInvalid()
 | 
				
			||||||
 | 
					      hasUpdated = true
 | 
				
			||||||
 | 
					    } else if (audiobook.isInvalid) {
 | 
				
			||||||
 | 
					      audiobook.isInvalid = false
 | 
				
			||||||
 | 
					      hasUpdated = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (hasUpdated) {
 | 
				
			||||||
 | 
					      this.emitter('audiobook_updated', audiobook.toJSONExpanded())
 | 
				
			||||||
 | 
					      await this.db.updateEntity('audiobook', audiobook)
 | 
				
			||||||
 | 
					      return ScanResult.UPDATED
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return ScanResult.UPTODATE
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scan(libraryId, options = {}) {
 | 
					  async scan(libraryId, options = {}) {
 | 
				
			||||||
    if (this.librariesScanning.includes(libraryId)) {
 | 
					    if (this.isLibraryScanning(libraryId)) {
 | 
				
			||||||
      Logger.error(`[Scanner] Already scanning ${libraryId}`)
 | 
					      Logger.error(`[Scanner] Already scanning ${libraryId}`)
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -66,172 +141,380 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    var libraryScan = new LibraryScan()
 | 
					    var libraryScan = new LibraryScan()
 | 
				
			||||||
    libraryScan.setData(library, scanOptions)
 | 
					    libraryScan.setData(library, scanOptions)
 | 
				
			||||||
    this.librariesScanning.push(libraryScan)
 | 
					    libraryScan.verbose = false
 | 
				
			||||||
 | 
					    this.librariesScanning.push(libraryScan.getScanEmitData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emitter('scan_start', libraryScan.getScanEmitData)
 | 
					    this.emitter('scan_start', libraryScan.getScanEmitData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`)
 | 
					    Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.scanLibrary(libraryScan)
 | 
					    var canceled = await this.scanLibrary(libraryScan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (canceled) {
 | 
				
			||||||
 | 
					      Logger.info(`[Scanner] Library scan canceled for "${libraryScan.libraryName}"`)
 | 
				
			||||||
 | 
					      delete this.cancelLibraryScan[libraryScan.libraryId]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    libraryScan.setComplete()
 | 
					    libraryScan.setComplete()
 | 
				
			||||||
    Logger.info(`[Scanner] Library scan ${libraryScan.id} completed in ${libraryScan.elapsedTimestamp}. ${libraryScan.resultStats}`)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Logger.info(`[Scanner] Library scan ${libraryScan.id} completed in ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}`)
 | 
				
			||||||
    this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
 | 
					    this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (canceled && !libraryScan.totalResults) {
 | 
				
			||||||
 | 
					      var emitData = libraryScan.getScanEmitData
 | 
				
			||||||
 | 
					      emitData.results = null
 | 
				
			||||||
 | 
					      this.emitter('scan_complete', emitData)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emitter('scan_complete', libraryScan.getScanEmitData)
 | 
					    this.emitter('scan_complete', libraryScan.getScanEmitData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (libraryScan.totalResults) {
 | 
				
			||||||
 | 
					      libraryScan.saveLog(this.ScanLogPath)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scanLibrary(libraryScan) {
 | 
					  async scanLibrary(libraryScan) {
 | 
				
			||||||
    var audiobookDataFound = []
 | 
					    var audiobookDataFound = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scan each library
 | 
				
			||||||
    for (let i = 0; i < libraryScan.folders.length; i++) {
 | 
					    for (let i = 0; i < libraryScan.folders.length; i++) {
 | 
				
			||||||
      var folder = libraryScan.folders[i]
 | 
					      var folder = libraryScan.folders[i]
 | 
				
			||||||
      var abDataFoundInFolder = await scanRootDir(folder, this.db.serverSettings)
 | 
					      var abDataFoundInFolder = await scanRootDir(folder, this.db.serverSettings)
 | 
				
			||||||
      Logger.debug(`[Scanner] ${abDataFoundInFolder.length} ab data found in folder "${folder.fullPath}"`)
 | 
					      libraryScan.addLog(LogLevel.INFO, `${abDataFoundInFolder.length} ab data found in folder "${folder.fullPath}"`)
 | 
				
			||||||
      audiobookDataFound = audiobookDataFound.concat(abDataFoundInFolder)
 | 
					      audiobookDataFound = audiobookDataFound.concat(abDataFoundInFolder)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.cancelLibraryScan[libraryScan.libraryId]) return true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Remove audiobooks with no inode
 | 
					    // Remove audiobooks with no inode
 | 
				
			||||||
    audiobookDataFound = audiobookDataFound.filter(abd => abd.ino)
 | 
					    audiobookDataFound = audiobookDataFound.filter(abd => abd.ino)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryScan.libraryId)
 | 
					    var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryScan.libraryId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const NumScansPerChunk = 25
 | 
				
			||||||
 | 
					    const audiobooksToUpdateChunks = []
 | 
				
			||||||
 | 
					    const audiobookDataToRescanChunks = []
 | 
				
			||||||
 | 
					    const newAudiobookDataToScanChunks = []
 | 
				
			||||||
    var audiobooksToUpdate = []
 | 
					    var audiobooksToUpdate = []
 | 
				
			||||||
    var audiobookRescans = []
 | 
					    var audiobookDataToRescan = []
 | 
				
			||||||
    var newAudiobookScans = []
 | 
					    var newAudiobookDataToScan = []
 | 
				
			||||||
 | 
					    var audiobooksToFindCovers = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check for existing & removed audiobooks
 | 
					    // Check for existing & removed audiobooks
 | 
				
			||||||
    for (let i = 0; i < audiobooksInLibrary.length; i++) {
 | 
					    for (let i = 0; i < audiobooksInLibrary.length; i++) {
 | 
				
			||||||
      var audiobook = audiobooksInLibrary[i]
 | 
					      var audiobook = audiobooksInLibrary[i]
 | 
				
			||||||
      var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
 | 
					      var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
 | 
				
			||||||
      if (!dataFound) {
 | 
					      if (!dataFound) {
 | 
				
			||||||
        Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`)
 | 
					        libraryScan.addLog(LogLevel.WARN, `Audiobook "${audiobook.title}" is missing`)
 | 
				
			||||||
 | 
					        libraryScan.resultsMissing++
 | 
				
			||||||
        audiobook.setMissing()
 | 
					        audiobook.setMissing()
 | 
				
			||||||
        audiobooksToUpdate.push(audiobook)
 | 
					        audiobooksToUpdate.push(audiobook)
 | 
				
			||||||
 | 
					        if (audiobooksToUpdate.length === NumScansPerChunk) {
 | 
				
			||||||
 | 
					          audiobooksToUpdateChunks.push(audiobooksToUpdate)
 | 
				
			||||||
 | 
					          audiobooksToUpdate = []
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        var checkRes = audiobook.checkScanData(dataFound)
 | 
					        var checkRes = audiobook.checkScanData(dataFound, version)
 | 
				
			||||||
        if (checkRes.newAudioFileData.length || checkRes.newOtherFileData.length) {
 | 
					        if (checkRes.newAudioFileData.length || checkRes.newOtherFileData.length) { // Audiobook has new files
 | 
				
			||||||
          // existing audiobook has new files
 | 
					 | 
				
			||||||
          checkRes.audiobook = audiobook
 | 
					          checkRes.audiobook = audiobook
 | 
				
			||||||
          checkRes.bookScanData = dataFound
 | 
					          checkRes.bookScanData = dataFound
 | 
				
			||||||
          audiobookRescans.push(this.rescanAudiobook(checkRes, libraryScan))
 | 
					          audiobookDataToRescan.push(checkRes)
 | 
				
			||||||
          libraryScan.resultsMissing++
 | 
					          if (audiobookDataToRescan.length === NumScansPerChunk) {
 | 
				
			||||||
        } else if (checkRes.updated) {
 | 
					            audiobookDataToRescanChunks.push(audiobookDataToRescan)
 | 
				
			||||||
          audiobooksToUpdate.push(audiobook)
 | 
					            audiobookDataToRescan = []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else if (libraryScan.findCovers && audiobook.book.shouldSearchForCover) {
 | 
				
			||||||
          libraryScan.resultsUpdated++
 | 
					          libraryScan.resultsUpdated++
 | 
				
			||||||
 | 
					          audiobooksToFindCovers.push(audiobook)
 | 
				
			||||||
 | 
					          audiobooksToUpdate.push(audiobook)
 | 
				
			||||||
 | 
					          if (audiobooksToUpdate.length === NumScansPerChunk) {
 | 
				
			||||||
 | 
					            audiobooksToUpdateChunks.push(audiobooksToUpdate)
 | 
				
			||||||
 | 
					            audiobooksToUpdate = []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else if (checkRes.updated) { // Updated but no scan required
 | 
				
			||||||
 | 
					          libraryScan.resultsUpdated++
 | 
				
			||||||
 | 
					          audiobooksToUpdate.push(audiobook)
 | 
				
			||||||
 | 
					          if (audiobooksToUpdate.length === NumScansPerChunk) {
 | 
				
			||||||
 | 
					            audiobooksToUpdateChunks.push(audiobooksToUpdate)
 | 
				
			||||||
 | 
					            audiobooksToUpdate = []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        audiobookDataFound = audiobookDataFound.filter(abf => abf.ino !== dataFound.ino)
 | 
					        audiobookDataFound = audiobookDataFound.filter(abf => abf.ino !== dataFound.ino)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (audiobooksToUpdate.length) audiobooksToUpdateChunks.push(audiobooksToUpdate)
 | 
				
			||||||
 | 
					    if (audiobookDataToRescan.length) audiobookDataToRescanChunks.push(audiobookDataToRescan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Potential NEW Audiobooks
 | 
					    // Potential NEW Audiobooks
 | 
				
			||||||
    for (let i = 0; i < audiobookDataFound.length; i++) {
 | 
					    for (let i = 0; i < audiobookDataFound.length; i++) {
 | 
				
			||||||
      var dataFound = audiobookDataFound[i]
 | 
					      var dataFound = audiobookDataFound[i]
 | 
				
			||||||
      var hasEbook = dataFound.otherFiles.find(otherFile => otherFile.filetype === 'ebook')
 | 
					      var hasEbook = dataFound.otherFiles.find(otherFile => otherFile.filetype === 'ebook')
 | 
				
			||||||
      if (!hasEbook && !dataFound.audioFiles.length) {
 | 
					      if (!hasEbook && !dataFound.audioFiles.length) {
 | 
				
			||||||
        Logger.info(`[Scanner] Directory found "${audiobookDataFound.path}" has no ebook or audio files`)
 | 
					        libraryScan.addLog(LogLevel.WARN, `Directory found "${audiobookDataFound.path}" has no ebook or audio files`)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        newAudiobookScans.push(this.scanNewAudiobook(dataFound, libraryScan))
 | 
					        newAudiobookDataToScan.push(dataFound)
 | 
				
			||||||
 | 
					        if (newAudiobookDataToScan.length === NumScansPerChunk) {
 | 
				
			||||||
 | 
					          newAudiobookDataToScanChunks.push(newAudiobookDataToScan)
 | 
				
			||||||
 | 
					          newAudiobookDataToScan = []
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (newAudiobookDataToScan.length) newAudiobookDataToScanChunks.push(newAudiobookDataToScan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // console.log('Num chunks to update', audiobooksToUpdateChunks.length)
 | 
				
			||||||
 | 
					    // console.log('Num chunks to rescan', audiobookDataToRescanChunks.length)
 | 
				
			||||||
 | 
					    // console.log('Num chunks to new scan', newAudiobookDataToScanChunks.length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Audiobooks not requiring a scan but require a search for cover
 | 
				
			||||||
 | 
					    for (let i = 0; i < audiobooksToFindCovers.length; i++) {
 | 
				
			||||||
 | 
					      var audiobook = audiobooksToFindCovers[i]
 | 
				
			||||||
 | 
					      var updatedCover = await this.searchForCover(audiobook, libraryScan)
 | 
				
			||||||
 | 
					      audiobook.book.updateLastCoverSearch(updatedCover)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < audiobooksToUpdateChunks.length; i++) {
 | 
				
			||||||
 | 
					      await this.updateAudiobooksChunk(audiobooksToUpdateChunks[i])
 | 
				
			||||||
 | 
					      if (this.cancelLibraryScan[libraryScan.libraryId]) return true
 | 
				
			||||||
 | 
					      // console.log('Update chunk done', i, 'of', audiobooksToUpdateChunks.length)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (let i = 0; i < audiobookDataToRescanChunks.length; i++) {
 | 
				
			||||||
 | 
					      await this.rescanAudiobookDataChunk(audiobookDataToRescanChunks[i], libraryScan)
 | 
				
			||||||
 | 
					      if (this.cancelLibraryScan[libraryScan.libraryId]) return true
 | 
				
			||||||
 | 
					      // console.log('Rescan chunk done', i, 'of', audiobookDataToRescanChunks.length)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (let i = 0; i < newAudiobookDataToScanChunks.length; i++) {
 | 
				
			||||||
 | 
					      await this.scanNewAudiobookDataChunk(newAudiobookDataToScanChunks[i], libraryScan)
 | 
				
			||||||
 | 
					      // console.log('New scan chunk done', i, 'of', newAudiobookDataToScanChunks.length)
 | 
				
			||||||
 | 
					      if (this.cancelLibraryScan[libraryScan.libraryId]) return true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (audiobookRescans.length) {
 | 
					  async updateAudiobooksChunk(audiobooksToUpdate) {
 | 
				
			||||||
      var updatedAudiobooks = (await Promise.all(audiobookRescans)).filter(ab => !!ab)
 | 
					 | 
				
			||||||
      if (updatedAudiobooks.length) {
 | 
					 | 
				
			||||||
        audiobooksToUpdate = audiobooksToUpdate.concat(updatedAudiobooks)
 | 
					 | 
				
			||||||
        libraryScan.resultsUpdated += updatedAudiobooks.length
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (audiobooksToUpdate.length) {
 | 
					 | 
				
			||||||
      Logger.debug(`[Scanner] Library "${libraryScan.libraryName}" updating ${audiobooksToUpdate.length} books`)
 | 
					 | 
				
			||||||
    await this.db.updateEntities('audiobook', audiobooksToUpdate)
 | 
					    await this.db.updateEntities('audiobook', audiobooksToUpdate)
 | 
				
			||||||
 | 
					    this.emitter('audiobooks_updated', audiobooksToUpdate.map(ab => ab.toJSONExpanded()))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newAudiobookScans.length) {
 | 
					  async rescanAudiobookDataChunk(audiobookDataToRescan, libraryScan) {
 | 
				
			||||||
      var newAudiobooks = (await Promise.all(newAudiobookScans)).filter(ab => !!ab)
 | 
					    var audiobooksUpdated = await Promise.all(audiobookDataToRescan.map((abd) => {
 | 
				
			||||||
      if (newAudiobooks.length) {
 | 
					      return this.rescanAudiobook(abd, libraryScan)
 | 
				
			||||||
        Logger.debug(`[Scanner] Library "${libraryScan.libraryName}" inserting ${newAudiobooks.length} books`)
 | 
					    }))
 | 
				
			||||||
 | 
					    audiobooksUpdated = audiobooksUpdated.filter(ab => ab) // Filter out nulls
 | 
				
			||||||
 | 
					    libraryScan.resultsUpdated += audiobooksUpdated.length
 | 
				
			||||||
 | 
					    await this.db.updateEntities('audiobook', audiobooksUpdated)
 | 
				
			||||||
 | 
					    this.emitter('audiobooks_updated', audiobooksUpdated.map(ab => ab.toJSONExpanded()))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanNewAudiobookDataChunk(newAudiobookDataToScan, libraryScan) {
 | 
				
			||||||
 | 
					    var newAudiobooks = await Promise.all(newAudiobookDataToScan.map((abd) => {
 | 
				
			||||||
 | 
					      return this.scanNewAudiobook(abd, libraryScan.preferAudioMetadata, libraryScan.preferOpfMetadata, libraryScan.findCovers, libraryScan)
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					    newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
 | 
				
			||||||
 | 
					    libraryScan.resultsAdded += newAudiobooks.length
 | 
				
			||||||
    await this.db.insertEntities('audiobook', newAudiobooks)
 | 
					    await this.db.insertEntities('audiobook', newAudiobooks)
 | 
				
			||||||
        libraryScan.resultsAdded = newAudiobooks.length
 | 
					    this.emitter('audiobooks_added', newAudiobooks.map(ab => ab.toJSONExpanded()))
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async rescanAudiobook(audiobookCheckData, libraryScan) {
 | 
					  async rescanAudiobook(audiobookCheckData, libraryScan) {
 | 
				
			||||||
    const { newAudioFileData, newOtherFileData, audiobook, bookScanData } = audiobookCheckData
 | 
					    const { newAudioFileData, newOtherFileData, audiobook, bookScanData } = audiobookCheckData
 | 
				
			||||||
    Logger.debug(`[Scanner] Library "${libraryScan.libraryName}" Re-scanning "${audiobook.path}"`)
 | 
					    libraryScan.addLog(LogLevel.DEBUG, `Library "${libraryScan.libraryName}" Re-scanning "${audiobook.path}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync other files first to use local images as cover before extracting audio file cover
 | 
				
			||||||
 | 
					    if (newOtherFileData.length) {
 | 
				
			||||||
 | 
					      // TODO: Cleanup other file sync
 | 
				
			||||||
 | 
					      var allOtherFiles = newOtherFileData.concat(audiobook._otherFiles)
 | 
				
			||||||
 | 
					      await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, libraryScan.preferOpfMetadata)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newAudioFileData.length) {
 | 
					    if (newAudioFileData.length) {
 | 
				
			||||||
      var audioScanResult = await AudioFileScanner.scanAudioFiles(newAudioFileData, bookScanData)
 | 
					      await AudioFileScanner.scanAudioFiles(newAudioFileData, bookScanData, audiobook, libraryScan.preferAudioMetadata, libraryScan)
 | 
				
			||||||
      Logger.debug(`[Scanner] Library "${libraryScan.libraryName}" Book "${audiobook.path}" Audio file scan took ${msToTimestamp(audioScanResult.elapsed, true)} for ${audioScanResult.audioFiles.length} with average time of ${msToTimestamp(audioScanResult.averageScanDuration, true)}`)
 | 
					 | 
				
			||||||
      if (audioScanResult.audioFiles.length) {
 | 
					 | 
				
			||||||
        var totalAudioFilesToInclude = audiobook.audioFilesToInclude.length + audioScanResult.audioFiles.length
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // validate & add audio files to audiobook
 | 
					 | 
				
			||||||
        for (let i = 0; i < audioScanResult.audioFiles.length; i++) {
 | 
					 | 
				
			||||||
          var newAF = audioScanResult.audioFiles[i]
 | 
					 | 
				
			||||||
          var trackIndex = newAF.validateTrackIndex(totalAudioFilesToInclude === 1)
 | 
					 | 
				
			||||||
          if (trackIndex !== null) {
 | 
					 | 
				
			||||||
            if (audiobook.checkHasTrackNum(trackIndex)) {
 | 
					 | 
				
			||||||
              newAF.setDuplicateTrackNumber(trackIndex)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              newAF.index = trackIndex
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          audiobook.addAudioFile(newAF)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        audiobook.rebuildTracks()
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (newOtherFileData.length) {
 | 
					 | 
				
			||||||
      await audiobook.syncOtherFiles(newOtherFileData, this.MetadataPath)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return audiobook
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async scanNewAudiobook(audiobookData, libraryScan) {
 | 
					 | 
				
			||||||
    Logger.debug(`[Scanner] Library "${libraryScan.libraryName}" Scanning new "${audiobookData.path}"`)
 | 
					 | 
				
			||||||
    var audiobook = new Audiobook()
 | 
					 | 
				
			||||||
    audiobook.setData(audiobookData)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (audiobookData.audioFiles.length) {
 | 
					 | 
				
			||||||
      var audioScanResult = await AudioFileScanner.scanAudioFiles(audiobookData.audioFiles, audiobookData)
 | 
					 | 
				
			||||||
      Logger.debug(`[Scanner] Library "${libraryScan.libraryName}" Book "${audiobookData.path}" Audio file scan took ${msToTimestamp(audioScanResult.elapsed, true)} for ${audioScanResult.audioFiles.length} with average time of ${msToTimestamp(audioScanResult.averageScanDuration, true)}`)
 | 
					 | 
				
			||||||
      if (audioScanResult.audioFiles.length) {
 | 
					 | 
				
			||||||
        // validate & add audio files to audiobook
 | 
					 | 
				
			||||||
        for (let i = 0; i < audioScanResult.audioFiles.length; i++) {
 | 
					 | 
				
			||||||
          var newAF = audioScanResult.audioFiles[i]
 | 
					 | 
				
			||||||
          var trackIndex = newAF.validateTrackIndex(audioScanResult.audioFiles.length === 1)
 | 
					 | 
				
			||||||
          if (trackIndex !== null) {
 | 
					 | 
				
			||||||
            if (audiobook.checkHasTrackNum(trackIndex)) {
 | 
					 | 
				
			||||||
              newAF.setDuplicateTrackNumber(trackIndex)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              newAF.index = trackIndex
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          audiobook.addAudioFile(newAF)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        audiobook.rebuildTracks()
 | 
					 | 
				
			||||||
      } else if (!audiobook.ebooks.length) {
 | 
					 | 
				
			||||||
        // Audiobook has no ebooks and no valid audio tracks do not continue
 | 
					 | 
				
			||||||
        Logger.warn(`[Scanner] Audiobook has no ebooks and no valid audio tracks "${audiobook.path}"`)
 | 
					 | 
				
			||||||
        return null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Look for desc.txt and reader.txt and update
 | 
					 | 
				
			||||||
    await audiobook.saveDataFromTextFiles()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Extract embedded cover art if cover is not already in directory
 | 
					      // Extract embedded cover art if cover is not already in directory
 | 
				
			||||||
      if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
 | 
					      if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
 | 
				
			||||||
        var outputCoverDirs = this.getCoverDirectory(audiobook)
 | 
					        var outputCoverDirs = this.getCoverDirectory(audiobook)
 | 
				
			||||||
        var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
 | 
					        var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
 | 
				
			||||||
        if (relativeDir) {
 | 
					        if (relativeDir) {
 | 
				
			||||||
        Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`)
 | 
					          libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${relativeDir}"`)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!audiobook.audioFilesToInclude.length && !audiobook.ebooks.length) { // Audiobook is invalid
 | 
				
			||||||
 | 
					      audiobook.setInvalid()
 | 
				
			||||||
 | 
					    } else if (audiobook.isInvalid) {
 | 
				
			||||||
 | 
					      audiobook.isInvalid = false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scan for cover if enabled and has no cover
 | 
				
			||||||
 | 
					    if (audiobook && libraryScan.findCovers && !audiobook.cover && audiobook.book.shouldSearchForCover) {
 | 
				
			||||||
 | 
					      var updatedCover = await this.searchForCover(audiobook, libraryScan)
 | 
				
			||||||
 | 
					      audiobook.book.updateLastCoverSearch(updatedCover)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return audiobook
 | 
					    return audiobook
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanNewAudiobook(audiobookData, preferAudioMetadata, preferOpfMetadata, findCovers, libraryScan = null) {
 | 
				
			||||||
 | 
					    if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new book "${audiobookData.path}"`)
 | 
				
			||||||
 | 
					    else Logger.debug(`[Scanner] Scanning new book "${audiobookData.path}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var audiobook = new Audiobook()
 | 
				
			||||||
 | 
					    audiobook.setData(audiobookData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (audiobookData.audioFiles.length) {
 | 
				
			||||||
 | 
					      await AudioFileScanner.scanAudioFiles(audiobookData.audioFiles, audiobookData, audiobook, preferAudioMetadata, libraryScan)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!audiobook.audioFilesToInclude.length && !audiobook.ebooks.length) {
 | 
				
			||||||
 | 
					      // Audiobook has no ebooks and no valid audio tracks do not continue
 | 
				
			||||||
 | 
					      Logger.warn(`[Scanner] Audiobook has no ebooks and no valid audio tracks "${audiobook.path}"`)
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Look for desc.txt and reader.txt and update
 | 
				
			||||||
 | 
					    await audiobook.saveDataFromTextFiles(preferOpfMetadata)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Extract embedded cover art if cover is not already in directory
 | 
				
			||||||
 | 
					    if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
 | 
				
			||||||
 | 
					      var outputCoverDirs = this.getCoverDirectory(audiobook)
 | 
				
			||||||
 | 
					      var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
 | 
				
			||||||
 | 
					      if (relativeDir) {
 | 
				
			||||||
 | 
					        if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${relativeDir}"`)
 | 
				
			||||||
 | 
					        else Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scan for cover if enabled and has no cover
 | 
				
			||||||
 | 
					    if (audiobook && findCovers && !audiobook.cover && audiobook.book.shouldSearchForCover) {
 | 
				
			||||||
 | 
					      var updatedCover = await this.searchForCover(audiobook, libraryScan)
 | 
				
			||||||
 | 
					      audiobook.book.updateLastCoverSearch(updatedCover)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return audiobook
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getFileUpdatesGrouped(fileUpdates) {
 | 
				
			||||||
 | 
					    var folderGroups = {}
 | 
				
			||||||
 | 
					    fileUpdates.forEach((file) => {
 | 
				
			||||||
 | 
					      if (folderGroups[file.folderId]) {
 | 
				
			||||||
 | 
					        folderGroups[file.folderId].fileUpdates.push(file)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        folderGroups[file.folderId] = {
 | 
				
			||||||
 | 
					          libraryId: file.libraryId,
 | 
				
			||||||
 | 
					          folderId: file.folderId,
 | 
				
			||||||
 | 
					          fileUpdates: [file]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return folderGroups
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanFilesChanged(fileUpdates) {
 | 
				
			||||||
 | 
					    if (!fileUpdates.length) return
 | 
				
			||||||
 | 
					    // files grouped by folder
 | 
				
			||||||
 | 
					    var folderGroups = this.getFileUpdatesGrouped(fileUpdates)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const folderId in folderGroups) {
 | 
				
			||||||
 | 
					      var libraryId = folderGroups[folderId].libraryId
 | 
				
			||||||
 | 
					      var library = this.db.libraries.find(lib => lib.id === libraryId)
 | 
				
			||||||
 | 
					      if (!library) {
 | 
				
			||||||
 | 
					        Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      var folder = library.getFolderById(folderId)
 | 
				
			||||||
 | 
					      if (!folder) {
 | 
				
			||||||
 | 
					        Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`)
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
 | 
				
			||||||
 | 
					      var fileUpdateBookGroup = groupFilesIntoAudiobookPaths(relFilePaths, true)
 | 
				
			||||||
 | 
					      var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateBookGroup)
 | 
				
			||||||
 | 
					      Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanFolderUpdates(library, folder, fileUpdateBookGroup) {
 | 
				
			||||||
 | 
					    Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var bookGroupingResults = {}
 | 
				
			||||||
 | 
					    for (const bookDir in fileUpdateBookGroup) {
 | 
				
			||||||
 | 
					      var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), bookDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Check if book dir group is already an audiobook or in a subdir of an audiobook
 | 
				
			||||||
 | 
					      var existingAudiobook = this.db.audiobooks.find(ab => fullPath.startsWith(ab.fullPath))
 | 
				
			||||||
 | 
					      if (existingAudiobook) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Is the audiobook exactly - check if was deleted
 | 
				
			||||||
 | 
					        if (existingAudiobook.fullPath === fullPath) {
 | 
				
			||||||
 | 
					          var exists = await fs.pathExists(fullPath)
 | 
				
			||||||
 | 
					          if (!exists) {
 | 
				
			||||||
 | 
					            Logger.info(`[Scanner] Scanning file update group and audiobook was deleted "${existingAudiobook.title}" - marking as missing`)
 | 
				
			||||||
 | 
					            existingAudiobook.setMissing()
 | 
				
			||||||
 | 
					            await this.db.updateAudiobook(existingAudiobook)
 | 
				
			||||||
 | 
					            this.emitter('audiobook_updated', existingAudiobook.toJSONExpanded())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            bookGroupingResults[bookDir] = ScanResult.REMOVED
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Scan audiobook for updates
 | 
				
			||||||
 | 
					        Logger.debug(`[Scanner] Folder update for relative path "${bookDir}" is in audiobook "${existingAudiobook.title}" - scan for updates`)
 | 
				
			||||||
 | 
					        bookGroupingResults[bookDir] = await this.scanAudiobook(folder, existingAudiobook)
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Check if an audiobook is a subdirectory of this dir
 | 
				
			||||||
 | 
					      var childAudiobook = this.db.audiobooks.find(ab => ab.fullPath.startsWith(fullPath))
 | 
				
			||||||
 | 
					      if (childAudiobook) {
 | 
				
			||||||
 | 
					        Logger.warn(`[Scanner] Files were modified in a parent directory of an audiobook "${childAudiobook.title}" - ignoring`)
 | 
				
			||||||
 | 
					        bookGroupingResults[bookDir] = ScanResult.NOTHING
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`)
 | 
				
			||||||
 | 
					      var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
 | 
				
			||||||
 | 
					      if (newAudiobook) {
 | 
				
			||||||
 | 
					        await this.db.insertEntity('audiobook', newAudiobook)
 | 
				
			||||||
 | 
					        this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      bookGroupingResults[bookDir] = newAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return bookGroupingResults
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async scanPotentialNewAudiobook(folder, fullPath) {
 | 
				
			||||||
 | 
					    var audiobookData = await getAudiobookFileData(folder, fullPath, this.db.serverSettings)
 | 
				
			||||||
 | 
					    if (!audiobookData) return null
 | 
				
			||||||
 | 
					    var serverSettings = this.db.serverSettings
 | 
				
			||||||
 | 
					    return this.scanNewAudiobook(audiobookData, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async searchForCover(audiobook, libraryScan = null) {
 | 
				
			||||||
 | 
					    var options = {
 | 
				
			||||||
 | 
					      titleDistance: 2,
 | 
				
			||||||
 | 
					      authorDistance: 2
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var results = await this.bookFinder.findCovers('google', audiobook.title, audiobook.authorFL, options)
 | 
				
			||||||
 | 
					    if (results.length) {
 | 
				
			||||||
 | 
					      if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${audiobook.title}"`)
 | 
				
			||||||
 | 
					      else Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 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
 | 
				
			||||||
 | 
					        var result = await this.coverController.downloadCoverFromUrl(audiobook, results[i])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (result.error) {
 | 
				
			||||||
 | 
					          Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = Scanner
 | 
					module.exports = Scanner
 | 
				
			||||||
@ -6,14 +6,6 @@ module.exports.ScanResult = {
 | 
				
			|||||||
  UPTODATE: 4
 | 
					  UPTODATE: 4
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports.ScanStatus = {
 | 
					 | 
				
			||||||
  NOTHING: 0,
 | 
					 | 
				
			||||||
  ADDED: 1,
 | 
					 | 
				
			||||||
  UPDATED: 2,
 | 
					 | 
				
			||||||
  REMOVED: 3,
 | 
					 | 
				
			||||||
  UPTODATE: 4
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports.CoverDestination = {
 | 
					module.exports.CoverDestination = {
 | 
				
			||||||
  METADATA: 0,
 | 
					  METADATA: 0,
 | 
				
			||||||
  AUDIOBOOK: 1
 | 
					  AUDIOBOOK: 1
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,7 @@ function isBookFile(path) {
 | 
				
			|||||||
// TODO: Function needs to be re-done
 | 
					// TODO: Function needs to be re-done
 | 
				
			||||||
// Input: array of relative file paths
 | 
					// Input: array of relative file paths
 | 
				
			||||||
// Output: map of files grouped into potential audiobook dirs
 | 
					// Output: map of files grouped into potential audiobook dirs
 | 
				
			||||||
function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) {
 | 
					function groupFilesIntoAudiobookPaths(paths) {
 | 
				
			||||||
  // Step 1: Clean path, Remove leading "/", Filter out files in root dir
 | 
					  // Step 1: Clean path, Remove leading "/", Filter out files in root dir
 | 
				
			||||||
  var pathsFiltered = paths.map(path => {
 | 
					  var pathsFiltered = paths.map(path => {
 | 
				
			||||||
    return path.startsWith('/') ? path.slice(1) : path
 | 
					    return path.startsWith('/') ? path.slice(1) : path
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user