diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 21d97b20..075c0fd4 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -15,24 +15,33 @@
- +
- +
+ + +
+ sync +
+
+

{{ $strings.LabelDirectory }} (auto)

- +
- +
-

{{ $strings.LabelDirectory }} (auto)

- + +
@@ -48,8 +57,8 @@

{{ $strings.MessageUploaderItemFailed }}

-
- +
+
@@ -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 = '' @@ -128,4 +179,4 @@ export default { } } } - \ No newline at end of file + diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 4fa8e394..2009b28d 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -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 = [] diff --git a/client/pages/account.vue b/client/pages/account.vue index c582c264..4bb68727 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -19,8 +19,8 @@
-

{{ $strings.HeaderChangePassword }}

-
+

{{ $strings.HeaderChangePassword }}

+ @@ -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: { diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index e645569e..9e028307 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -46,6 +46,9 @@ + +

+

@@ -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) diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index 09a9008b..547f5b05 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -14,6 +14,20 @@
+
+ + + info_outlined + + +
+ +
+
+

{{ error }}

@@ -61,9 +75,7 @@ - +
@@ -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) @@ -359,4 +411,4 @@ export default { window.removeEventListener('drop', this.drop) } } - \ No newline at end of file + diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index 711c526a..a16e6fa1 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -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 diff --git a/client/strings/cs.json b/client/strings/cs.json index ba027b92..6d39569e 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/da.json b/client/strings/da.json index 7ddc787e..fa28dd24 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/de.json b/client/strings/de.json index 78e64804..da8af1d8 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, den Sie entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen können. Die Verwendung eines Sternchens (*) 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", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 8e1f6ce6..02f9df05 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", @@ -738,4 +747,4 @@ "ToastSocketFailedToConnect": "Socket failed to connect", "ToastUserDeleteFailed": "Failed to delete user", "ToastUserDeleteSuccess": "User deleted" -} \ No newline at end of file +} diff --git a/client/strings/es.json b/client/strings/es.json index bbfbf267..47315301 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/fr.json b/client/strings/fr.json index 8999222b..f6efa428 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/gu.json b/client/strings/gu.json index 4318ad5a..0317e2f9 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/hi.json b/client/strings/hi.json index 69244330..eb4f074f 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/hr.json b/client/strings/hr.json index 2370dc33..eb7d27d8 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/it.json b/client/strings/it.json index 3f86bf2e..7e526721 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/lt.json b/client/strings/lt.json index ae54e8bf..9c4b9a63 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/nl.json b/client/strings/nl.json index d9399455..d4779abd 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/no.json b/client/strings/no.json index b4541229..511c8b86 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/pl.json b/client/strings/pl.json index 51587fed..b51084e9 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/ru.json b/client/strings/ru.json index c0fd0cf7..b48e0dbd 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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": "Пользователь", diff --git a/client/strings/sv.json b/client/strings/sv.json index 6bb0eec2..fde0cd87 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 2f20a42e..7c559489 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -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 /login?autoLaunch=0)", "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 audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) 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": "用户", diff --git a/server/Auth.js b/server/Auth.js index 57792177..0a282c9c 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -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 diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 267db5c8..db4110e0 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -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,8 +692,9 @@ class MiscController { } res.json({ + updated: hasUpdates, serverSettings: Database.serverSettings.toJSONForBrowser() }) } } -module.exports = new MiscController() \ No newline at end of file +module.exports = new MiscController() diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index c6579ab3..1af069f3 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -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))) } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index bf3db557..6e9d8456 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -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 } } diff --git a/server/providers/Audible.js b/server/providers/Audible.js index 31719eef..e46ed323 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -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}`) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 26578f57..ebad97db 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -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