mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge branch 'advplyr:master' into bookfinder-improvements
This commit is contained in:
		
						commit
						b6c789dee6
					
				@ -15,24 +15,33 @@
 | 
			
		||||
 | 
			
		||||
      <div class="flex my-2 -mx-2">
 | 
			
		||||
        <div class="w-1/2 px-2">
 | 
			
		||||
          <ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
 | 
			
		||||
          <ui-text-input-with-label v-model.trim="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="w-1/2 px-2">
 | 
			
		||||
          <ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
 | 
			
		||||
          <div v-if="!isPodcast" class="flex items-end">
 | 
			
		||||
            <ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
 | 
			
		||||
            <ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
 | 
			
		||||
              <div
 | 
			
		||||
                class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer"
 | 
			
		||||
                @click="fetchMetadata">
 | 
			
		||||
                <span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </ui-tooltip>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else class="w-full">
 | 
			
		||||
            <p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
 | 
			
		||||
            <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
 | 
			
		||||
            <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-if="!isPodcast" class="flex my-2 -mx-2">
 | 
			
		||||
        <div class="w-1/2 px-2">
 | 
			
		||||
          <ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" />
 | 
			
		||||
          <ui-text-input-with-label v-model.trim="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" inputClass="h-10" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="w-1/2 px-2">
 | 
			
		||||
          <div class="w-full">
 | 
			
		||||
            <p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
 | 
			
		||||
            <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
 | 
			
		||||
            <label class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></label>
 | 
			
		||||
            <ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -48,8 +57,8 @@
 | 
			
		||||
      <p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
 | 
			
		||||
    </widgets-alert>
 | 
			
		||||
 | 
			
		||||
    <div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
 | 
			
		||||
      <ui-loading-indicator :text="$strings.MessageUploading" />
 | 
			
		||||
    <div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
 | 
			
		||||
      <ui-loading-indicator :text="nonInteractionLabel" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@ -61,10 +70,11 @@ export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    item: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      default: () => {}
 | 
			
		||||
      default: () => { }
 | 
			
		||||
    },
 | 
			
		||||
    mediaType: String,
 | 
			
		||||
    processing: Boolean
 | 
			
		||||
    processing: Boolean,
 | 
			
		||||
    provider: String
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
@ -76,7 +86,8 @@ export default {
 | 
			
		||||
      error: '',
 | 
			
		||||
      isUploading: false,
 | 
			
		||||
      uploadFailed: false,
 | 
			
		||||
      uploadSuccess: false
 | 
			
		||||
      uploadSuccess: false,
 | 
			
		||||
      isFetchingMetadata: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
@ -87,12 +98,19 @@ export default {
 | 
			
		||||
      if (!this.itemData.title) return ''
 | 
			
		||||
      if (this.isPodcast) return this.itemData.title
 | 
			
		||||
 | 
			
		||||
      if (this.itemData.series && this.itemData.author) {
 | 
			
		||||
        return Path.join(this.itemData.author, this.itemData.series, this.itemData.title)
 | 
			
		||||
      } else if (this.itemData.author) {
 | 
			
		||||
        return Path.join(this.itemData.author, this.itemData.title)
 | 
			
		||||
      } else {
 | 
			
		||||
        return this.itemData.title
 | 
			
		||||
      const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
 | 
			
		||||
      const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part))
 | 
			
		||||
 | 
			
		||||
      return Path.join(...cleanedOutputPathParts)
 | 
			
		||||
    },
 | 
			
		||||
    isNonInteractable() {
 | 
			
		||||
      return this.isUploading || this.isFetchingMetadata
 | 
			
		||||
    },
 | 
			
		||||
    nonInteractionLabel() {
 | 
			
		||||
      if (this.isUploading) {
 | 
			
		||||
        return this.$strings.MessageUploading
 | 
			
		||||
      } else if (this.isFetchingMetadata) {
 | 
			
		||||
        return this.$strings.LabelFetchingMetadata
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
@ -105,9 +123,42 @@ export default {
 | 
			
		||||
    titleUpdated() {
 | 
			
		||||
      this.error = ''
 | 
			
		||||
    },
 | 
			
		||||
    async fetchMetadata() {
 | 
			
		||||
      if (!this.itemData.title.trim().length) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.isFetchingMetadata = true
 | 
			
		||||
      this.error = ''
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const searchQueryString = new URLSearchParams({
 | 
			
		||||
          title: this.itemData.title,
 | 
			
		||||
          author: this.itemData.author,
 | 
			
		||||
          provider: this.provider
 | 
			
		||||
        })
 | 
			
		||||
        const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`)
 | 
			
		||||
 | 
			
		||||
        if (bestCandidate) {
 | 
			
		||||
          this.itemData = {
 | 
			
		||||
            ...this.itemData,
 | 
			
		||||
            title: bestCandidate.title,
 | 
			
		||||
            author: bestCandidate.author,
 | 
			
		||||
            series: (bestCandidate.series || [])[0]?.series
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          this.error = this.$strings.ErrorUploadFetchMetadataNoResults
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error('Failed', e)
 | 
			
		||||
        this.error = this.$strings.ErrorUploadFetchMetadataAPI
 | 
			
		||||
      } finally {
 | 
			
		||||
        this.isFetchingMetadata = false
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    getData() {
 | 
			
		||||
      if (!this.itemData.title) {
 | 
			
		||||
        this.error = 'Must have a title'
 | 
			
		||||
        this.error = this.$strings.ErrorUploadLacksTitle
 | 
			
		||||
        return null
 | 
			
		||||
      }
 | 
			
		||||
      this.error = ''
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,11 @@ export default {
 | 
			
		||||
    label: String,
 | 
			
		||||
    disabled: Boolean,
 | 
			
		||||
    readonly: Boolean,
 | 
			
		||||
    showEdit: Boolean
 | 
			
		||||
    showEdit: Boolean,
 | 
			
		||||
    menuDisabled: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
@ -77,7 +81,7 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    showMenu() {
 | 
			
		||||
      return this.isFocused
 | 
			
		||||
      return this.isFocused && !this.menuDisabled
 | 
			
		||||
    },
 | 
			
		||||
    wrapperClass() {
 | 
			
		||||
      var classes = []
 | 
			
		||||
 | 
			
		||||
@ -19,8 +19,8 @@
 | 
			
		||||
 | 
			
		||||
        <div class="w-full h-px bg-white/10 my-4" />
 | 
			
		||||
 | 
			
		||||
        <p v-if="!isGuest" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
 | 
			
		||||
        <form v-if="!isGuest" @submit.prevent="submitChangePassword">
 | 
			
		||||
        <p v-if="showChangePasswordForm" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
 | 
			
		||||
        <form v-if="showChangePasswordForm" @submit.prevent="submitChangePassword">
 | 
			
		||||
          <ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" :label="$strings.LabelPassword" class="my-2" />
 | 
			
		||||
          <ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" :label="$strings.LabelNewPassword" class="my-2" />
 | 
			
		||||
          <ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" :label="$strings.LabelConfirmPassword" class="my-2" />
 | 
			
		||||
@ -68,6 +68,13 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    isGuest() {
 | 
			
		||||
      return this.usertype === 'guest'
 | 
			
		||||
    },
 | 
			
		||||
    isPasswordAuthEnabled() {
 | 
			
		||||
      const activeAuthMethods = this.$store.getters['getServerSetting']('authActiveAuthMethods') || []
 | 
			
		||||
      return activeAuthMethods.includes('local')
 | 
			
		||||
    },
 | 
			
		||||
    showChangePasswordForm() {
 | 
			
		||||
      return !this.isGuest && this.isPasswordAuthEnabled
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,9 @@
 | 
			
		||||
 | 
			
		||||
            <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" />
 | 
			
		||||
 | 
			
		||||
            <ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
 | 
			
		||||
            <p class="pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
 | 
			
		||||
 | 
			
		||||
            <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
 | 
			
		||||
 | 
			
		||||
            <div class="flex items-center pt-1 mb-2">
 | 
			
		||||
@ -187,6 +190,25 @@ export default {
 | 
			
		||||
        this.$toast.error('Client Secret required')
 | 
			
		||||
        isValid = false
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function isValidRedirectURI(uri) {
 | 
			
		||||
        // Check for somestring://someother/string
 | 
			
		||||
        const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i')
 | 
			
		||||
        return pattern.test(uri)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const uris = this.newAuthSettings.authOpenIDMobileRedirectURIs
 | 
			
		||||
      if (uris.includes('*') && uris.length > 1) {
 | 
			
		||||
        this.$toast.error('Mobile Redirect URIs: Asterisk (*) must be the only entry if used')
 | 
			
		||||
        isValid = false
 | 
			
		||||
      } else {
 | 
			
		||||
        uris.forEach((uri) => {
 | 
			
		||||
          if (uri !== '*' && !isValidRedirectURI(uri)) {
 | 
			
		||||
            this.$toast.error(`Mobile Redirect URIs: Invalid URI ${uri}`)
 | 
			
		||||
            isValid = false
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      return isValid
 | 
			
		||||
    },
 | 
			
		||||
    async saveSettings() {
 | 
			
		||||
@ -208,7 +230,11 @@ export default {
 | 
			
		||||
        .$patch('/api/auth-settings', this.newAuthSettings)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.$store.commit('setServerSettings', data.serverSettings)
 | 
			
		||||
          this.$toast.success('Server settings updated')
 | 
			
		||||
          if (data.updated) {
 | 
			
		||||
            this.$toast.success('Server settings updated')
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update server settings', error)
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,20 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6">
 | 
			
		||||
        <label class="flex cursor-pointer pt-4">
 | 
			
		||||
          <ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
 | 
			
		||||
          <span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
 | 
			
		||||
        </label>
 | 
			
		||||
        <ui-tooltip :text="$strings.LabelAutoFetchMetadataHelp" class="inline-flex pt-4">
 | 
			
		||||
          <span class="pl-1 material-icons icon-text text-sm cursor-pointer">info_outlined</span>
 | 
			
		||||
        </ui-tooltip>
 | 
			
		||||
 | 
			
		||||
        <div class="flex-grow ml-4">
 | 
			
		||||
          <ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <widgets-alert v-if="error" type="error">
 | 
			
		||||
        <p class="text-lg">{{ error }}</p>
 | 
			
		||||
      </widgets-alert>
 | 
			
		||||
@ -61,9 +75,7 @@
 | 
			
		||||
      </widgets-alert>
 | 
			
		||||
 | 
			
		||||
      <!-- Item Upload cards -->
 | 
			
		||||
      <template v-for="item in items">
 | 
			
		||||
        <cards-item-upload-card :ref="`itemCard-${item.index}`" :key="item.index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
 | 
			
		||||
      </template>
 | 
			
		||||
      <cards-item-upload-card v-for="item in items" :key="item.index" :ref="`itemCard-${item.index}`" :media-type="selectedLibraryMediaType" :item="item" :provider="fetchMetadata.provider" :processing="processing" @remove="removeItem(item)" />
 | 
			
		||||
 | 
			
		||||
      <!-- Upload/Reset btns -->
 | 
			
		||||
      <div v-show="items.length" class="flex justify-end pb-8 pt-4">
 | 
			
		||||
@ -92,13 +104,18 @@ export default {
 | 
			
		||||
      selectedLibraryId: null,
 | 
			
		||||
      selectedFolderId: null,
 | 
			
		||||
      processing: false,
 | 
			
		||||
      uploadFinished: false
 | 
			
		||||
      uploadFinished: false,
 | 
			
		||||
      fetchMetadata: {
 | 
			
		||||
        enabled: false,
 | 
			
		||||
        provider: null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    selectedLibrary(newVal) {
 | 
			
		||||
      if (newVal && !this.selectedFolderId) {
 | 
			
		||||
        this.setDefaultFolder()
 | 
			
		||||
        this.setMetadataProvider()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
@ -133,6 +150,13 @@ export default {
 | 
			
		||||
    selectedLibraryIsPodcast() {
 | 
			
		||||
      return this.selectedLibraryMediaType === 'podcast'
 | 
			
		||||
    },
 | 
			
		||||
    providers() {
 | 
			
		||||
      if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
 | 
			
		||||
      return this.$store.state.scanners.providers
 | 
			
		||||
    },
 | 
			
		||||
    canFetchMetadata() {
 | 
			
		||||
      return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
 | 
			
		||||
    },
 | 
			
		||||
    selectedFolder() {
 | 
			
		||||
      if (!this.selectedLibrary) return null
 | 
			
		||||
      return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
 | 
			
		||||
@ -160,12 +184,16 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this.setDefaultFolder()
 | 
			
		||||
      this.setMetadataProvider()
 | 
			
		||||
    },
 | 
			
		||||
    setDefaultFolder() {
 | 
			
		||||
      if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) {
 | 
			
		||||
        this.selectedFolderId = this.selectedLibrary.folders[0].id
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setMetadataProvider() {
 | 
			
		||||
      this.fetchMetadata.provider ||= this.$store.getters['libraries/getLibraryProvider'](this.selectedLibraryId)
 | 
			
		||||
    },
 | 
			
		||||
    removeItem(item) {
 | 
			
		||||
      this.items = this.items.filter((b) => b.index !== item.index)
 | 
			
		||||
      if (!this.items.length) {
 | 
			
		||||
@ -213,27 +241,49 @@ export default {
 | 
			
		||||
      var items = e.dataTransfer.items || []
 | 
			
		||||
 | 
			
		||||
      var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType)
 | 
			
		||||
      this.setResults(itemResults)
 | 
			
		||||
      this.onItemsSelected(itemResults)
 | 
			
		||||
    },
 | 
			
		||||
    inputChanged(e) {
 | 
			
		||||
      if (!e.target || !e.target.files) return
 | 
			
		||||
      var _files = Array.from(e.target.files)
 | 
			
		||||
      if (_files && _files.length) {
 | 
			
		||||
        var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType)
 | 
			
		||||
        this.setResults(itemResults)
 | 
			
		||||
        this.onItemsSelected(itemResults)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setResults(itemResults) {
 | 
			
		||||
    onItemsSelected(itemResults) {
 | 
			
		||||
      if (this.itemSelectionSuccessful(itemResults)) {
 | 
			
		||||
        // setTimeout ensures the new item ref is attached before this method is called
 | 
			
		||||
        setTimeout(this.attemptMetadataFetch, 0)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    itemSelectionSuccessful(itemResults) {
 | 
			
		||||
      console.log('Upload results', itemResults)
 | 
			
		||||
 | 
			
		||||
      if (itemResults.error) {
 | 
			
		||||
        this.error = itemResults.error
 | 
			
		||||
        this.items = []
 | 
			
		||||
        this.ignoredFiles = []
 | 
			
		||||
      } else {
 | 
			
		||||
        this.error = ''
 | 
			
		||||
        this.items = itemResults.items
 | 
			
		||||
        this.ignoredFiles = itemResults.ignoredFiles
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      console.log('Upload results', itemResults)
 | 
			
		||||
 | 
			
		||||
      this.error = ''
 | 
			
		||||
      this.items = itemResults.items
 | 
			
		||||
      this.ignoredFiles = itemResults.ignoredFiles
 | 
			
		||||
      return true
 | 
			
		||||
    },
 | 
			
		||||
    attemptMetadataFetch() {
 | 
			
		||||
      if (!this.canFetchMetadata) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.items.forEach((item) => {
 | 
			
		||||
        let itemRef = this.$refs[`itemCard-${item.index}`]
 | 
			
		||||
 | 
			
		||||
        if (itemRef?.length) {
 | 
			
		||||
          itemRef[0].fetchMetadata(this.fetchMetadata.provider)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    updateItemCardStatus(index, status) {
 | 
			
		||||
      var ref = this.$refs[`itemCard-${index}`]
 | 
			
		||||
@ -248,8 +298,8 @@ export default {
 | 
			
		||||
      var form = new FormData()
 | 
			
		||||
      form.set('title', item.title)
 | 
			
		||||
      if (!this.selectedLibraryIsPodcast) {
 | 
			
		||||
        form.set('author', item.author)
 | 
			
		||||
        form.set('series', item.series)
 | 
			
		||||
        form.set('author', item.author || '')
 | 
			
		||||
        form.set('series', item.series || '')
 | 
			
		||||
      }
 | 
			
		||||
      form.set('library', this.selectedLibraryId)
 | 
			
		||||
      form.set('folder', this.selectedFolderId)
 | 
			
		||||
@ -346,6 +396,8 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.selectedLibraryId = this.$store.state.libraries.currentLibraryId
 | 
			
		||||
    this.setMetadataProvider()
 | 
			
		||||
 | 
			
		||||
    this.setDefaultFolder()
 | 
			
		||||
    window.addEventListener('dragenter', this.dragenter)
 | 
			
		||||
    window.addEventListener('dragleave', this.dragleave)
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,7 @@ Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => {
 | 
			
		||||
    .replace(lineBreaks, replacement)
 | 
			
		||||
    .replace(windowsReservedRe, replacement)
 | 
			
		||||
    .replace(windowsTrailingRe, replacement)
 | 
			
		||||
    .replace(/\s+/g, ' ') // Replace consecutive spaces with a single space
 | 
			
		||||
 | 
			
		||||
  // Check if basename is too many bytes
 | 
			
		||||
  const ext = Path.extname(sanitized) // separate out file extension
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Upravit uživatelské {0}",
 | 
			
		||||
  "ButtonViewAll": "Zobrazit vše",
 | 
			
		||||
  "ButtonYes": "Ano",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Účet",
 | 
			
		||||
  "HeaderAdvanced": "Pokročilé",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Autor (příjmení a jméno)",
 | 
			
		||||
  "LabelAuthors": "Autoři",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Automaticky stahovat epizody",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Příklad",
 | 
			
		||||
  "LabelExplicit": "Explicitní",
 | 
			
		||||
  "LabelFeedURL": "URL zdroje",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Soubor",
 | 
			
		||||
  "LabelFileBirthtime": "Čas vzniku souboru",
 | 
			
		||||
  "LabelFileModified": "Soubor změněn",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minuta",
 | 
			
		||||
  "LabelMissing": "Chybějící",
 | 
			
		||||
  "LabelMissingParts": "Chybějící díly",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Více",
 | 
			
		||||
  "LabelMoreInfo": "Více informací",
 | 
			
		||||
  "LabelName": "Jméno",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky",
 | 
			
		||||
  "LabelUploaderDropFiles": "Odstranit soubory",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Použít stopu kapitoly",
 | 
			
		||||
  "LabelUseFullTrack": "Použít celou stopu",
 | 
			
		||||
  "LabelUser": "Uživatel",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Rediger bruger {0}",
 | 
			
		||||
  "ButtonViewAll": "Vis Alle",
 | 
			
		||||
  "ButtonYes": "Ja",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Konto",
 | 
			
		||||
  "HeaderAdvanced": "Avanceret",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
 | 
			
		||||
  "LabelAuthors": "Forfattere",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Auto Download Episoder",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Eksempel",
 | 
			
		||||
  "LabelExplicit": "Eksplisit",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Fil",
 | 
			
		||||
  "LabelFileBirthtime": "Fødselstidspunkt for fil",
 | 
			
		||||
  "LabelFileModified": "Fil ændret",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minut",
 | 
			
		||||
  "LabelMissing": "Mangler",
 | 
			
		||||
  "LabelMissingParts": "Manglende dele",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Mere",
 | 
			
		||||
  "LabelMoreInfo": "Mere info",
 | 
			
		||||
  "LabelName": "Navn",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Tillad overskrivning af eksisterende detaljer for de valgte bøger, når der findes en match",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Træk og slip filer eller mapper",
 | 
			
		||||
  "LabelUploaderDropFiles": "Smid filer",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Brug kapitel-spor",
 | 
			
		||||
  "LabelUseFullTrack": "Brug fuldt spor",
 | 
			
		||||
  "LabelUser": "Bruger",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Benutzer {0} bearbeiten",
 | 
			
		||||
  "ButtonViewAll": "Alles anzeigen",
 | 
			
		||||
  "ButtonYes": "Ja",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Konto",
 | 
			
		||||
  "HeaderAdvanced": "Erweitert",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
 | 
			
		||||
  "LabelAuthors": "Autoren",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Automatischer Start",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Automatische Registrierung",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Beispiel",
 | 
			
		||||
  "LabelExplicit": "Explizit (Altersbeschränkung)",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Datei",
 | 
			
		||||
  "LabelFileBirthtime": "Datei erstellt",
 | 
			
		||||
  "LabelFileModified": "Datei geändert",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minute",
 | 
			
		||||
  "LabelMissing": "Fehlend",
 | 
			
		||||
  "LabelMissingParts": "Fehlende Teile",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den Sie entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen können. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.",
 | 
			
		||||
  "LabelMore": "Mehr",
 | 
			
		||||
  "LabelMoreInfo": "Mehr Info",
 | 
			
		||||
  "LabelName": "Name",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
 | 
			
		||||
  "LabelUploaderDropFiles": "Dateien löschen",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Kapiteldatei verwenden",
 | 
			
		||||
  "LabelUseFullTrack": "Gesamte Datei verwenden",
 | 
			
		||||
  "LabelUser": "Benutzer",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Edit user {0}",
 | 
			
		||||
  "ButtonViewAll": "View All",
 | 
			
		||||
  "ButtonYes": "Yes",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Account",
 | 
			
		||||
  "HeaderAdvanced": "Advanced",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Author (Last, First)",
 | 
			
		||||
  "LabelAuthors": "Authors",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Auto Download Episodes",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Example",
 | 
			
		||||
  "LabelExplicit": "Explicit",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "File",
 | 
			
		||||
  "LabelFileBirthtime": "File Birthtime",
 | 
			
		||||
  "LabelFileModified": "File Modified",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minute",
 | 
			
		||||
  "LabelMissing": "Missing",
 | 
			
		||||
  "LabelMissingParts": "Missing Parts",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "More",
 | 
			
		||||
  "LabelMoreInfo": "More Info",
 | 
			
		||||
  "LabelName": "Name",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Drag & drop files or folders",
 | 
			
		||||
  "LabelUploaderDropFiles": "Drop files",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Use chapter track",
 | 
			
		||||
  "LabelUseFullTrack": "Use full track",
 | 
			
		||||
  "LabelUser": "User",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Editar Usuario {0}",
 | 
			
		||||
  "ButtonViewAll": "Ver Todos",
 | 
			
		||||
  "ButtonYes": "Aceptar",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Cuenta",
 | 
			
		||||
  "HeaderAdvanced": "Avanzado",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
 | 
			
		||||
  "LabelAuthors": "Autores",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Ejemplo",
 | 
			
		||||
  "LabelExplicit": "Explicito",
 | 
			
		||||
  "LabelFeedURL": "Fuente de URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Archivo",
 | 
			
		||||
  "LabelFileBirthtime": "Archivo Creado en",
 | 
			
		||||
  "LabelFileModified": "Archivo modificado",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minuto",
 | 
			
		||||
  "LabelMissing": "Ausente",
 | 
			
		||||
  "LabelMissingParts": "Partes Ausentes",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Más",
 | 
			
		||||
  "LabelMoreInfo": "Más Información",
 | 
			
		||||
  "LabelName": "Nombre",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
 | 
			
		||||
  "LabelUploaderDropFiles": "Suelte los Archivos",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Usar pista por capitulo",
 | 
			
		||||
  "LabelUseFullTrack": "Usar pista completa",
 | 
			
		||||
  "LabelUser": "Usuario",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Modifier l’utilisateur {0}",
 | 
			
		||||
  "ButtonViewAll": "Afficher tout",
 | 
			
		||||
  "ButtonYes": "Oui",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Compte",
 | 
			
		||||
  "HeaderAdvanced": "Avancé",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
 | 
			
		||||
  "LabelAuthors": "Auteurs",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Téléchargement automatique d’épisode",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Exemple",
 | 
			
		||||
  "LabelExplicit": "Restriction",
 | 
			
		||||
  "LabelFeedURL": "URL du flux",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Fichier",
 | 
			
		||||
  "LabelFileBirthtime": "Création du fichier",
 | 
			
		||||
  "LabelFileModified": "Modification du fichier",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minute",
 | 
			
		||||
  "LabelMissing": "Manquant",
 | 
			
		||||
  "LabelMissingParts": "Parties manquantes",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Plus",
 | 
			
		||||
  "LabelMoreInfo": "Plus d’info",
 | 
			
		||||
  "LabelName": "Nom",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsqu’une correspondance est trouvée",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers",
 | 
			
		||||
  "LabelUploaderDropFiles": "Déposer des fichiers",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Utiliser la piste du chapitre",
 | 
			
		||||
  "LabelUseFullTrack": "Utiliser la piste Complète",
 | 
			
		||||
  "LabelUser": "Utilisateur",
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "ButtonAdd": "ઉમેરો",
 | 
			
		||||
  "ButtonAddChapters": "પ્રકરણો ઉમેરો",
 | 
			
		||||
  "ButtonAddDevice": "Add Device",
 | 
			
		||||
  "ButtonAddLibrary": "Add Library",
 | 
			
		||||
  "ButtonAddDevice": "ઉપકરણ ઉમેરો",
 | 
			
		||||
  "ButtonAddLibrary": "પુસ્તકાલય ઉમેરો",
 | 
			
		||||
  "ButtonAddPodcasts": "પોડકાસ્ટ ઉમેરો",
 | 
			
		||||
  "ButtonAddUser": "Add User",
 | 
			
		||||
  "ButtonAddUser": "વપરાશકર્તા ઉમેરો",
 | 
			
		||||
  "ButtonAddYourFirstLibrary": "તમારી પ્રથમ પુસ્તકાલય ઉમેરો",
 | 
			
		||||
  "ButtonApply": "લાગુ કરો",
 | 
			
		||||
  "ButtonApplyChapters": "પ્રકરણો લાગુ કરો",
 | 
			
		||||
@ -58,11 +58,11 @@
 | 
			
		||||
  "ButtonRemoveAll": "બધું કાઢી નાખો",
 | 
			
		||||
  "ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
 | 
			
		||||
  "ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
 | 
			
		||||
  "ButtonRemoveFromContinueReading": "Remove from Continue Reading",
 | 
			
		||||
  "ButtonRemoveFromContinueReading": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
 | 
			
		||||
  "ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
 | 
			
		||||
  "ButtonReScan": "ફરીથી સ્કેન કરો",
 | 
			
		||||
  "ButtonReset": "રીસેટ કરો",
 | 
			
		||||
  "ButtonResetToDefault": "Reset to default",
 | 
			
		||||
  "ButtonResetToDefault": "ડિફોલ્ટ પર રીસેટ કરો",
 | 
			
		||||
  "ButtonRestore": "પુનઃસ્થાપિત કરો",
 | 
			
		||||
  "ButtonSave": "સાચવો",
 | 
			
		||||
  "ButtonSaveAndClose": "સાચવો અને બંધ કરો",
 | 
			
		||||
@ -78,7 +78,7 @@
 | 
			
		||||
  "ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
 | 
			
		||||
  "ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
 | 
			
		||||
  "ButtonSubmit": "સબમિટ કરો",
 | 
			
		||||
  "ButtonTest": "Test",
 | 
			
		||||
  "ButtonTest": "પરખ કરો",
 | 
			
		||||
  "ButtonUpload": "અપલોડ કરો",
 | 
			
		||||
  "ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
 | 
			
		||||
  "ButtonUploadCover": "કવર અપલોડ કરો",
 | 
			
		||||
@ -87,81 +87,84 @@
 | 
			
		||||
  "ButtonUserEdit": "વપરાશકર્તા {0} સંપાદિત કરો",
 | 
			
		||||
  "ButtonViewAll": "બધું જુઓ",
 | 
			
		||||
  "ButtonYes": "હા",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "એકાઉન્ટ",
 | 
			
		||||
  "HeaderAdvanced": "અડ્વાન્સડ",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ",
 | 
			
		||||
  "HeaderAudiobookTools": "Audiobook File Management Tools",
 | 
			
		||||
  "HeaderAudioTracks": "Audio Tracks",
 | 
			
		||||
  "HeaderAudiobookTools": "ઓડિયોબુક ફાઇલ વ્યવસ્થાપન ટૂલ્સ",
 | 
			
		||||
  "HeaderAudioTracks": "ઓડિયો ટ્રેક્સ",
 | 
			
		||||
  "HeaderAuthentication": "Authentication",
 | 
			
		||||
  "HeaderBackups": "Backups",
 | 
			
		||||
  "HeaderChangePassword": "Change Password",
 | 
			
		||||
  "HeaderChapters": "Chapters",
 | 
			
		||||
  "HeaderChooseAFolder": "Choose a Folder",
 | 
			
		||||
  "HeaderCollection": "Collection",
 | 
			
		||||
  "HeaderCollectionItems": "Collection Items",
 | 
			
		||||
  "HeaderCover": "Cover",
 | 
			
		||||
  "HeaderCurrentDownloads": "Current Downloads",
 | 
			
		||||
  "HeaderDetails": "Details",
 | 
			
		||||
  "HeaderDownloadQueue": "Download Queue",
 | 
			
		||||
  "HeaderEbookFiles": "Ebook Files",
 | 
			
		||||
  "HeaderEmail": "Email",
 | 
			
		||||
  "HeaderEmailSettings": "Email Settings",
 | 
			
		||||
  "HeaderEpisodes": "Episodes",
 | 
			
		||||
  "HeaderEreaderDevices": "Ereader Devices",
 | 
			
		||||
  "HeaderEreaderSettings": "Ereader Settings",
 | 
			
		||||
  "HeaderFiles": "Files",
 | 
			
		||||
  "HeaderFindChapters": "Find Chapters",
 | 
			
		||||
  "HeaderIgnoredFiles": "Ignored Files",
 | 
			
		||||
  "HeaderItemFiles": "Item Files",
 | 
			
		||||
  "HeaderItemMetadataUtils": "Item Metadata Utils",
 | 
			
		||||
  "HeaderLastListeningSession": "Last Listening Session",
 | 
			
		||||
  "HeaderLatestEpisodes": "Latest episodes",
 | 
			
		||||
  "HeaderLibraries": "Libraries",
 | 
			
		||||
  "HeaderLibraryFiles": "Library Files",
 | 
			
		||||
  "HeaderLibraryStats": "Library Stats",
 | 
			
		||||
  "HeaderListeningSessions": "Listening Sessions",
 | 
			
		||||
  "HeaderListeningStats": "Listening Stats",
 | 
			
		||||
  "HeaderLogin": "Login",
 | 
			
		||||
  "HeaderLogs": "Logs",
 | 
			
		||||
  "HeaderManageGenres": "Manage Genres",
 | 
			
		||||
  "HeaderManageTags": "Manage Tags",
 | 
			
		||||
  "HeaderMapDetails": "Map details",
 | 
			
		||||
  "HeaderMatch": "Match",
 | 
			
		||||
  "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
 | 
			
		||||
  "HeaderMetadataToEmbed": "Metadata to embed",
 | 
			
		||||
  "HeaderNewAccount": "New Account",
 | 
			
		||||
  "HeaderNewLibrary": "New Library",
 | 
			
		||||
  "HeaderNotifications": "Notifications",
 | 
			
		||||
  "HeaderBackups": "બેકઅપ્સ",
 | 
			
		||||
  "HeaderChangePassword": "પાસવર્ડ બદલો",
 | 
			
		||||
  "HeaderChapters": "પ્રકરણો",
 | 
			
		||||
  "HeaderChooseAFolder": "ફોલ્ડર પસંદ કરો",
 | 
			
		||||
  "HeaderCollection": "સંગ્રહ",
 | 
			
		||||
  "HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
 | 
			
		||||
  "HeaderCover": "આવરણ",
 | 
			
		||||
  "HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
 | 
			
		||||
  "HeaderDetails": "વિગતો",
 | 
			
		||||
  "HeaderDownloadQueue": "ડાઉનલોડ કતાર",
 | 
			
		||||
  "HeaderEbookFiles": "ઇબુક ફાઇલો",
 | 
			
		||||
  "HeaderEmail": "ઈમેલ",
 | 
			
		||||
  "HeaderEmailSettings": "ઈમેલ સેટિંગ્સ",
 | 
			
		||||
  "HeaderEpisodes": "એપિસોડ્સ",
 | 
			
		||||
  "HeaderEreaderDevices": "ઇરીડર ઉપકરણો",
 | 
			
		||||
  "HeaderEreaderSettings": "ઇરીડર સેટિંગ્સ",
 | 
			
		||||
  "HeaderFiles": "ફાઇલો",
 | 
			
		||||
  "HeaderFindChapters": "પ્રકરણો શોધો",
 | 
			
		||||
  "HeaderIgnoredFiles": "અવગણેલી ફાઇલો",
 | 
			
		||||
  "HeaderItemFiles": "વાસ્તુ ની ફાઈલો",
 | 
			
		||||
  "HeaderItemMetadataUtils": "વસ્તુ મેટાડેટા સાધનો",
 | 
			
		||||
  "HeaderLastListeningSession": "છેલ્લી સાંભળતી સેશન",
 | 
			
		||||
  "HeaderLatestEpisodes": "નવીનતમ એપિસોડ્સ",
 | 
			
		||||
  "HeaderLibraries": "પુસ્તકાલયો",
 | 
			
		||||
  "HeaderLibraryFiles": "પુસ્તકાલય ફાઇલો",
 | 
			
		||||
  "HeaderLibraryStats": "પુસ્તકાલય આંકડા",
 | 
			
		||||
  "HeaderListeningSessions": "સાંભળતી સેશન્સ",
 | 
			
		||||
  "HeaderListeningStats": "સાંભળતી આંકડા",
 | 
			
		||||
  "HeaderLogin": "લોગિન",
 | 
			
		||||
  "HeaderLogs": "લોગ્સ",
 | 
			
		||||
  "HeaderManageGenres": "જાતિઓ મેનેજ કરો",
 | 
			
		||||
  "HeaderManageTags": "ટેગ્સ મેનેજ કરો",
 | 
			
		||||
  "HeaderMapDetails": "વિગતો મેપ કરો",
 | 
			
		||||
  "HeaderMatch": "મેળ ખાતી શોધો",
 | 
			
		||||
  "HeaderMetadataOrderOfPrecedence": "મેટાડેટા પ્રાધાન્યતાનો ક્રમ",
 | 
			
		||||
  "HeaderMetadataToEmbed": "એમ્બેડ કરવા માટે મેટાડેટા",
 | 
			
		||||
  "HeaderNewAccount": "નવું એકાઉન્ટ",
 | 
			
		||||
  "HeaderNewLibrary": "નવી પુસ્તકાલય",
 | 
			
		||||
  "HeaderNotifications": "સૂચનાઓ",
 | 
			
		||||
  "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
 | 
			
		||||
  "HeaderOpenRSSFeed": "Open RSS Feed",
 | 
			
		||||
  "HeaderOtherFiles": "Other Files",
 | 
			
		||||
  "HeaderOpenRSSFeed": "RSS ફીડ ખોલો",
 | 
			
		||||
  "HeaderOtherFiles": "અન્ય ફાઇલો",
 | 
			
		||||
  "HeaderPasswordAuthentication": "Password Authentication",
 | 
			
		||||
  "HeaderPermissions": "Permissions",
 | 
			
		||||
  "HeaderPlayerQueue": "Player Queue",
 | 
			
		||||
  "HeaderPlaylist": "Playlist",
 | 
			
		||||
  "HeaderPlaylistItems": "Playlist Items",
 | 
			
		||||
  "HeaderPodcastsToAdd": "Podcasts to Add",
 | 
			
		||||
  "HeaderPreviewCover": "Preview Cover",
 | 
			
		||||
  "HeaderRemoveEpisode": "Remove Episode",
 | 
			
		||||
  "HeaderRemoveEpisodes": "Remove {0} Episodes",
 | 
			
		||||
  "HeaderRSSFeedGeneral": "RSS Details",
 | 
			
		||||
  "HeaderRSSFeedIsOpen": "RSS Feed is Open",
 | 
			
		||||
  "HeaderRSSFeeds": "RSS Feeds",
 | 
			
		||||
  "HeaderSavedMediaProgress": "Saved Media Progress",
 | 
			
		||||
  "HeaderSchedule": "Schedule",
 | 
			
		||||
  "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
 | 
			
		||||
  "HeaderSession": "Session",
 | 
			
		||||
  "HeaderSetBackupSchedule": "Set Backup Schedule",
 | 
			
		||||
  "HeaderSettings": "Settings",
 | 
			
		||||
  "HeaderSettingsDisplay": "Display",
 | 
			
		||||
  "HeaderSettingsExperimental": "Experimental Features",
 | 
			
		||||
  "HeaderSettingsGeneral": "General",
 | 
			
		||||
  "HeaderSettingsScanner": "Scanner",
 | 
			
		||||
  "HeaderSleepTimer": "Sleep Timer",
 | 
			
		||||
  "HeaderStatsLargestItems": "Largest Items",
 | 
			
		||||
  "HeaderStatsLongestItems": "Longest Items (hrs)",
 | 
			
		||||
  "HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
 | 
			
		||||
  "HeaderStatsRecentSessions": "Recent Sessions",
 | 
			
		||||
  "HeaderPermissions": "પરવાનગીઓ",
 | 
			
		||||
  "HeaderPlayerQueue": "પ્લેયર કતાર",
 | 
			
		||||
  "HeaderPlaylist": "પ્લેલિસ્ટ",
 | 
			
		||||
  "HeaderPlaylistItems": "પ્લેલિસ્ટ ની વસ્તુઓ",
 | 
			
		||||
  "HeaderPodcastsToAdd": "ઉમેરવા માટે પોડકાસ્ટ્સ",
 | 
			
		||||
  "HeaderPreviewCover": "પૂર્વાવલોકન કવર",
 | 
			
		||||
  "HeaderRemoveEpisode": "એપિસોડ કાઢી નાખો",
 | 
			
		||||
  "HeaderRemoveEpisodes": "{0} એપિસોડ્સ કાઢી નાખો",
 | 
			
		||||
  "HeaderRSSFeedGeneral": "સામાન્ય RSS ફીડ",
 | 
			
		||||
  "HeaderRSSFeedIsOpen": "RSS ફીડ ખોલેલી છે",
 | 
			
		||||
  "HeaderRSSFeeds": "RSS ફીડ્સ",
 | 
			
		||||
  "HeaderSavedMediaProgress": "સાચવેલ મીડિયા પ્રગતિ",
 | 
			
		||||
  "HeaderSchedule": "સમયપત્રક",
 | 
			
		||||
  "HeaderScheduleLibraryScans": "પુસ્તકાલય સ્કેન સમયપત્રક",
 | 
			
		||||
  "HeaderSession": "સેશન",
 | 
			
		||||
  "HeaderSetBackupSchedule": "બેકઅપ સમયપત્રક સેટ કરો",
 | 
			
		||||
  "HeaderSettings": "સેટિંગ્સ",
 | 
			
		||||
  "HeaderSettingsDisplay": "ડિસ્પ્લે સેટિંગ્સ",
 | 
			
		||||
  "HeaderSettingsExperimental": "પ્રયોગશીલ સેટિંગ્સ",
 | 
			
		||||
  "HeaderSettingsGeneral": "સામાન્ય સેટિંગ્સ",
 | 
			
		||||
  "HeaderSettingsScanner": "સ્કેનર સેટિંગ્સ",
 | 
			
		||||
  "HeaderSleepTimer": "સ્લીપ ટાઈમર",
 | 
			
		||||
  "HeaderStatsLargestItems": "સૌથી મોટી વસ્તુઓ",
 | 
			
		||||
  "HeaderStatsLongestItems": "સૌથી લાંબી વસ્તુઓ (કલાક)",
 | 
			
		||||
  "HeaderStatsMinutesListeningChart": "સાંભળવાની મિનિટ (છેલ્લા ૭ દિવસ)",
 | 
			
		||||
  "HeaderStatsRecentSessions": "છેલ્લી સાંભળતી સેશન્સ",
 | 
			
		||||
  "HeaderStatsTop10Authors": "Top 10 Authors",
 | 
			
		||||
  "HeaderStatsTop5Genres": "Top 5 Genres",
 | 
			
		||||
  "HeaderTableOfContents": "Table of Contents",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Author (Last, First)",
 | 
			
		||||
  "LabelAuthors": "Authors",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Auto Download Episodes",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Example",
 | 
			
		||||
  "LabelExplicit": "Explicit",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "File",
 | 
			
		||||
  "LabelFileBirthtime": "File Birthtime",
 | 
			
		||||
  "LabelFileModified": "File Modified",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minute",
 | 
			
		||||
  "LabelMissing": "Missing",
 | 
			
		||||
  "LabelMissingParts": "Missing Parts",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "More",
 | 
			
		||||
  "LabelMoreInfo": "More Info",
 | 
			
		||||
  "LabelName": "Name",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Drag & drop files or folders",
 | 
			
		||||
  "LabelUploaderDropFiles": "Drop files",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Use chapter track",
 | 
			
		||||
  "LabelUseFullTrack": "Use full track",
 | 
			
		||||
  "LabelUser": "User",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "उपयोगकर्ता {0} को संपादित करें",
 | 
			
		||||
  "ButtonViewAll": "सभी को देखें",
 | 
			
		||||
  "ButtonYes": "हाँ",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "खाता",
 | 
			
		||||
  "HeaderAdvanced": "विकसित",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Author (Last, First)",
 | 
			
		||||
  "LabelAuthors": "Authors",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Auto Download Episodes",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Example",
 | 
			
		||||
  "LabelExplicit": "Explicit",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "File",
 | 
			
		||||
  "LabelFileBirthtime": "File Birthtime",
 | 
			
		||||
  "LabelFileModified": "File Modified",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minute",
 | 
			
		||||
  "LabelMissing": "Missing",
 | 
			
		||||
  "LabelMissingParts": "Missing Parts",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "More",
 | 
			
		||||
  "LabelMoreInfo": "More Info",
 | 
			
		||||
  "LabelName": "Name",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Drag & drop files or folders",
 | 
			
		||||
  "LabelUploaderDropFiles": "Drop files",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Use chapter track",
 | 
			
		||||
  "LabelUseFullTrack": "Use full track",
 | 
			
		||||
  "LabelUser": "User",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Edit user {0}",
 | 
			
		||||
  "ButtonViewAll": "Prikaži sve",
 | 
			
		||||
  "ButtonYes": "Da",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Korisnički račun",
 | 
			
		||||
  "HeaderAdvanced": "Napredno",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Author (Last, First)",
 | 
			
		||||
  "LabelAuthors": "Autori",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Automatski preuzmi epizode",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Example",
 | 
			
		||||
  "LabelExplicit": "Explicit",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Datoteka",
 | 
			
		||||
  "LabelFileBirthtime": "File Birthtime",
 | 
			
		||||
  "LabelFileModified": "File Modified",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minuta",
 | 
			
		||||
  "LabelMissing": "Nedostaje",
 | 
			
		||||
  "LabelMissingParts": "Nedostajali dijelovi",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Više",
 | 
			
		||||
  "LabelMoreInfo": "More Info",
 | 
			
		||||
  "LabelName": "Ime",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Dozvoli postavljanje novih detalja za odabrane knjige nakon što je match pronađen",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Drag & Drop datoteke ili foldere",
 | 
			
		||||
  "LabelUploaderDropFiles": "Ubaci datoteke",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Koristi poglavlja track",
 | 
			
		||||
  "LabelUseFullTrack": "Koristi cijeli track",
 | 
			
		||||
  "LabelUser": "Korisnik",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Modifica Utente {0}",
 | 
			
		||||
  "ButtonViewAll": "Mostra Tutto",
 | 
			
		||||
  "ButtonYes": "Si",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Account",
 | 
			
		||||
  "HeaderAdvanced": "Avanzate",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Autori (Per Cognome)",
 | 
			
		||||
  "LabelAuthors": "Autori",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Auto Download Episodi",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Esempio",
 | 
			
		||||
  "LabelExplicit": "Esplicito",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "File",
 | 
			
		||||
  "LabelFileBirthtime": "Data Creazione",
 | 
			
		||||
  "LabelFileModified": "Ultima modifica",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minuto",
 | 
			
		||||
  "LabelMissing": "Altro",
 | 
			
		||||
  "LabelMissingParts": "Parti rimantenti",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Molto",
 | 
			
		||||
  "LabelMoreInfo": "Più Info",
 | 
			
		||||
  "LabelName": "Nome",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
 | 
			
		||||
  "LabelUploaderDropFiles": "Elimina file",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Usa il Capitolo della Traccia",
 | 
			
		||||
  "LabelUseFullTrack": "Usa la traccia totale",
 | 
			
		||||
  "LabelUser": "Utente",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Redaguoti naudotoją {0}",
 | 
			
		||||
  "ButtonViewAll": "Peržiūrėti visus",
 | 
			
		||||
  "ButtonYes": "Taip",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Paskyra",
 | 
			
		||||
  "HeaderAdvanced": "Papildomi",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)",
 | 
			
		||||
  "LabelAuthors": "Autoriai",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Automatiškai atsisiųsti epizodus",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Pavyzdys",
 | 
			
		||||
  "LabelExplicit": "Suaugusiems",
 | 
			
		||||
  "LabelFeedURL": "Srauto URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Failas",
 | 
			
		||||
  "LabelFileBirthtime": "Failo kūrimo laikas",
 | 
			
		||||
  "LabelFileModified": "Failo keitimo laikas",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minutė",
 | 
			
		||||
  "LabelMissing": "Trūksta",
 | 
			
		||||
  "LabelMissingParts": "Trūkstamos dalys",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Daugiau",
 | 
			
		||||
  "LabelMoreInfo": "Daugiau informacijos",
 | 
			
		||||
  "LabelName": "Pavadinimas",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Leisti perrašyti esamus duomenis pasirinktoms knygoms, kai yra rasta atitikmenų",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Tempkite ir paleiskite failus ar aplankus",
 | 
			
		||||
  "LabelUploaderDropFiles": "Nutempti failus",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Naudoti skyrių takelį",
 | 
			
		||||
  "LabelUseFullTrack": "Naudoti visą takelį",
 | 
			
		||||
  "LabelUser": "Vartotojas",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Wijzig gebruiker {0}",
 | 
			
		||||
  "ButtonViewAll": "Toon alle",
 | 
			
		||||
  "ButtonYes": "Ja",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Account",
 | 
			
		||||
  "HeaderAdvanced": "Geavanceerd",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)",
 | 
			
		||||
  "LabelAuthors": "Auteurs",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Voorbeeld",
 | 
			
		||||
  "LabelExplicit": "Expliciet",
 | 
			
		||||
  "LabelFeedURL": "Feed URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Bestand",
 | 
			
		||||
  "LabelFileBirthtime": "Aanmaaktijd bestand",
 | 
			
		||||
  "LabelFileModified": "Bestand gewijzigd",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minuut",
 | 
			
		||||
  "LabelMissing": "Ontbrekend",
 | 
			
		||||
  "LabelMissingParts": "Ontbrekende delen",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Meer",
 | 
			
		||||
  "LabelMoreInfo": "Meer info",
 | 
			
		||||
  "LabelName": "Naam",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Sta overschrijven van bestaande details toe voor de geselecteerde boeken wanneer een match is gevonden",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen",
 | 
			
		||||
  "LabelUploaderDropFiles": "Bestanden neerzetten",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Gebruik hoofdstuktrack",
 | 
			
		||||
  "LabelUseFullTrack": "Gebruik volledige track",
 | 
			
		||||
  "LabelUser": "Gebruiker",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Rediger bruker {0}",
 | 
			
		||||
  "ButtonViewAll": "Vis alt",
 | 
			
		||||
  "ButtonYes": "Ja",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Konto",
 | 
			
		||||
  "HeaderAdvanced": "Avansert",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise notifikasjonsinstillinger",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Forfatter (Etternavn Fornavn)",
 | 
			
		||||
  "LabelAuthors": "Forfattere",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Last ned episoder automatisk",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Eksempel",
 | 
			
		||||
  "LabelExplicit": "Eksplisitt",
 | 
			
		||||
  "LabelFeedURL": "Feed Adresse",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Fil",
 | 
			
		||||
  "LabelFileBirthtime": "Fil Opprettelsesdato",
 | 
			
		||||
  "LabelFileModified": "Fil Endret",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minutt",
 | 
			
		||||
  "LabelMissing": "Mangler",
 | 
			
		||||
  "LabelMissingParts": "Manglende deler",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Mer",
 | 
			
		||||
  "LabelMoreInfo": "Mer info",
 | 
			
		||||
  "LabelName": "Navn",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Tillat overskriving av eksisterende detaljer for de valgte bøkene når en lik bok er funnet",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Dra og slipp filer eller mapper",
 | 
			
		||||
  "LabelUploaderDropFiles": "Slipp filer",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Bruk kapittelspor",
 | 
			
		||||
  "LabelUseFullTrack": "Bruke hele sporet",
 | 
			
		||||
  "LabelUser": "Bruker",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Edit user {0}",
 | 
			
		||||
  "ButtonViewAll": "Zobacz wszystko",
 | 
			
		||||
  "ButtonYes": "Tak",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Konto",
 | 
			
		||||
  "HeaderAdvanced": "Zaawansowane",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Author (Malejąco)",
 | 
			
		||||
  "LabelAuthors": "Autorzy",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Automatyczne pobieranie odcinków",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Example",
 | 
			
		||||
  "LabelExplicit": "Nieprzyzwoite",
 | 
			
		||||
  "LabelFeedURL": "URL kanału",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Plik",
 | 
			
		||||
  "LabelFileBirthtime": "Data utworzenia pliku",
 | 
			
		||||
  "LabelFileModified": "Data modyfikacji pliku",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minuta",
 | 
			
		||||
  "LabelMissing": "Brakujący",
 | 
			
		||||
  "LabelMissingParts": "Brakujące cześci",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Więcej",
 | 
			
		||||
  "LabelMoreInfo": "More Info",
 | 
			
		||||
  "LabelName": "Nazwa",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Umożliwienie nadpisania istniejących szczegółów dla wybranych książek w przypadku znalezienia dopasowania",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Przeciągnij i puść foldery lub pliki",
 | 
			
		||||
  "LabelUploaderDropFiles": "Puść pliki",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Użyj ścieżki rozdziału",
 | 
			
		||||
  "LabelUseFullTrack": "Użycie ścieżki rozdziału",
 | 
			
		||||
  "LabelUser": "Użytkownik",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Редактировать пользователя {0}",
 | 
			
		||||
  "ButtonViewAll": "Посмотреть все",
 | 
			
		||||
  "ButtonYes": "Да",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Учетная запись",
 | 
			
		||||
  "HeaderAdvanced": "Дополнительно",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Настройки оповещений",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Автор (Фамилия, Имя)",
 | 
			
		||||
  "LabelAuthors": "Авторы",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Пример",
 | 
			
		||||
  "LabelExplicit": "Явный",
 | 
			
		||||
  "LabelFeedURL": "URL канала",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Файл",
 | 
			
		||||
  "LabelFileBirthtime": "Дата создания",
 | 
			
		||||
  "LabelFileModified": "Дата модификации",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Минуты",
 | 
			
		||||
  "LabelMissing": "Потеряно",
 | 
			
		||||
  "LabelMissingParts": "Потерянные части",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Еще",
 | 
			
		||||
  "LabelMoreInfo": "Больше информации",
 | 
			
		||||
  "LabelName": "Имя",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Позволяет перезаписывать текущие подробности для выбранных книг если будут найдены",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Перетащите файлы или каталоги",
 | 
			
		||||
  "LabelUploaderDropFiles": "Перетащите файлы",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Показывать время главы",
 | 
			
		||||
  "LabelUseFullTrack": "Показывать время книги",
 | 
			
		||||
  "LabelUser": "Пользователь",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "Redigera användare {0}",
 | 
			
		||||
  "ButtonViewAll": "Visa alla",
 | 
			
		||||
  "ButtonYes": "Ja",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Konto",
 | 
			
		||||
  "HeaderAdvanced": "Avancerad",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)",
 | 
			
		||||
  "LabelAuthors": "Författare",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "Exempel",
 | 
			
		||||
  "LabelExplicit": "Explicit",
 | 
			
		||||
  "LabelFeedURL": "Flödes-URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "Fil",
 | 
			
		||||
  "LabelFileBirthtime": "Födelse-tidpunkt för fil",
 | 
			
		||||
  "LabelFileModified": "Fil ändrad",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "Minut",
 | 
			
		||||
  "LabelMissing": "Saknad",
 | 
			
		||||
  "LabelMissingParts": "Saknade delar",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "Mer",
 | 
			
		||||
  "LabelMoreInfo": "Mer information",
 | 
			
		||||
  "LabelName": "Namn",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "Tillåt överskrivning av befintliga detaljer för de valda böckerna när en matchning hittas",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "Dra och släpp filer eller mappar",
 | 
			
		||||
  "LabelUploaderDropFiles": "Släpp filer",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "Använd kapitelspår",
 | 
			
		||||
  "LabelUseFullTrack": "Använd hela spåret",
 | 
			
		||||
  "LabelUser": "Användare",
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@
 | 
			
		||||
  "ButtonUserEdit": "编辑用户 {0}",
 | 
			
		||||
  "ButtonViewAll": "查看全部",
 | 
			
		||||
  "ButtonYes": "确定",
 | 
			
		||||
  "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "帐户",
 | 
			
		||||
  "HeaderAdvanced": "高级",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "测试通知设置",
 | 
			
		||||
@ -196,6 +199,8 @@
 | 
			
		||||
  "LabelAuthorLastFirst": "作者 (名, 姓)",
 | 
			
		||||
  "LabelAuthors": "作者",
 | 
			
		||||
  "LabelAutoDownloadEpisodes": "自动下载剧集",
 | 
			
		||||
  "LabelAutoFetchMetadata": "Auto Fetch Metadata",
 | 
			
		||||
  "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
 | 
			
		||||
  "LabelAutoLaunch": "Auto Launch",
 | 
			
		||||
  "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
 | 
			
		||||
  "LabelAutoRegister": "Auto Register",
 | 
			
		||||
@ -266,6 +271,7 @@
 | 
			
		||||
  "LabelExample": "示例",
 | 
			
		||||
  "LabelExplicit": "信息准确",
 | 
			
		||||
  "LabelFeedURL": "源 URL",
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "文件",
 | 
			
		||||
  "LabelFileBirthtime": "文件创建时间",
 | 
			
		||||
  "LabelFileModified": "文件修改时间",
 | 
			
		||||
@ -337,6 +343,8 @@
 | 
			
		||||
  "LabelMinute": "分钟",
 | 
			
		||||
  "LabelMissing": "丢失",
 | 
			
		||||
  "LabelMissingParts": "丢失的部分",
 | 
			
		||||
  "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
 | 
			
		||||
  "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
 | 
			
		||||
  "LabelMore": "更多",
 | 
			
		||||
  "LabelMoreInfo": "更多..",
 | 
			
		||||
  "LabelName": "名称",
 | 
			
		||||
@ -515,6 +523,7 @@
 | 
			
		||||
  "LabelUpdateDetailsHelp": "找到匹配项时允许覆盖所选书籍存在的详细信息",
 | 
			
		||||
  "LabelUploaderDragAndDrop": "拖放文件或文件夹",
 | 
			
		||||
  "LabelUploaderDropFiles": "删除文件",
 | 
			
		||||
  "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
 | 
			
		||||
  "LabelUseChapterTrack": "使用章节音轨",
 | 
			
		||||
  "LabelUseFullTrack": "使用完整音轨",
 | 
			
		||||
  "LabelUser": "用户",
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
 | 
			
		||||
const OpenIDClient = require('openid-client')
 | 
			
		||||
const Database = require('./Database')
 | 
			
		||||
const Logger = require('./Logger')
 | 
			
		||||
const e = require('express')
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @class Class for handling all the authentication related functionality.
 | 
			
		||||
@ -15,6 +16,8 @@ const Logger = require('./Logger')
 | 
			
		||||
class Auth {
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    // Map of openId sessions indexed by oauth2 state-variable
 | 
			
		||||
    this.openIdAuthSession = new Map()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -187,9 +190,10 @@ class Auth {
 | 
			
		||||
   * @param {import('express').Response} res
 | 
			
		||||
   */
 | 
			
		||||
  paramsToCookies(req, res) {
 | 
			
		||||
    if (req.query.isRest?.toLowerCase() == 'true') {
 | 
			
		||||
    // Set if isRest flag is set or if mobile oauth flow is used
 | 
			
		||||
    if (req.query.isRest?.toLowerCase() == 'true' || req.query.redirect_uri) {
 | 
			
		||||
      // store the isRest flag to the is_rest cookie 
 | 
			
		||||
      res.cookie('is_rest', req.query.isRest.toLowerCase(), {
 | 
			
		||||
      res.cookie('is_rest', 'true', {
 | 
			
		||||
        maxAge: 120000, // 2 min
 | 
			
		||||
        httpOnly: true
 | 
			
		||||
      })
 | 
			
		||||
@ -283,8 +287,27 @@ class Auth {
 | 
			
		||||
        //    for API or mobile clients
 | 
			
		||||
        const oidcStrategy = passport._strategy('openid-client')
 | 
			
		||||
        const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
 | 
			
		||||
        oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
 | 
			
		||||
        Logger.debug(`[Auth] Set oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
 | 
			
		||||
 | 
			
		||||
        let mobile_redirect_uri = null
 | 
			
		||||
 | 
			
		||||
        // The client wishes a different redirect_uri
 | 
			
		||||
        // We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect
 | 
			
		||||
        //    where we will handle the redirect to it
 | 
			
		||||
        if (req.query.redirect_uri) {
 | 
			
		||||
          // Check if the redirect_uri is in the whitelist
 | 
			
		||||
          if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) ||
 | 
			
		||||
           (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
 | 
			
		||||
            oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString()
 | 
			
		||||
            mobile_redirect_uri = req.query.redirect_uri
 | 
			
		||||
          } else {
 | 
			
		||||
            Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`)
 | 
			
		||||
            return res.status(400).send('Invalid redirect_uri')
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
 | 
			
		||||
        const client = oidcStrategy._client
 | 
			
		||||
        const sessionKey = oidcStrategy._key
 | 
			
		||||
 | 
			
		||||
@ -324,16 +347,21 @@ class Auth {
 | 
			
		||||
        req.session[sessionKey] = {
 | 
			
		||||
          ...req.session[sessionKey],
 | 
			
		||||
          ...pick(params, 'nonce', 'state', 'max_age', 'response_type'),
 | 
			
		||||
          mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later
 | 
			
		||||
          mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out
 | 
			
		||||
          sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // We cannot save redirect_uri in the session, because it the mobile client uses browser instead of the API
 | 
			
		||||
        //   for the request to mobile-redirect and as such the session is not shared
 | 
			
		||||
        this.openIdAuthSession.set(params.state, { mobile_redirect_uri: mobile_redirect_uri })
 | 
			
		||||
 | 
			
		||||
        // Now get the URL to direct to
 | 
			
		||||
        const authorizationUrl = client.authorizationUrl({
 | 
			
		||||
          ...params,
 | 
			
		||||
          scope: 'openid profile email',
 | 
			
		||||
          response_type: 'code',
 | 
			
		||||
          code_challenge,
 | 
			
		||||
          code_challenge_method,
 | 
			
		||||
          code_challenge_method
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // params (isRest, callback) to a cookie that will be send to the client
 | 
			
		||||
@ -347,6 +375,37 @@ class Auth {
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // This will be the oauth2 callback route for mobile clients
 | 
			
		||||
    // It will redirect to an app-link like audiobookshelf://oauth
 | 
			
		||||
    router.get('/auth/openid/mobile-redirect', (req, res) => {
 | 
			
		||||
      try {
 | 
			
		||||
        // Extract the state parameter from the request
 | 
			
		||||
        const { state, code } = req.query
 | 
			
		||||
    
 | 
			
		||||
        // Check if the state provided is in our list
 | 
			
		||||
        if (!state || !this.openIdAuthSession.has(state)) {
 | 
			
		||||
          Logger.error('[Auth] /auth/openid/mobile-redirect route: State parameter mismatch')
 | 
			
		||||
          return res.status(400).send('State parameter mismatch')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mobile_redirect_uri = this.openIdAuthSession.get(state).mobile_redirect_uri
 | 
			
		||||
 | 
			
		||||
        if (!mobile_redirect_uri) {
 | 
			
		||||
          Logger.error('[Auth] No redirect URI')
 | 
			
		||||
          return res.status(400).send('No redirect URI')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.openIdAuthSession.delete(state)
 | 
			
		||||
 | 
			
		||||
        const redirectUri = `${mobile_redirect_uri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`
 | 
			
		||||
        // Redirect to the overwrite URI saved in the map
 | 
			
		||||
        res.redirect(redirectUri)
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}`)
 | 
			
		||||
        res.status(500).send('Internal Server Error')
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // openid strategy callback route (this receives the token from the configured openid login provider)
 | 
			
		||||
    router.get('/auth/openid/callback', (req, res, next) => {
 | 
			
		||||
      const oidcStrategy = passport._strategy('openid-client')
 | 
			
		||||
@ -403,11 +462,8 @@ class Auth {
 | 
			
		||||
 | 
			
		||||
      // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request
 | 
			
		||||
      // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided
 | 
			
		||||
      if (req.session[sessionKey].mobile) {
 | 
			
		||||
        return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' }, passportCallback(req, res, next))(req, res, next)
 | 
			
		||||
      } else {
 | 
			
		||||
        return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next)
 | 
			
		||||
      }
 | 
			
		||||
      // We set it here again because the passport param can change between requests
 | 
			
		||||
      return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next)
 | 
			
		||||
    },
 | 
			
		||||
      // on a successfull login: read the cookies and react like the client requested (callback or json)
 | 
			
		||||
      this.handleLoginSuccessBasedOnCookie.bind(this))
 | 
			
		||||
@ -542,13 +598,13 @@ class Auth {
 | 
			
		||||
    // Load the user given it's username
 | 
			
		||||
    const user = await Database.userModel.getUserByUsername(username.toLowerCase())
 | 
			
		||||
 | 
			
		||||
    if (!user || !user.isActive) {
 | 
			
		||||
    if (!user?.isActive) {
 | 
			
		||||
      done(null, null)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check passwordless root user
 | 
			
		||||
    if (user.type === 'root' && (!user.pash || user.pash === '')) {
 | 
			
		||||
    if (user.type === 'root' && !user.pash) {
 | 
			
		||||
      if (password) {
 | 
			
		||||
        // deny login
 | 
			
		||||
        done(null, null)
 | 
			
		||||
@ -557,6 +613,10 @@ class Auth {
 | 
			
		||||
      // approve login
 | 
			
		||||
      done(null, user)
 | 
			
		||||
      return
 | 
			
		||||
    } else if (!user.pash) {
 | 
			
		||||
      Logger.error(`[Auth] User "${user.username}"/"${user.type}" attempted to login without a password set`)
 | 
			
		||||
      done(null, null)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check password match
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ const Database = require('../Database')
 | 
			
		||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
 | 
			
		||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
 | 
			
		||||
const { isObject, getTitleIgnorePrefix } = require('../utils/index')
 | 
			
		||||
const { sanitizeFilename } = require('../utils/fileUtils')
 | 
			
		||||
 | 
			
		||||
const TaskManager = require('../managers/TaskManager')
 | 
			
		||||
 | 
			
		||||
@ -32,12 +33,9 @@ class MiscController {
 | 
			
		||||
      Logger.error('Invalid request, no files')
 | 
			
		||||
      return res.sendStatus(400)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const files = Object.values(req.files)
 | 
			
		||||
    const title = req.body.title
 | 
			
		||||
    const author = req.body.author
 | 
			
		||||
    const series = req.body.series
 | 
			
		||||
    const libraryId = req.body.library
 | 
			
		||||
    const folderId = req.body.folder
 | 
			
		||||
    const { title, author, series, folder: folderId, library: libraryId } = req.body
 | 
			
		||||
 | 
			
		||||
    const library = await Database.libraryModel.getOldById(libraryId)
 | 
			
		||||
    if (!library) {
 | 
			
		||||
@ -52,43 +50,29 @@ class MiscController {
 | 
			
		||||
      return res.status(500).send(`Invalid post data`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For setting permissions recursively
 | 
			
		||||
    let outputDirectory = ''
 | 
			
		||||
    let firstDirPath = ''
 | 
			
		||||
 | 
			
		||||
    if (library.isPodcast) { // Podcasts only in 1 folder
 | 
			
		||||
      outputDirectory = Path.join(folder.fullPath, title)
 | 
			
		||||
      firstDirPath = outputDirectory
 | 
			
		||||
    } else {
 | 
			
		||||
      firstDirPath = Path.join(folder.fullPath, author)
 | 
			
		||||
      if (series && author) {
 | 
			
		||||
        outputDirectory = Path.join(folder.fullPath, author, series, title)
 | 
			
		||||
      } else if (author) {
 | 
			
		||||
        outputDirectory = Path.join(folder.fullPath, author, title)
 | 
			
		||||
      } else {
 | 
			
		||||
        outputDirectory = Path.join(folder.fullPath, title)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (await fs.pathExists(outputDirectory)) {
 | 
			
		||||
      Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
 | 
			
		||||
      return res.status(500).send(`Directory "${outputDirectory}" already exists`)
 | 
			
		||||
    }
 | 
			
		||||
    // Podcasts should only be one folder deep
 | 
			
		||||
    const outputDirectoryParts = library.isPodcast ? [title] : [author, series, title]
 | 
			
		||||
    // `.filter(Boolean)` to strip out all the potentially missing details (eg: `author`)
 | 
			
		||||
    // before sanitizing all the directory parts to remove illegal chars and finally prepending
 | 
			
		||||
    // the base folder path
 | 
			
		||||
    const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map(part => sanitizeFilename(part))
 | 
			
		||||
    const outputDirectory = Path.join(...[folder.fullPath, ...cleanedOutputDirectoryParts])
 | 
			
		||||
 | 
			
		||||
    await fs.ensureDir(outputDirectory)
 | 
			
		||||
 | 
			
		||||
    Logger.info(`Uploading ${files.length} files to`, outputDirectory)
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < files.length; i++) {
 | 
			
		||||
      var file = files[i]
 | 
			
		||||
    for (const file of files) {
 | 
			
		||||
      const path = Path.join(outputDirectory, sanitizeFilename(file.name))
 | 
			
		||||
 | 
			
		||||
      var path = Path.join(outputDirectory, file.name)
 | 
			
		||||
      await file.mv(path).then(() => {
 | 
			
		||||
        return true
 | 
			
		||||
      }).catch((error) => {
 | 
			
		||||
        Logger.error('Failed to move file', path, error)
 | 
			
		||||
        return false
 | 
			
		||||
      })
 | 
			
		||||
      await file.mv(path)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          return true
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          Logger.error('Failed to move file', path, error)
 | 
			
		||||
          return false
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.sendStatus(200)
 | 
			
		||||
@ -645,6 +629,27 @@ class MiscController {
 | 
			
		||||
        } else {
 | 
			
		||||
          Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`)
 | 
			
		||||
        }
 | 
			
		||||
      } else if (key === 'authOpenIDMobileRedirectURIs') {
 | 
			
		||||
        function isValidRedirectURI(uri) {
 | 
			
		||||
          if (typeof uri !== 'string') return false
 | 
			
		||||
          const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i')
 | 
			
		||||
          return pattern.test(uri)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const uris = settingsUpdate[key]
 | 
			
		||||
        if (!Array.isArray(uris) ||
 | 
			
		||||
          (uris.includes('*') && uris.length > 1) ||
 | 
			
		||||
          uris.some(uri => uri !== '*' && !isValidRedirectURI(uri))) {
 | 
			
		||||
          Logger.warn(`[MiscController] Invalid value for authOpenIDMobileRedirectURIs`)
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the URIs
 | 
			
		||||
        if (Database.serverSettings[key].some(uri => !uris.includes(uri)) || uris.some(uri => !Database.serverSettings[key].includes(uri))) {
 | 
			
		||||
          Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${Database.serverSettings[key]}" to "${uris}"`)
 | 
			
		||||
          Database.serverSettings[key] = uris
 | 
			
		||||
          hasUpdates = true
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        const updatedValueType = typeof settingsUpdate[key]
 | 
			
		||||
        if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) {
 | 
			
		||||
@ -687,6 +692,7 @@ class MiscController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
      updated: hasUpdates,
 | 
			
		||||
      serverSettings: Database.serverSettings.toJSONForBrowser()
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ class ApiCacheManager {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init(database = Database) {
 | 
			
		||||
    let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy']
 | 
			
		||||
    let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy', 'afterUpsert']
 | 
			
		||||
    hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook)))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -71,6 +71,7 @@ class ServerSettings {
 | 
			
		||||
    this.authOpenIDAutoLaunch = false
 | 
			
		||||
    this.authOpenIDAutoRegister = false
 | 
			
		||||
    this.authOpenIDMatchExistingBy = null
 | 
			
		||||
    this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth']
 | 
			
		||||
 | 
			
		||||
    if (settings) {
 | 
			
		||||
      this.construct(settings)
 | 
			
		||||
@ -126,6 +127,7 @@ class ServerSettings {
 | 
			
		||||
    this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch
 | 
			
		||||
    this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister
 | 
			
		||||
    this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null
 | 
			
		||||
    this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth']
 | 
			
		||||
 | 
			
		||||
    if (!Array.isArray(this.authActiveAuthMethods)) {
 | 
			
		||||
      this.authActiveAuthMethods = ['local']
 | 
			
		||||
@ -211,7 +213,8 @@ class ServerSettings {
 | 
			
		||||
      authOpenIDButtonText: this.authOpenIDButtonText,
 | 
			
		||||
      authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
 | 
			
		||||
      authOpenIDAutoRegister: this.authOpenIDAutoRegister,
 | 
			
		||||
      authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy
 | 
			
		||||
      authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, 
 | 
			
		||||
      authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -220,6 +223,7 @@ class ServerSettings {
 | 
			
		||||
    delete json.tokenSecret
 | 
			
		||||
    delete json.authOpenIDClientID
 | 
			
		||||
    delete json.authOpenIDClientSecret
 | 
			
		||||
    delete json.authOpenIDMobileRedirectURIs
 | 
			
		||||
    return json
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -254,7 +258,8 @@ class ServerSettings {
 | 
			
		||||
      authOpenIDButtonText: this.authOpenIDButtonText,
 | 
			
		||||
      authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
 | 
			
		||||
      authOpenIDAutoRegister: this.authOpenIDAutoRegister,
 | 
			
		||||
      authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy
 | 
			
		||||
      authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy,
 | 
			
		||||
      authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,27 @@ class Audible {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Audible will sometimes send sequences with "Book 1" or "2, Dramatized Adaptation"
 | 
			
		||||
     * @see https://github.com/advplyr/audiobookshelf/issues/2380
 | 
			
		||||
     * @see https://github.com/advplyr/audiobookshelf/issues/1339
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {string} seriesName
 | 
			
		||||
     * @param {string} sequence 
 | 
			
		||||
     * @returns {string}
 | 
			
		||||
     */
 | 
			
		||||
    cleanSeriesSequence(seriesName, sequence) {
 | 
			
		||||
        if (!sequence) return ''
 | 
			
		||||
        let updatedSequence = sequence.replace(/Book /, '').trim()
 | 
			
		||||
        if (updatedSequence.includes(' ')) {
 | 
			
		||||
            updatedSequence = updatedSequence.split(' ').shift().replace(/,$/, '')
 | 
			
		||||
        }
 | 
			
		||||
        if (sequence !== updatedSequence) {
 | 
			
		||||
            Logger.debug(`[Audible] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`)
 | 
			
		||||
        }
 | 
			
		||||
        return updatedSequence
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cleanResult(item) {
 | 
			
		||||
        const { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin, formatType } = item
 | 
			
		||||
 | 
			
		||||
@ -25,13 +46,13 @@ class Audible {
 | 
			
		||||
        if (seriesPrimary) {
 | 
			
		||||
            series.push({
 | 
			
		||||
                series: seriesPrimary.name,
 | 
			
		||||
                sequence: (seriesPrimary.position || '').replace(/Book /, '') // Can be badly formatted see #1339
 | 
			
		||||
                sequence: this.cleanSeriesSequence(seriesPrimary.name, seriesPrimary.position || '')
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        if (seriesSecondary) {
 | 
			
		||||
            series.push({
 | 
			
		||||
                series: seriesSecondary.name,
 | 
			
		||||
                sequence: (seriesSecondary.position || '').replace(/Book /, '')
 | 
			
		||||
                sequence: this.cleanSeriesSequence(seriesSecondary.name, seriesSecondary.position || '')
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -64,7 +85,7 @@ class Audible {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    asinSearch(asin, region) {
 | 
			
		||||
        asin = encodeURIComponent(asin);
 | 
			
		||||
        asin = encodeURIComponent(asin)
 | 
			
		||||
        var regionQuery = region ? `?region=${region}` : ''
 | 
			
		||||
        var url = `https://api.audnex.us/books/${asin}${regionQuery}`
 | 
			
		||||
        Logger.debug(`[Audible] ASIN url: ${url}`)
 | 
			
		||||
 | 
			
		||||
@ -308,6 +308,7 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => {
 | 
			
		||||
    .replace(lineBreaks, replacement)
 | 
			
		||||
    .replace(windowsReservedRe, replacement)
 | 
			
		||||
    .replace(windowsTrailingRe, replacement)
 | 
			
		||||
    .replace(/\s+/g, ' ') // Replace consecutive spaces with a single space
 | 
			
		||||
 | 
			
		||||
  // Check if basename is too many bytes
 | 
			
		||||
  const ext = Path.extname(sanitized) // separate out file extension
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user