mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge branch 'master' into plugin-implementation-demo
This commit is contained in:
		
						commit
						a762e6ca03
					
				@ -54,8 +54,7 @@ export default {
 | 
				
			|||||||
      options: {
 | 
					      options: {
 | 
				
			||||||
        provider: undefined,
 | 
					        provider: undefined,
 | 
				
			||||||
        overrideDetails: true,
 | 
					        overrideDetails: true,
 | 
				
			||||||
        overrideCover: true,
 | 
					        overrideCover: true
 | 
				
			||||||
        overrideDefaults: true
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -99,8 +98,8 @@ export default {
 | 
				
			|||||||
    init() {
 | 
					    init() {
 | 
				
			||||||
      // If we don't have a set provider (first open of dialog) or we've switched library, set
 | 
					      // If we don't have a set provider (first open of dialog) or we've switched library, set
 | 
				
			||||||
      // the selected provider to the current library default provider
 | 
					      // the selected provider to the current library default provider
 | 
				
			||||||
      if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) {
 | 
					      if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
 | 
				
			||||||
        this.options.lastUsedLibrary = this.currentLibraryId
 | 
					        this.lastUsedLibrary = this.currentLibraryId
 | 
				
			||||||
        this.options.provider = this.libraryProvider
 | 
					        this.options.provider = this.libraryProvider
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div>
 | 
					  <div>
 | 
				
			||||||
    <button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
 | 
					    <button :aria-labelledby="labeledBy" :aria-label="label" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
 | 
				
			||||||
      <span class="rounded-full border border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
 | 
					      <span class="rounded-full border border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@ -20,6 +20,7 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    disabled: Boolean,
 | 
					    disabled: Boolean,
 | 
				
			||||||
    labeledBy: String,
 | 
					    labeledBy: String,
 | 
				
			||||||
 | 
					    label: String,
 | 
				
			||||||
    size: {
 | 
					    size: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      default: 'md'
 | 
					      default: 'md'
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,9 @@
 | 
				
			|||||||
          <div class="pt-4">
 | 
					          <div class="pt-4">
 | 
				
			||||||
            <h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
 | 
					            <h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="flex items-end py-2">
 | 
					          <div role="article" :aria-label="$strings.LabelSettingsStoreCoversWithItemHelp" class="flex items-end py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
 | 
					            <ui-toggle-switch :label="$strings.LabelSettingsStoreCoversWithItem" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
 | 
				
			||||||
            <ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
 | 
					            <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreCoversWithItemHelp">
 | 
				
			||||||
              <p class="pl-4">
 | 
					              <p class="pl-4">
 | 
				
			||||||
                <span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
 | 
					                <span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
 | 
				
			||||||
                <span class="material-symbols icon-text">info</span>
 | 
					                <span class="material-symbols icon-text">info</span>
 | 
				
			||||||
@ -16,9 +16,9 @@
 | 
				
			|||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2">
 | 
					          <div role="article" :aria-label="$strings.LabelSettingsStoreMetadataWithItemHelp" class="flex items-center py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-store-metadata-with-items" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
 | 
					            <ui-toggle-switch :label="$strings.LabelSettingsStoreMetadataWithItem" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
 | 
				
			||||||
            <ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
 | 
					            <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
 | 
				
			||||||
              <p class="pl-4">
 | 
					              <p class="pl-4">
 | 
				
			||||||
                <span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
 | 
					                <span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
 | 
				
			||||||
                <span class="material-symbols icon-text">info</span>
 | 
					                <span class="material-symbols icon-text">info</span>
 | 
				
			||||||
@ -26,9 +26,9 @@
 | 
				
			|||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2">
 | 
					          <div role="article" :aria-label="$strings.LabelSettingsSortingIgnorePrefixesHelp" class="flex items-center py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-sorting-ignore-prefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
 | 
					            <ui-toggle-switch :label="$strings.LabelSettingsSortingIgnorePrefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
 | 
				
			||||||
            <ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
 | 
					            <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
 | 
				
			||||||
              <p class="pl-4">
 | 
					              <p class="pl-4">
 | 
				
			||||||
                <span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
 | 
					                <span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
 | 
				
			||||||
                <span class="material-symbols icon-text">info</span>
 | 
					                <span class="material-symbols icon-text">info</span>
 | 
				
			||||||
@ -46,9 +46,9 @@
 | 
				
			|||||||
            <h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
 | 
					            <h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2">
 | 
					          <div role="article" :aria-label="$strings.LabelSettingsParseSubtitlesHelp" class="flex items-center py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
 | 
					            <ui-toggle-switch :label="$strings.LabelSettingsParseSubtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
 | 
				
			||||||
            <ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
 | 
					            <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsParseSubtitlesHelp">
 | 
				
			||||||
              <p class="pl-4">
 | 
					              <p class="pl-4">
 | 
				
			||||||
                <span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
 | 
					                <span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
 | 
				
			||||||
                <span class="material-symbols icon-text">info</span>
 | 
					                <span class="material-symbols icon-text">info</span>
 | 
				
			||||||
@ -56,9 +56,9 @@
 | 
				
			|||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2">
 | 
					          <div role="article" :aria-label="$strings.LabelSettingsFindCoversHelp" class="flex items-center py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
 | 
					            <ui-toggle-switch :label="$strings.LabelSettingsFindCovers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
 | 
				
			||||||
            <ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
 | 
					            <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsFindCoversHelp">
 | 
				
			||||||
              <p class="pl-4">
 | 
					              <p class="pl-4">
 | 
				
			||||||
                <span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
 | 
					                <span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
 | 
				
			||||||
                <span class="material-symbols icon-text">info</span>
 | 
					                <span class="material-symbols icon-text">info</span>
 | 
				
			||||||
@ -70,9 +70,9 @@
 | 
				
			|||||||
            <ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
 | 
					            <ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2">
 | 
					          <div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
 | 
					            <ui-toggle-switch :label="$strings.LabelSettingsPreferMatchedMetadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
 | 
				
			||||||
            <ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
 | 
					            <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
 | 
				
			||||||
              <p class="pl-4">
 | 
					              <p class="pl-4">
 | 
				
			||||||
                <span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
 | 
					                <span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
 | 
				
			||||||
                <span class="material-symbols icon-text">info</span>
 | 
					                <span class="material-symbols icon-text">info</span>
 | 
				
			||||||
@ -80,9 +80,9 @@
 | 
				
			|||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2">
 | 
					          <div role="article" :aria-label="$strings.LabelSettingsEnableWatcherHelp" class="flex items-center py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
 | 
					            <ui-toggle-switch :label="$strings.LabelSettingsEnableWatcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
 | 
				
			||||||
            <ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
 | 
					            <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableWatcherHelp">
 | 
				
			||||||
              <p class="pl-4">
 | 
					              <p class="pl-4">
 | 
				
			||||||
                <span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
 | 
					                <span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
 | 
				
			||||||
                <span class="material-symbols icon-text">info</span>
 | 
					                <span class="material-symbols icon-text">info</span>
 | 
				
			||||||
@ -95,13 +95,13 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2">
 | 
					          <div class="flex items-center py-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
 | 
					            <ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :label="$strings.LabelSettingsChromecastSupport" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
 | 
				
			||||||
            <p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
 | 
					            <p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsChromecastSupport }}</p>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center py-2 mb-2">
 | 
					          <div class="flex items-center py-2 mb-2">
 | 
				
			||||||
            <ui-toggle-switch labeledBy="settings-allow-iframe" v-model="newServerSettings.allowIframe" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('allowIframe', val)" />
 | 
					            <ui-toggle-switch v-model="newServerSettings.allowIframe" :label="$strings.LabelSettingsAllowIframe" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('allowIframe', val)" />
 | 
				
			||||||
            <p class="pl-4" id="settings-allow-iframe">{{ $strings.LabelSettingsAllowIframe }}</p>
 | 
					            <p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsAllowIframe }}</p>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -190,7 +190,9 @@ class FolderWatcher extends EventEmitter {
 | 
				
			|||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    Logger.debug('[Watcher] File Added', path)
 | 
					    Logger.debug('[Watcher] File Added', path)
 | 
				
			||||||
    this.addFileUpdate(libraryId, path, 'added')
 | 
					    if (!this.addFileUpdate(libraryId, path, 'added')) {
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.filesBeingAdded.has(path)) {
 | 
					    if (!this.filesBeingAdded.has(path)) {
 | 
				
			||||||
      this.filesBeingAdded.add(path)
 | 
					      this.filesBeingAdded.add(path)
 | 
				
			||||||
@ -261,22 +263,23 @@ class FolderWatcher extends EventEmitter {
 | 
				
			|||||||
   * @param {string} libraryId
 | 
					   * @param {string} libraryId
 | 
				
			||||||
   * @param {string} path
 | 
					   * @param {string} path
 | 
				
			||||||
   * @param {string} type
 | 
					   * @param {string} type
 | 
				
			||||||
 | 
					   * @returns {boolean} - If file was added to pending updates
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  addFileUpdate(libraryId, path, type) {
 | 
					  addFileUpdate(libraryId, path, type) {
 | 
				
			||||||
    if (this.pendingFilePaths.includes(path)) return
 | 
					    if (this.pendingFilePaths.includes(path)) return false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Get file library
 | 
					    // Get file library
 | 
				
			||||||
    const libwatcher = this.libraryWatchers.find((lw) => lw.id === libraryId)
 | 
					    const libwatcher = this.libraryWatchers.find((lw) => lw.id === libraryId)
 | 
				
			||||||
    if (!libwatcher) {
 | 
					    if (!libwatcher) {
 | 
				
			||||||
      Logger.error(`[Watcher] Invalid library id from watcher ${libraryId}`)
 | 
					      Logger.error(`[Watcher] Invalid library id from watcher ${libraryId}`)
 | 
				
			||||||
      return
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Get file folder
 | 
					    // Get file folder
 | 
				
			||||||
    const folder = libwatcher.libraryFolders.find((fold) => isSameOrSubPath(fold.path, path))
 | 
					    const folder = libwatcher.libraryFolders.find((fold) => isSameOrSubPath(fold.path, path))
 | 
				
			||||||
    if (!folder) {
 | 
					    if (!folder) {
 | 
				
			||||||
      Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
 | 
					      Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
 | 
				
			||||||
      return
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const folderPath = filePathToPOSIX(folder.path)
 | 
					    const folderPath = filePathToPOSIX(folder.path)
 | 
				
			||||||
@ -285,14 +288,14 @@ class FolderWatcher extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (Path.extname(relPath).toLowerCase() === '.part') {
 | 
					    if (Path.extname(relPath).toLowerCase() === '.part') {
 | 
				
			||||||
      Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`)
 | 
					      Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`)
 | 
				
			||||||
      return
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Ignore files/folders starting with "."
 | 
					    // Ignore files/folders starting with "."
 | 
				
			||||||
    const hasDotPath = relPath.split('/').find((p) => p.startsWith('.'))
 | 
					    const hasDotPath = relPath.split('/').find((p) => p.startsWith('.'))
 | 
				
			||||||
    if (hasDotPath) {
 | 
					    if (hasDotPath) {
 | 
				
			||||||
      Logger.debug(`[Watcher] Ignoring dot path "${relPath}" | Piece "${hasDotPath}"`)
 | 
					      Logger.debug(`[Watcher] Ignoring dot path "${relPath}" | Piece "${hasDotPath}"`)
 | 
				
			||||||
      return
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
 | 
					    Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
 | 
				
			||||||
@ -318,6 +321,7 @@ class FolderWatcher extends EventEmitter {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.handlePendingFileUpdatesTimeout()
 | 
					    this.handlePendingFileUpdatesTimeout()
 | 
				
			||||||
 | 
					    return true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
 | 
				
			|||||||
@ -1217,7 +1217,7 @@ class LibraryController {
 | 
				
			|||||||
      Logger.error(`[LibraryController] Non-root user "${req.user.username}" attempted to match library items`)
 | 
					      Logger.error(`[LibraryController] Non-root user "${req.user.username}" attempted to match library items`)
 | 
				
			||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    Scanner.matchLibraryItems(req.library)
 | 
					    Scanner.matchLibraryItems(this, req.library)
 | 
				
			||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -456,10 +456,24 @@ class LibraryItemController {
 | 
				
			|||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async match(req, res) {
 | 
					  async match(req, res) {
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					    const libraryItem = req.libraryItem
 | 
				
			||||||
 | 
					    const reqBody = req.body || {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var options = req.body || {}
 | 
					    const options = {}
 | 
				
			||||||
    var matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options)
 | 
					    const matchOptions = ['provider', 'title', 'author', 'isbn', 'asin']
 | 
				
			||||||
 | 
					    for (const key of matchOptions) {
 | 
				
			||||||
 | 
					      if (reqBody[key] && typeof reqBody[key] === 'string') {
 | 
				
			||||||
 | 
					        options[key] = reqBody[key]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (reqBody.overrideCover !== undefined) {
 | 
				
			||||||
 | 
					      options.overrideCover = !!reqBody.overrideCover
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (reqBody.overrideDetails !== undefined) {
 | 
				
			||||||
 | 
					      options.overrideDetails = !!reqBody.overrideDetails
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options)
 | 
				
			||||||
    res.json(matchResult)
 | 
					    res.json(matchResult)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -642,7 +656,6 @@ class LibraryItemController {
 | 
				
			|||||||
    let itemsUpdated = 0
 | 
					    let itemsUpdated = 0
 | 
				
			||||||
    let itemsUnmatched = 0
 | 
					    let itemsUnmatched = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const options = req.body.options || {}
 | 
					 | 
				
			||||||
    if (!req.body.libraryItemIds?.length) {
 | 
					    if (!req.body.libraryItemIds?.length) {
 | 
				
			||||||
      return res.sendStatus(400)
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -656,8 +669,20 @@ class LibraryItemController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const reqBodyOptions = req.body.options || {}
 | 
				
			||||||
 | 
					    const options = {}
 | 
				
			||||||
 | 
					    if (reqBodyOptions.provider && typeof reqBodyOptions.provider === 'string') {
 | 
				
			||||||
 | 
					      options.provider = reqBodyOptions.provider
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (reqBodyOptions.overrideCover !== undefined) {
 | 
				
			||||||
 | 
					      options.overrideCover = !!reqBodyOptions.overrideCover
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (reqBodyOptions.overrideDetails !== undefined) {
 | 
				
			||||||
 | 
					      options.overrideDetails = !!reqBodyOptions.overrideDetails
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const libraryItem of libraryItems) {
 | 
					    for (const libraryItem of libraryItems) {
 | 
				
			||||||
      const matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options)
 | 
					      const matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options)
 | 
				
			||||||
      if (matchResult.updated) {
 | 
					      if (matchResult.updated) {
 | 
				
			||||||
        itemsUpdated++
 | 
					        itemsUpdated++
 | 
				
			||||||
      } else if (matchResult.warning) {
 | 
					      } else if (matchResult.warning) {
 | 
				
			||||||
 | 
				
			|||||||
@ -342,7 +342,6 @@ class RssFeedManager {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    if (!feed) {
 | 
					    if (!feed) {
 | 
				
			||||||
      Logger.warn(`[RssFeedManager] closeFeedForEntityId: Feed not found for entity id ${entityId}`)
 | 
					 | 
				
			||||||
      return false
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return this.handleCloseFeed(feed)
 | 
					    return this.handleCloseFeed(feed)
 | 
				
			||||||
 | 
				
			|||||||
@ -13,36 +13,58 @@ const LibraryScanner = require('./LibraryScanner')
 | 
				
			|||||||
const CoverManager = require('../managers/CoverManager')
 | 
					const CoverManager = require('../managers/CoverManager')
 | 
				
			||||||
const TaskManager = require('../managers/TaskManager')
 | 
					const TaskManager = require('../managers/TaskManager')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef QuickMatchOptions
 | 
				
			||||||
 | 
					 * @property {string} [provider]
 | 
				
			||||||
 | 
					 * @property {string} [title]
 | 
				
			||||||
 | 
					 * @property {string} [author]
 | 
				
			||||||
 | 
					 * @property {string} [isbn] - This override is currently unused in Abs clients
 | 
				
			||||||
 | 
					 * @property {string} [asin] - This override is currently unused in Abs clients
 | 
				
			||||||
 | 
					 * @property {boolean} [overrideCover]
 | 
				
			||||||
 | 
					 * @property {boolean} [overrideDetails]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Scanner {
 | 
					class Scanner {
 | 
				
			||||||
  constructor() {}
 | 
					  constructor() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async quickMatchLibraryItem(libraryItem, options = {}) {
 | 
					  /**
 | 
				
			||||||
    var provider = options.provider || 'google'
 | 
					   *
 | 
				
			||||||
    var searchTitle = options.title || libraryItem.media.metadata.title
 | 
					   * @param {import('../routers/ApiRouter')} apiRouterCtx
 | 
				
			||||||
    var searchAuthor = options.author || libraryItem.media.metadata.authorName
 | 
					   * @param {import('../objects/LibraryItem')} libraryItem
 | 
				
			||||||
    var overrideDefaults = options.overrideDefaults || false
 | 
					   * @param {QuickMatchOptions} options
 | 
				
			||||||
 | 
					   * @returns {Promise<{updated: boolean, libraryItem: import('../objects/LibraryItem')}>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async quickMatchLibraryItem(apiRouterCtx, libraryItem, options = {}) {
 | 
				
			||||||
 | 
					    const provider = options.provider || 'google'
 | 
				
			||||||
 | 
					    const searchTitle = options.title || libraryItem.media.metadata.title
 | 
				
			||||||
 | 
					    const searchAuthor = options.author || libraryItem.media.metadata.authorName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
 | 
					    // If overrideCover and overrideDetails is not sent in options than use the server setting to determine if we should override
 | 
				
			||||||
    // the overrideDefaults option is not set or set to false.
 | 
					    if (options.overrideCover === undefined && options.overrideDetails === undefined && Database.serverSettings.scannerPreferMatchedMetadata) {
 | 
				
			||||||
    if (overrideDefaults == false && Database.serverSettings.scannerPreferMatchedMetadata) {
 | 
					 | 
				
			||||||
      options.overrideCover = true
 | 
					      options.overrideCover = true
 | 
				
			||||||
      options.overrideDetails = true
 | 
					      options.overrideDetails = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var updatePayload = {}
 | 
					    let updatePayload = {}
 | 
				
			||||||
    var hasUpdated = false
 | 
					    let hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let existingAuthors = [] // Used for checking if authors or series are now empty
 | 
				
			||||||
 | 
					    let existingSeries = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (libraryItem.isBook) {
 | 
					    if (libraryItem.isBook) {
 | 
				
			||||||
      var searchISBN = options.isbn || libraryItem.media.metadata.isbn
 | 
					      existingAuthors = libraryItem.media.metadata.authors.map((a) => a.id)
 | 
				
			||||||
      var searchASIN = options.asin || libraryItem.media.metadata.asin
 | 
					      existingSeries = libraryItem.media.metadata.series.map((s) => s.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var results = await BookFinder.search(libraryItem, provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 })
 | 
					      const searchISBN = options.isbn || libraryItem.media.metadata.isbn
 | 
				
			||||||
 | 
					      const searchASIN = options.asin || libraryItem.media.metadata.asin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const results = await BookFinder.search(libraryItem, provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 })
 | 
				
			||||||
      if (!results.length) {
 | 
					      if (!results.length) {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          warning: `No ${provider} match found`
 | 
					          warning: `No ${provider} match found`
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var matchData = results[0]
 | 
					      const matchData = results[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Update cover if not set OR overrideCover flag
 | 
					      // Update cover if not set OR overrideCover flag
 | 
				
			||||||
      if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
 | 
					      if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
 | 
				
			||||||
@ -58,13 +80,13 @@ class Scanner {
 | 
				
			|||||||
      updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options)
 | 
					      updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options)
 | 
				
			||||||
    } else if (libraryItem.isPodcast) {
 | 
					    } else if (libraryItem.isPodcast) {
 | 
				
			||||||
      // Podcast quick match
 | 
					      // Podcast quick match
 | 
				
			||||||
      var results = await PodcastFinder.search(searchTitle)
 | 
					      const results = await PodcastFinder.search(searchTitle)
 | 
				
			||||||
      if (!results.length) {
 | 
					      if (!results.length) {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          warning: `No ${provider} match found`
 | 
					          warning: `No ${provider} match found`
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var matchData = results[0]
 | 
					      const matchData = results[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Update cover if not set OR overrideCover flag
 | 
					      // Update cover if not set OR overrideCover flag
 | 
				
			||||||
      if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
 | 
					      if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
 | 
				
			||||||
@ -95,6 +117,19 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await Database.updateLibraryItem(libraryItem)
 | 
					      await Database.updateLibraryItem(libraryItem)
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Check if any authors or series are now empty and should be removed
 | 
				
			||||||
 | 
					      if (libraryItem.isBook) {
 | 
				
			||||||
 | 
					        const authorsRemoved = existingAuthors.filter((aid) => !libraryItem.media.metadata.authors.find((au) => au.id === aid))
 | 
				
			||||||
 | 
					        const seriesRemoved = existingSeries.filter((sid) => !libraryItem.media.metadata.series.find((se) => se.id === sid))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (authorsRemoved.length) {
 | 
				
			||||||
 | 
					          await apiRouterCtx.checkRemoveAuthorsWithNoBooks(authorsRemoved)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (seriesRemoved.length) {
 | 
				
			||||||
 | 
					          await apiRouterCtx.checkRemoveEmptySeries(seriesRemoved)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@ -149,6 +184,13 @@ class Scanner {
 | 
				
			|||||||
    return updatePayload
 | 
					    return updatePayload
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../objects/LibraryItem')} libraryItem
 | 
				
			||||||
 | 
					   * @param {*} matchData
 | 
				
			||||||
 | 
					   * @param {QuickMatchOptions} options
 | 
				
			||||||
 | 
					   * @returns
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) {
 | 
					  async quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) {
 | 
				
			||||||
    // Update media metadata if not set OR overrideDetails flag
 | 
					    // Update media metadata if not set OR overrideDetails flag
 | 
				
			||||||
    const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'abridged', 'asin', 'isbn']
 | 
					    const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'abridged', 'asin', 'isbn']
 | 
				
			||||||
@ -307,12 +349,13 @@ class Scanner {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Quick match library items
 | 
					   * Quick match library items
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../routers/ApiRouter')} apiRouterCtx
 | 
				
			||||||
   * @param {import('../models/Library')} library
 | 
					   * @param {import('../models/Library')} library
 | 
				
			||||||
   * @param {import('../objects/LibraryItem')[]} libraryItems
 | 
					   * @param {import('../objects/LibraryItem')[]} libraryItems
 | 
				
			||||||
   * @param {LibraryScan} libraryScan
 | 
					   * @param {LibraryScan} libraryScan
 | 
				
			||||||
   * @returns {Promise<boolean>} false if scan canceled
 | 
					   * @returns {Promise<boolean>} false if scan canceled
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async matchLibraryItemsChunk(library, libraryItems, libraryScan) {
 | 
					  async matchLibraryItemsChunk(apiRouterCtx, library, libraryItems, libraryScan) {
 | 
				
			||||||
    for (let i = 0; i < libraryItems.length; i++) {
 | 
					    for (let i = 0; i < libraryItems.length; i++) {
 | 
				
			||||||
      const libraryItem = libraryItems[i]
 | 
					      const libraryItem = libraryItems[i]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -327,7 +370,7 @@ class Scanner {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Logger.debug(`[Scanner] matchLibraryItems: Quick matching "${libraryItem.media.metadata.title}" (${i + 1} of ${libraryItems.length})`)
 | 
					      Logger.debug(`[Scanner] matchLibraryItems: Quick matching "${libraryItem.media.metadata.title}" (${i + 1} of ${libraryItems.length})`)
 | 
				
			||||||
      const result = await this.quickMatchLibraryItem(libraryItem, { provider: library.provider })
 | 
					      const result = await this.quickMatchLibraryItem(apiRouterCtx, libraryItem, { provider: library.provider })
 | 
				
			||||||
      if (result.warning) {
 | 
					      if (result.warning) {
 | 
				
			||||||
        Logger.warn(`[Scanner] matchLibraryItems: Match warning ${result.warning} for library item "${libraryItem.media.metadata.title}"`)
 | 
					        Logger.warn(`[Scanner] matchLibraryItems: Match warning ${result.warning} for library item "${libraryItem.media.metadata.title}"`)
 | 
				
			||||||
      } else if (result.updated) {
 | 
					      } else if (result.updated) {
 | 
				
			||||||
@ -346,9 +389,10 @@ class Scanner {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Quick match all library items for library
 | 
					   * Quick match all library items for library
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../routers/ApiRouter')} apiRouterCtx
 | 
				
			||||||
   * @param {import('../models/Library')} library
 | 
					   * @param {import('../models/Library')} library
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async matchLibraryItems(library) {
 | 
					  async matchLibraryItems(apiRouterCtx, library) {
 | 
				
			||||||
    if (library.mediaType === 'podcast') {
 | 
					    if (library.mediaType === 'podcast') {
 | 
				
			||||||
      Logger.error(`[Scanner] matchLibraryItems: Match all not supported for podcasts yet`)
 | 
					      Logger.error(`[Scanner] matchLibraryItems: Match all not supported for podcasts yet`)
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
@ -388,7 +432,7 @@ class Scanner {
 | 
				
			|||||||
      hasMoreChunks = libraryItems.length === limit
 | 
					      hasMoreChunks = libraryItems.length === limit
 | 
				
			||||||
      let oldLibraryItems = libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
 | 
					      let oldLibraryItems = libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan)
 | 
					      const shouldContinue = await this.matchLibraryItemsChunk(apiRouterCtx, library, oldLibraryItems, libraryScan)
 | 
				
			||||||
      if (!shouldContinue) {
 | 
					      if (!shouldContinue) {
 | 
				
			||||||
        isCanceled = true
 | 
					        isCanceled = true
 | 
				
			||||||
        break
 | 
					        break
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user