mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merged parent
This commit is contained in:
		
						commit
						2f6756eddf
					
				| @ -338,9 +338,15 @@ export default { | ||||
|     libraryItemsAdded(libraryItems) { | ||||
|       console.log('libraryItems added', libraryItems) | ||||
| 
 | ||||
|       const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId) | ||||
|       if (!this.search && isThisLibrary) { | ||||
|         this.fetchCategories() | ||||
|       const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added') | ||||
|       if (!recentlyAddedShelf) return | ||||
| 
 | ||||
|       // Add new library item to the recently added shelf | ||||
|       for (const libraryItem of libraryItems) { | ||||
|         if (libraryItem.libraryId === this.currentLibraryId && !recentlyAddedShelf.entities.some((ent) => ent.id === libraryItem.id)) { | ||||
|           // Add to front of array | ||||
|           recentlyAddedShelf.entities.unshift(libraryItem) | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     libraryItemsUpdated(items) { | ||||
|  | ||||
| @ -127,7 +127,7 @@ export default { | ||||
|           skipMatchingMediaWithIsbn: false, | ||||
|           autoScanCronExpression: null, | ||||
|           hideSingleBookSeries: false, | ||||
|           metadataPrecedence: ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|           metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|  | ||||
| @ -19,9 +19,11 @@ | ||||
|         <li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10"> | ||||
|           <span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span> | ||||
|           <div class="text-center py-1 w-8 min-w-8"> | ||||
|             {{ source.include ? index + 1 : '' }} | ||||
|             {{ source.include ? getSourceIndex(source.id) : '' }} | ||||
|           </div> | ||||
|           <div class="flex-grow inline-flex justify-between px-4 py-3"> | ||||
|             {{ source.name }} <span v-if="source.include && (index === firstActiveSourceIndex || index === lastActiveSourceIndex)" class="px-2 italic font-semibold text-xs text-gray-400">{{ index === firstActiveSourceIndex ? $strings.LabelHighestPriority : $strings.LabelLowestPriority }}</span> | ||||
|           </div> | ||||
|           <div class="flex-grow px-4 py-3">{{ source.name }}</div> | ||||
|           <div class="px-2 opacity-100"> | ||||
|             <ui-toggle-switch v-model="source.include" :off-color="'error'" @input="includeToggled(source)" /> | ||||
|           </div> | ||||
| @ -64,6 +66,11 @@ export default { | ||||
|           name: 'Audio file meta tags', | ||||
|           include: true | ||||
|         }, | ||||
|         nfoFile: { | ||||
|           id: 'nfoFile', | ||||
|           name: 'NFO file', | ||||
|           include: true | ||||
|         }, | ||||
|         txtFiles: { | ||||
|           id: 'txtFiles', | ||||
|           name: 'desc.txt & reader.txt files', | ||||
| @ -92,20 +99,34 @@ export default { | ||||
|     }, | ||||
|     isBookLibrary() { | ||||
|       return this.mediaType === 'book' | ||||
|     }, | ||||
|     firstActiveSourceIndex() { | ||||
|       return this.metadataSourceMapped.findIndex((source) => source.include) | ||||
|     }, | ||||
|     lastActiveSourceIndex() { | ||||
|       return this.metadataSourceMapped.findLastIndex((source) => source.include) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getSourceIndex(source) { | ||||
|       const activeSources = (this.librarySettings.metadataPrecedence || []).map((s) => s).reverse() | ||||
|       return activeSources.findIndex((s) => s === source) + 1 | ||||
|     }, | ||||
|     resetToDefault() { | ||||
|       this.metadataSourceMapped = [] | ||||
|       for (const key in this.metadataSourceData) { | ||||
|         this.metadataSourceMapped.push({ ...this.metadataSourceData[key] }) | ||||
|       } | ||||
|       this.metadataSourceMapped.reverse() | ||||
| 
 | ||||
|       this.$emit('update', this.getLibraryData()) | ||||
|     }, | ||||
|     getLibraryData() { | ||||
|       const metadataSourceIds = this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) | ||||
|       metadataSourceIds.reverse() | ||||
|       return { | ||||
|         settings: { | ||||
|           metadataPrecedence: this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) | ||||
|           metadataPrecedence: metadataSourceIds | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @ -120,15 +141,16 @@ export default { | ||||
|     }, | ||||
|     init() { | ||||
|       const metadataPrecedence = this.librarySettings.metadataPrecedence || [] | ||||
| 
 | ||||
|       this.metadataSourceMapped = metadataPrecedence.map((source) => this.metadataSourceData[source]).filter((s) => s) | ||||
| 
 | ||||
|       for (const sourceKey in this.metadataSourceData) { | ||||
|         if (!metadataPrecedence.includes(sourceKey)) { | ||||
|           const unusedSourceData = { ...this.metadataSourceData[sourceKey], include: false } | ||||
|           this.metadataSourceMapped.push(unusedSourceData) | ||||
|           this.metadataSourceMapped.unshift(unusedSourceData) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.metadataSourceMapped.reverse() | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|  | ||||
							
								
								
									
										4
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,12 +1,12 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "2.5.0", | ||||
|   "version": "2.6.0", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "audiobookshelf-client", | ||||
|       "version": "2.5.0", | ||||
|       "version": "2.6.0", | ||||
|       "license": "ISC", | ||||
|       "dependencies": { | ||||
|         "@nuxtjs/axios": "^5.13.6", | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "2.5.0", | ||||
|   "version": "2.6.0", | ||||
|   "buildNumber": 1, | ||||
|   "description": "Self-hosted audiobook and podcast client", | ||||
|   "main": "index.js", | ||||
|  | ||||
| @ -1,16 +1,21 @@ | ||||
| <template> | ||||
|   <div> | ||||
|   <div id="authentication-settings"> | ||||
|     <app-settings-content :header-text="$strings.HeaderAuthentication"> | ||||
|       <div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25"> | ||||
|         <div class="flex items-center"> | ||||
|           <ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" /> | ||||
|           <p class="text-lg pl-4">Password Authentication</p> | ||||
|           <p class="text-lg pl-4">{{ $strings.HeaderPasswordAuthentication }}</p> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25"> | ||||
|         <div class="flex items-center"> | ||||
|           <ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" /> | ||||
|           <p class="text-lg pl-4">OpenID Connect Authentication</p> | ||||
|           <p class="text-lg pl-4">{{ $strings.HeaderOpenIDConnectAuthentication }}</p> | ||||
|           <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> | ||||
|             <a href="https://www.audiobookshelf.org/guides/oidc_authentication" target="_blank" class="inline-flex"> | ||||
|               <span class="material-icons text-xl w-5 text-gray-200">help_outline</span> | ||||
|             </a> | ||||
|           </ui-tooltip> | ||||
|         </div> | ||||
| 
 | ||||
|         <transition name="slide"> | ||||
| @ -41,25 +46,25 @@ | ||||
| 
 | ||||
|             <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> | ||||
| 
 | ||||
|             <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="'Button Text'" class="mb-2" /> | ||||
|             <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"> | ||||
|               <div class="w-44"> | ||||
|                 <ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" label="Match existing users by" :disabled="savingSettings" /> | ||||
|                 <ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" :label="$strings.LabelMatchExistingUsersBy" :disabled="savingSettings" /> | ||||
|               </div> | ||||
|               <p class="pl-4 text-sm text-gray-300 mt-5">Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider</p> | ||||
|               <p class="pl-4 text-sm text-gray-300 mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center py-4 px-1"> | ||||
|               <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> | ||||
|               <p id="auto-redirect-toggle" class="pl-4">Auto Launch</p> | ||||
|               <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the login page</p> | ||||
|               <p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoLaunch }}</p> | ||||
|               <p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelAutoLaunchDescription" /> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center py-4 px-1"> | ||||
|               <ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" /> | ||||
|               <p id="auto-register-toggle" class="pl-4">Auto Register</p> | ||||
|               <p class="pl-4 text-sm text-gray-300">Automatically create new users after logging in</p> | ||||
|               <p id="auto-register-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoRegister }}</p> | ||||
|               <p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p> | ||||
|             </div> | ||||
|           </div> | ||||
|         </transition> | ||||
| @ -227,3 +232,13 @@ export default { | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| #authentication-settings code { | ||||
|   font-size: 0.8rem; | ||||
|   border-radius: 6px; | ||||
|   background-color: rgb(82, 82, 82); | ||||
|   color: white; | ||||
|   padding: 2px 4px; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| </style> | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise", | ||||
|   "HeaderAudiobookTools": "Nástroje pro správu souborů audioknih", | ||||
|   "HeaderAudioTracks": "Zvukové stopy", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Zálohy", | ||||
|   "HeaderChangePassword": "Změnit heslo", | ||||
|   "HeaderChapters": "Kapitoly", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nový účet", | ||||
|   "HeaderNewLibrary": "Nová knihovna", | ||||
|   "HeaderNotifications": "Oznámení", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Otevřít RSS kanál", | ||||
|   "HeaderOtherFiles": "Ostatní soubory", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Oprávnění", | ||||
|   "HeaderPlayerQueue": "Fronta přehrávače", | ||||
|   "HeaderPlaylist": "Seznam skladeb", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Autor (příjmení a jméno)", | ||||
|   "LabelAuthors": "Autoři", | ||||
|   "LabelAutoDownloadEpisodes": "Automaticky stahovat epizody", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Zpět k uživateli", | ||||
|   "LabelBackupLocation": "Umístění zálohy", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.", | ||||
|   "LabelBitrate": "Datový tok", | ||||
|   "LabelBooks": "Knihy", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Změnit heslo", | ||||
|   "LabelChannels": "Kanály", | ||||
|   "LabelChapters": "Kapitoly", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Trvale smazat soubor", | ||||
|   "LabelHasEbook": "Obsahuje elektronickou knihu", | ||||
|   "LabelHasSupplementaryEbook": "Obsahuje doplňkovou elektronickou knihu", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Hostitel", | ||||
|   "LabelHour": "Hodina", | ||||
|   "LabelIcon": "Ikona", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Informace", | ||||
|   "LabelLogLevelWarn": "Varovat", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Přehrávač médií", | ||||
|   "LabelMediaType": "Typ média", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 je nejnižší priorita, 5 je nejvyšší priorita", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Poskytovatel metadat", | ||||
|   "LabelMetaTag": "Metaznačka", | ||||
|   "LabelMetaTags": "Metaznačky", | ||||
| @ -726,4 +738,4 @@ | ||||
|   "ToastSocketFailedToConnect": "Socket se nepodařilo připojit", | ||||
|   "ToastUserDeleteFailed": "Nepodařilo se smazat uživatele", | ||||
|   "ToastUserDeleteSuccess": "Uživatel smazán" | ||||
| } | ||||
| } | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger", | ||||
|   "HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer", | ||||
|   "HeaderAudioTracks": "Lydspor", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Sikkerhedskopier", | ||||
|   "HeaderChangePassword": "Skift Adgangskode", | ||||
|   "HeaderChapters": "Kapitler", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Ny Konto", | ||||
|   "HeaderNewLibrary": "Nyt Bibliotek", | ||||
|   "HeaderNotifications": "Meddelelser", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Åbn RSS Feed", | ||||
|   "HeaderOtherFiles": "Andre Filer", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Tilladelser", | ||||
|   "HeaderPlayerQueue": "Afspilningskø", | ||||
|   "HeaderPlaylist": "Afspilningsliste", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)", | ||||
|   "LabelAuthors": "Forfattere", | ||||
|   "LabelAutoDownloadEpisodes": "Auto Download Episoder", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Tilbage til Bruger", | ||||
|   "LabelBackupLocation": "Backup Placering", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Aktivér automatisk sikkerhedskopiering", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Bøger", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Ændre Adgangskode", | ||||
|   "LabelChannels": "Kanaler", | ||||
|   "LabelChapters": "Kapitler", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Permanent slet fil", | ||||
|   "LabelHasEbook": "Har e-bog", | ||||
|   "LabelHasSupplementaryEbook": "Har supplerende e-bog", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Vært", | ||||
|   "LabelHour": "Time", | ||||
|   "LabelIcon": "Ikon", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Information", | ||||
|   "LabelLogLevelWarn": "Advarsel", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Medieafspiller", | ||||
|   "LabelMediaType": "Medietype", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadataudbyder", | ||||
|   "LabelMetaTag": "Meta-tag", | ||||
|   "LabelMetaTags": "Meta-tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen", | ||||
|   "HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools", | ||||
|   "HeaderAudioTracks": "Audiodateien", | ||||
|   "HeaderAuthentication": "Authentifizierung", | ||||
|   "HeaderBackups": "Sicherungen", | ||||
|   "HeaderChangePassword": "Passwort ändern", | ||||
|   "HeaderChapters": "Kapitel", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Neues Konto", | ||||
|   "HeaderNewLibrary": "Neue Bibliothek", | ||||
|   "HeaderNotifications": "Benachrichtigungen", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung", | ||||
|   "HeaderOpenRSSFeed": "RSS-Feed öffnen", | ||||
|   "HeaderOtherFiles": "Sonstige Dateien", | ||||
|   "HeaderPasswordAuthentication": "Password Authentifizierung", | ||||
|   "HeaderPermissions": "Berechtigungen", | ||||
|   "HeaderPlayerQueue": "Spieler Warteschlange", | ||||
|   "HeaderPlaylist": "Wiedergabeliste", | ||||
| @ -181,11 +184,11 @@ | ||||
|   "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", | ||||
|   "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", | ||||
|   "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", | ||||
|   "LabelAdminUsersOnly": "Admin users only", | ||||
|   "LabelAdminUsersOnly": "Nur Admin Benutzer", | ||||
|   "LabelAll": "Alle", | ||||
|   "LabelAllUsers": "Alle Benutzer", | ||||
|   "LabelAllUsersExcludingGuests": "All users excluding guests", | ||||
|   "LabelAllUsersIncludingGuests": "All users including guests", | ||||
|   "LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen", | ||||
|   "LabelAllUsersIncludingGuests": "All Benutzer und Gäste", | ||||
|   "LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden", | ||||
|   "LabelAppend": "Anhängen", | ||||
|   "LabelAuthor": "Autor", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Autor (Nachname, Vorname)", | ||||
|   "LabelAuthors": "Autoren", | ||||
|   "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen", | ||||
|   "LabelAutoLaunch": "Automatischer Start", | ||||
|   "LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)", | ||||
|   "LabelAutoRegister": "Automatische Registrierung", | ||||
|   "LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Einloggen", | ||||
|   "LabelBackToUser": "Zurück zum Benutzer", | ||||
|   "LabelBackupLocation": "Backup-Ort", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn Sie bereits mehrere Sicherungen als die definierte max. Anzahl haben, sollten Sie diese manuell entfernen.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Bücher", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Passwort ändern", | ||||
|   "LabelChannels": "Kanäle", | ||||
|   "LabelChapters": "Kapitel", | ||||
| @ -232,7 +240,7 @@ | ||||
|   "LabelDeselectAll": "Alles abwählen", | ||||
|   "LabelDevice": "Gerät", | ||||
|   "LabelDeviceInfo": "Geräteinformationen", | ||||
|   "LabelDeviceIsAvailableTo": "Device is available to...", | ||||
|   "LabelDeviceIsAvailableTo": "Dem Geärt ist es möglich zu ...", | ||||
|   "LabelDirectory": "Verzeichnis", | ||||
|   "LabelDiscFromFilename": "CD aus dem Dateinamen", | ||||
|   "LabelDiscFromMetadata": "CD aus den Metadaten", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Datei dauerhaft löschen", | ||||
|   "LabelHasEbook": "mit E-Book", | ||||
|   "LabelHasSupplementaryEbook": "mit zusätlichem E-Book", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Stunde", | ||||
|   "LabelIcon": "Symbol", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Informationen", | ||||
|   "LabelLogLevelWarn": "Warnungen", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit", | ||||
|   "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID von Ihrem SSO-Anbieter zugeordnet", | ||||
|   "LabelMediaPlayer": "Mediaplayer", | ||||
|   "LabelMediaType": "Medientyp", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadatenanbieter", | ||||
|   "LabelMetaTag": "Meta Schlagwort", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
| @ -398,7 +410,7 @@ | ||||
|   "LabelSeason": "Staffel", | ||||
|   "LabelSelectAllEpisodes": "Alle Episoden auswählen", | ||||
|   "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", | ||||
|   "LabelSelectUsers": "Select users", | ||||
|   "LabelSelectUsers": "Benutzer auswählen", | ||||
|   "LabelSendEbookToDevice": "E-Book senden an...", | ||||
|   "LabelSequence": "Reihenfolge", | ||||
|   "LabelSeries": "Serien", | ||||
|  | ||||
| @ -132,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "New Account", | ||||
|   "HeaderNewLibrary": "New Library", | ||||
|   "HeaderNotifications": "Notifications", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Open RSS Feed", | ||||
|   "HeaderOtherFiles": "Other Files", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Permissions", | ||||
|   "HeaderPlayerQueue": "Player Queue", | ||||
|   "HeaderPlaylist": "Playlist", | ||||
| @ -195,6 +197,10 @@ | ||||
|   "LabelAuthors": "Authors", | ||||
|   "LabelAutoDownloadEpisodes": "Auto Download Episodes", | ||||
|   "LabelAutoFetchMetadata": "Auto Fetch Metadata", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Back to User", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", | ||||
| @ -205,6 +211,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Books", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Change Password", | ||||
|   "LabelChannels": "Channels", | ||||
|   "LabelChapters": "Chapters", | ||||
| @ -278,6 +285,7 @@ | ||||
|   "LabelHardDeleteFile": "Hard delete file", | ||||
|   "LabelHasEbook": "Has ebook", | ||||
|   "LabelHasSupplementaryEbook": "Has supplementary ebook", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hour", | ||||
|   "LabelIcon": "Icon", | ||||
| @ -319,9 +327,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Media Player", | ||||
|   "LabelMediaType": "Media Type", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadata Provider", | ||||
|   "LabelMetaTag": "Meta Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise", | ||||
|   "HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro", | ||||
|   "HeaderAudioTracks": "Pistas de Audio", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Respaldos", | ||||
|   "HeaderChangePassword": "Cambiar Contraseña", | ||||
|   "HeaderChapters": "Capítulos", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nueva Cuenta", | ||||
|   "HeaderNewLibrary": "Nueva Biblioteca", | ||||
|   "HeaderNotifications": "Notificaciones", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Abrir fuente RSS", | ||||
|   "HeaderOtherFiles": "Otros Archivos", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Permisos", | ||||
|   "HeaderPlayerQueue": "Fila del Reproductor", | ||||
|   "HeaderPlaylist": "Lista de Reproducción", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Autor (Apellido, Nombre)", | ||||
|   "LabelAuthors": "Autores", | ||||
|   "LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Regresar a Usuario", | ||||
|   "LabelBackupLocation": "Ubicación del Respaldo", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Libros", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Cambiar Contraseña", | ||||
|   "LabelChannels": "Canales", | ||||
|   "LabelChapters": "Capítulos", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Eliminar Definitivamente", | ||||
|   "LabelHasEbook": "Tiene Ebook", | ||||
|   "LabelHasSupplementaryEbook": "Tiene Ebook Suplementario", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hora", | ||||
|   "LabelIcon": "Icono", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Información", | ||||
|   "LabelLogLevelWarn": "Advertencia", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Reproductor de Medios", | ||||
|   "LabelMediaType": "Tipo de Multimedia", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Proveedor de Metadata", | ||||
|   "LabelMetaTag": "Meta Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise", | ||||
|   "HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook", | ||||
|   "HeaderAudioTracks": "Pistes audio", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Sauvegardes", | ||||
|   "HeaderChangePassword": "Modifier le mot de passe", | ||||
|   "HeaderChapters": "Chapitres", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nouveau compte", | ||||
|   "HeaderNewLibrary": "Nouvelle bibliothèque", | ||||
|   "HeaderNotifications": "Notifications", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Ouvrir Flux RSS", | ||||
|   "HeaderOtherFiles": "Autres fichiers", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Permissions", | ||||
|   "HeaderPlayerQueue": "Liste d’écoute", | ||||
|   "HeaderPlaylist": "Liste de lecture", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Auteur (Nom, Prénom)", | ||||
|   "LabelAuthors": "Auteurs", | ||||
|   "LabelAutoDownloadEpisodes": "Téléchargement automatique d’épisode", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Revenir à l’Utilisateur", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Livres", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Modifier le mot de passe", | ||||
|   "LabelChannels": "Canaux", | ||||
|   "LabelChapters": "Chapitres", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Suppression du fichier", | ||||
|   "LabelHasEbook": "Dispose d’un livre numérique", | ||||
|   "LabelHasSupplementaryEbook": "Dispose d’un livre numérique supplémentaire", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Hôte", | ||||
|   "LabelHour": "Heure", | ||||
|   "LabelIcon": "Icone", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Lecteur multimédia", | ||||
|   "LabelMediaType": "Type de média", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Fournisseur de métadonnées", | ||||
|   "LabelMetaTag": "Etiquette de métadonnée", | ||||
|   "LabelMetaTags": "Etiquettes de métadonnée", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ", | ||||
|   "HeaderAudiobookTools": "Audiobook File Management Tools", | ||||
|   "HeaderAudioTracks": "Audio Tracks", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Backups", | ||||
|   "HeaderChangePassword": "Change Password", | ||||
|   "HeaderChapters": "Chapters", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "New Account", | ||||
|   "HeaderNewLibrary": "New Library", | ||||
|   "HeaderNotifications": "Notifications", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Open RSS Feed", | ||||
|   "HeaderOtherFiles": "Other Files", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Permissions", | ||||
|   "HeaderPlayerQueue": "Player Queue", | ||||
|   "HeaderPlaylist": "Playlist", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Author (Last, First)", | ||||
|   "LabelAuthors": "Authors", | ||||
|   "LabelAutoDownloadEpisodes": "Auto Download Episodes", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Back to User", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Books", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Change Password", | ||||
|   "LabelChannels": "Channels", | ||||
|   "LabelChapters": "Chapters", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Hard delete file", | ||||
|   "LabelHasEbook": "Has ebook", | ||||
|   "LabelHasSupplementaryEbook": "Has supplementary ebook", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hour", | ||||
|   "LabelIcon": "Icon", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Media Player", | ||||
|   "LabelMediaType": "Media Type", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadata Provider", | ||||
|   "LabelMetaTag": "Meta Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स", | ||||
|   "HeaderAudiobookTools": "Audiobook File Management Tools", | ||||
|   "HeaderAudioTracks": "Audio Tracks", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Backups", | ||||
|   "HeaderChangePassword": "Change Password", | ||||
|   "HeaderChapters": "Chapters", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "New Account", | ||||
|   "HeaderNewLibrary": "New Library", | ||||
|   "HeaderNotifications": "Notifications", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Open RSS Feed", | ||||
|   "HeaderOtherFiles": "Other Files", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Permissions", | ||||
|   "HeaderPlayerQueue": "Player Queue", | ||||
|   "HeaderPlaylist": "Playlist", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Author (Last, First)", | ||||
|   "LabelAuthors": "Authors", | ||||
|   "LabelAutoDownloadEpisodes": "Auto Download Episodes", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Back to User", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Books", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Change Password", | ||||
|   "LabelChannels": "Channels", | ||||
|   "LabelChapters": "Chapters", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Hard delete file", | ||||
|   "LabelHasEbook": "Has ebook", | ||||
|   "LabelHasSupplementaryEbook": "Has supplementary ebook", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hour", | ||||
|   "LabelIcon": "Icon", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Media Player", | ||||
|   "LabelMediaType": "Media Type", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadata Provider", | ||||
|   "LabelMetaTag": "Meta Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise Notification Settings", | ||||
|   "HeaderAudiobookTools": "Audiobook File Management alati", | ||||
|   "HeaderAudioTracks": "Audio Tracks", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Backups", | ||||
|   "HeaderChangePassword": "Promijeni lozinku", | ||||
|   "HeaderChapters": "Poglavlja", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Novi korisnički račun", | ||||
|   "HeaderNewLibrary": "Nova biblioteka", | ||||
|   "HeaderNotifications": "Obavijesti", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Otvori RSS Feed", | ||||
|   "HeaderOtherFiles": "Druge datoteke", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Dozvole", | ||||
|   "HeaderPlayerQueue": "Player Queue", | ||||
|   "HeaderPlaylist": "Playlist", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Author (Last, First)", | ||||
|   "LabelAuthors": "Autori", | ||||
|   "LabelAutoDownloadEpisodes": "Automatski preuzmi epizode", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Nazad k korisniku", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Uključi automatski backup", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Samo 1 backup će biti odjednom obrisan. Ako koristite više njih, morati ćete ih ručno ukloniti.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Knjige", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Promijeni lozinku", | ||||
|   "LabelChannels": "Channels", | ||||
|   "LabelChapters": "Chapters", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Obriši datoteku zauvijek", | ||||
|   "LabelHasEbook": "Has ebook", | ||||
|   "LabelHasSupplementaryEbook": "Has supplementary ebook", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Sat", | ||||
|   "LabelIcon": "Ikona", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Media Player", | ||||
|   "LabelMediaType": "Media Type", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Poslužitelj metapodataka ", | ||||
|   "LabelMetaTag": "Meta Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica", | ||||
|   "HeaderAudiobookTools": "Utilità Audiobook File Management", | ||||
|   "HeaderAudioTracks": "Tracce Audio", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Backup", | ||||
|   "HeaderChangePassword": "Cambia Password", | ||||
|   "HeaderChapters": "Capitoli", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nuovo Account", | ||||
|   "HeaderNewLibrary": "Nuova Libreria", | ||||
|   "HeaderNotifications": "Notifiche", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Apri RSS Feed", | ||||
|   "HeaderOtherFiles": "Altri File", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Permessi", | ||||
|   "HeaderPlayerQueue": "Coda Riproduzione", | ||||
|   "HeaderPlaylist": "Playlist", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Autori (Per Cognome)", | ||||
|   "LabelAuthors": "Autori", | ||||
|   "LabelAutoDownloadEpisodes": "Auto Download Episodi", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Torna a Utenti", | ||||
|   "LabelBackupLocation": "Percorso del Backup", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Libri", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Cambia Password", | ||||
|   "LabelChannels": "Canali", | ||||
|   "LabelChapters": "Capitoli", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Elimina Definitivamente", | ||||
|   "LabelHasEbook": "Un ebook", | ||||
|   "LabelHasSupplementaryEbook": "Un ebook Supplementare", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Ora", | ||||
|   "LabelIcon": "Icona", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Allarme", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Media Player", | ||||
|   "LabelMediaType": "Tipo Media", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 e bassa priorità, 5 è alta priorità", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadata Provider", | ||||
|   "LabelMetaTag": "Meta Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
| @ -726,4 +738,4 @@ | ||||
|   "ToastSocketFailedToConnect": "Socket non riesce a connettersi", | ||||
|   "ToastUserDeleteFailed": "Errore eliminazione utente", | ||||
|   "ToastUserDeleteSuccess": "Utente eliminato" | ||||
| } | ||||
| } | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai", | ||||
|   "HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai", | ||||
|   "HeaderAudioTracks": "Garso takeliai", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Atsarginės kopijos", | ||||
|   "HeaderChangePassword": "Pakeisti slaptažodį", | ||||
|   "HeaderChapters": "Skyriai", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nauja paskyra", | ||||
|   "HeaderNewLibrary": "Nauja biblioteka", | ||||
|   "HeaderNotifications": "Pranešimai", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Atidaryti RSS srautą", | ||||
|   "HeaderOtherFiles": "Kiti failai", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Leidimai", | ||||
|   "HeaderPlayerQueue": "Grotuvo eilė", | ||||
|   "HeaderPlaylist": "Grojaraštis", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)", | ||||
|   "LabelAuthors": "Autoriai", | ||||
|   "LabelAutoDownloadEpisodes": "Automatiškai atsisiųsti epizodus", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Grįžti į naudotoją", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.", | ||||
|   "LabelBitrate": "Bitų sparta", | ||||
|   "LabelBooks": "Knygos", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Pakeisti slaptažodį", | ||||
|   "LabelChannels": "Kanalai", | ||||
|   "LabelChapters": "Skyriai", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Galutinai ištrinti failą", | ||||
|   "LabelHasEbook": "Turi e-knygą", | ||||
|   "LabelHasSupplementaryEbook": "Turi papildomą e-knygą", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Serveris", | ||||
|   "LabelHour": "Valanda", | ||||
|   "LabelIcon": "Piktograma", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Grotuvas", | ||||
|   "LabelMediaType": "Medijos tipas", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metaduomenų tiekėjas", | ||||
|   "LabelMetaTag": "Meta žymė", | ||||
|   "LabelMetaTags": "Meta žymos", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen", | ||||
|   "HeaderAudiobookTools": "Audioboekbestandbeheer tools", | ||||
|   "HeaderAudioTracks": "Audiotracks", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Back-ups", | ||||
|   "HeaderChangePassword": "Wachtwoord wijzigen", | ||||
|   "HeaderChapters": "Hoofdstukken", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nieuwe account", | ||||
|   "HeaderNewLibrary": "Nieuwe bibliotheek", | ||||
|   "HeaderNotifications": "Notificaties", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Open RSS-feed", | ||||
|   "HeaderOtherFiles": "Andere bestanden", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Toestemmingen", | ||||
|   "HeaderPlayerQueue": "Afspeelwachtrij", | ||||
|   "HeaderPlaylist": "Afspeellijst", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)", | ||||
|   "LabelAuthors": "Auteurs", | ||||
|   "LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Terug naar gebruiker", | ||||
|   "LabelBackupLocation": "Back-up locatie", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Automatische back-ups inschakelen", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Er wordt slechts 1 back-up per keer verwijderd, dus als je reeds meer back-ups dan dit hebt moet je ze handmatig verwijderen.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Boeken", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Wachtwoord wijzigen", | ||||
|   "LabelChannels": "Kanalen", | ||||
|   "LabelChapters": "Hoofdstukken", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Hard-delete bestand", | ||||
|   "LabelHasEbook": "Heeft ebook", | ||||
|   "LabelHasSupplementaryEbook": "Heeft supplementair ebook", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Uur", | ||||
|   "LabelIcon": "Icoon", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Waarschuwing", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Mediaspeler", | ||||
|   "LabelMediaType": "Mediatype", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadatabron", | ||||
|   "LabelMetaTag": "Meta-tag", | ||||
|   "LabelMetaTags": "Meta-tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise notifikasjonsinstillinger", | ||||
|   "HeaderAudiobookTools": "Lydbok Filbehandlingsverktøy", | ||||
|   "HeaderAudioTracks": "Lydspor", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Sikkerhetskopier", | ||||
|   "HeaderChangePassword": "Bytt passord", | ||||
|   "HeaderChapters": "Kapittel", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Ny konto", | ||||
|   "HeaderNewLibrary": "Ny bibliotek", | ||||
|   "HeaderNotifications": "Notifikasjoner", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Åpne RSS Feed", | ||||
|   "HeaderOtherFiles": "Andre filer", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Rettigheter", | ||||
|   "HeaderPlayerQueue": "Spiller kø", | ||||
|   "HeaderPlaylist": "Spilleliste", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Forfatter (Etternavn Fornavn)", | ||||
|   "LabelAuthors": "Forfattere", | ||||
|   "LabelAutoDownloadEpisodes": "Last ned episoder automatisk", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Tilbake til bruker", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Aktiver automatisk sikkerhetskopi", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhetskopi vil bli fjernet om gangen, hvis du allerede har flere sikkerhetskopier enn dette bør du fjerne de manuelt.", | ||||
|   "LabelBitrate": "Bithastighet", | ||||
|   "LabelBooks": "Bøker", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Endre passord", | ||||
|   "LabelChannels": "Kanaler", | ||||
|   "LabelChapters": "Kapitler", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Tving sletting av fil", | ||||
|   "LabelHasEbook": "Har ebok", | ||||
|   "LabelHasSupplementaryEbook": "Har supplerende ebok", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Tjener", | ||||
|   "LabelHour": "Time", | ||||
|   "LabelIcon": "Ikon", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Mediespiller", | ||||
|   "LabelMediaType": "Medie type", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadata Leverandør", | ||||
|   "LabelMetaTag": "Meta Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise", | ||||
|   "HeaderAudiobookTools": "Narzędzia do zarządzania audiobookami", | ||||
|   "HeaderAudioTracks": "Ścieżki audio", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Kopie zapasowe", | ||||
|   "HeaderChangePassword": "Zmień hasło", | ||||
|   "HeaderChapters": "Rozdziały", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nowe konto", | ||||
|   "HeaderNewLibrary": "Nowa biblioteka", | ||||
|   "HeaderNotifications": "Powiadomienia", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Utwórz kanał RSS", | ||||
|   "HeaderOtherFiles": "Inne pliki", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Uprawnienia", | ||||
|   "HeaderPlayerQueue": "Player Queue", | ||||
|   "HeaderPlaylist": "Playlist", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Author (Malejąco)", | ||||
|   "LabelAuthors": "Autorzy", | ||||
|   "LabelAutoDownloadEpisodes": "Automatyczne pobieranie odcinków", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Powrót", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.", | ||||
|   "LabelBitrate": "Bitrate", | ||||
|   "LabelBooks": "Książki", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Zmień hasło", | ||||
|   "LabelChannels": "Channels", | ||||
|   "LabelChapters": "Chapters", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Usuń trwale plik", | ||||
|   "LabelHasEbook": "Has ebook", | ||||
|   "LabelHasSupplementaryEbook": "Has supplementary ebook", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Godzina", | ||||
|   "LabelIcon": "Ikona", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Informacja", | ||||
|   "LabelLogLevelWarn": "Ostrzeżenie", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Odtwarzacz", | ||||
|   "LabelMediaType": "Typ mediów", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Dostawca metadanych", | ||||
|   "LabelMetaTag": "Tag", | ||||
|   "LabelMetaTags": "Meta Tags", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Настройки оповещений", | ||||
|   "HeaderAudiobookTools": "Инструменты файлов аудиокниг", | ||||
|   "HeaderAudioTracks": "Аудио треки", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Бэкапы", | ||||
|   "HeaderChangePassword": "Изменить пароль", | ||||
|   "HeaderChapters": "Главы", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Новая учетная запись", | ||||
|   "HeaderNewLibrary": "Новая библиотека", | ||||
|   "HeaderNotifications": "Уведомления", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Открыть RSS-канал", | ||||
|   "HeaderOtherFiles": "Другие файлы", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Разрешения", | ||||
|   "HeaderPlayerQueue": "Очередь воспроизведения", | ||||
|   "HeaderPlaylist": "Плейлист", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Автор (Фамилия, Имя)", | ||||
|   "LabelAuthors": "Авторы", | ||||
|   "LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Назад к пользователю", | ||||
|   "LabelBackupLocation": "Backup Location", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.", | ||||
|   "LabelBitrate": "Битрейт", | ||||
|   "LabelBooks": "Книги", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Изменить пароль", | ||||
|   "LabelChannels": "Каналы", | ||||
|   "LabelChapters": "Главы", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Жесткое удаление файла", | ||||
|   "LabelHasEbook": "Есть e-книга", | ||||
|   "LabelHasSupplementaryEbook": "Есть дополнительная e-книга", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Хост", | ||||
|   "LabelHour": "Часы", | ||||
|   "LabelIcon": "Иконка", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Info", | ||||
|   "LabelLogLevelWarn": "Warn", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Медиа проигрыватель", | ||||
|   "LabelMediaType": "Тип медиа", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Провайдер", | ||||
|   "LabelMetaTag": "Мета тег", | ||||
|   "LabelMetaTags": "Мета теги", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar", | ||||
|   "HeaderAudiobookTools": "Ljudbokshantering", | ||||
|   "HeaderAudioTracks": "Ljudspår", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "Säkerhetskopior", | ||||
|   "HeaderChangePassword": "Ändra lösenord", | ||||
|   "HeaderChapters": "Kapitel", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "Nytt konto", | ||||
|   "HeaderNewLibrary": "Nytt bibliotek", | ||||
|   "HeaderNotifications": "Meddelanden", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "Öppna RSS-flöde", | ||||
|   "HeaderOtherFiles": "Andra filer", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "Behörigheter", | ||||
|   "HeaderPlayerQueue": "Spelarkö", | ||||
|   "HeaderPlaylist": "Spellista", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)", | ||||
|   "LabelAuthors": "Författare", | ||||
|   "LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "Tillbaka till användaren", | ||||
|   "LabelBackupLocation": "Säkerhetskopia Plats", | ||||
|   "LabelBackupsEnableAutomaticBackups": "Aktivera automatiska säkerhetskopior", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "Endast en säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än detta bör du ta bort dem manuellt.", | ||||
|   "LabelBitrate": "Bitfrekvens", | ||||
|   "LabelBooks": "Böcker", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "Ändra lösenord", | ||||
|   "LabelChannels": "Kanaler", | ||||
|   "LabelChapters": "Kapitel", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "Hård radering av fil", | ||||
|   "LabelHasEbook": "Har e-bok", | ||||
|   "LabelHasSupplementaryEbook": "Har kompletterande e-bok", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "Värd", | ||||
|   "LabelHour": "Timme", | ||||
|   "LabelIcon": "Ikon", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "Felsökningsnivå: Information", | ||||
|   "LabelLogLevelWarn": "Felsökningsnivå: Varning", | ||||
|   "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "Mediaspelare", | ||||
|   "LabelMediaType": "Mediatyp", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 är lägsta prioritet, 5 är högsta prioritet", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "Metadataleverantör", | ||||
|   "LabelMetaTag": "Metamärke", | ||||
|   "LabelMetaTags": "Metamärken", | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|   "HeaderAppriseNotificationSettings": "测试通知设置", | ||||
|   "HeaderAudiobookTools": "有声读物文件管理工具", | ||||
|   "HeaderAudioTracks": "音轨", | ||||
|   "HeaderAuthentication": "Authentication", | ||||
|   "HeaderBackups": "备份", | ||||
|   "HeaderChangePassword": "更改密码", | ||||
|   "HeaderChapters": "章节", | ||||
| @ -131,8 +132,10 @@ | ||||
|   "HeaderNewAccount": "新建帐户", | ||||
|   "HeaderNewLibrary": "新建媒体库", | ||||
|   "HeaderNotifications": "通知", | ||||
|   "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", | ||||
|   "HeaderOpenRSSFeed": "打开 RSS 源", | ||||
|   "HeaderOtherFiles": "其他文件", | ||||
|   "HeaderPasswordAuthentication": "Password Authentication", | ||||
|   "HeaderPermissions": "权限", | ||||
|   "HeaderPlayerQueue": "播放队列", | ||||
|   "HeaderPlaylist": "播放列表", | ||||
| @ -193,6 +196,10 @@ | ||||
|   "LabelAuthorLastFirst": "作者 (名, 姓)", | ||||
|   "LabelAuthors": "作者", | ||||
|   "LabelAutoDownloadEpisodes": "自动下载剧集", | ||||
|   "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", | ||||
|   "LabelAutoRegisterDescription": "Automatically create new users after logging in", | ||||
|   "LabelBackToUser": "返回到用户", | ||||
|   "LabelBackupLocation": "备份位置", | ||||
|   "LabelBackupsEnableAutomaticBackups": "启用自动备份", | ||||
| @ -203,6 +210,7 @@ | ||||
|   "LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.", | ||||
|   "LabelBitrate": "比特率", | ||||
|   "LabelBooks": "图书", | ||||
|   "LabelButtonText": "Button Text", | ||||
|   "LabelChangePassword": "修改密码", | ||||
|   "LabelChannels": "声道", | ||||
|   "LabelChapters": "章节", | ||||
| @ -275,6 +283,7 @@ | ||||
|   "LabelHardDeleteFile": "完全删除文件", | ||||
|   "LabelHasEbook": "有电子书", | ||||
|   "LabelHasSupplementaryEbook": "有补充电子书", | ||||
|   "LabelHighestPriority": "Highest priority", | ||||
|   "LabelHost": "主机", | ||||
|   "LabelHour": "小时", | ||||
|   "LabelIcon": "图标", | ||||
| @ -316,9 +325,12 @@ | ||||
|   "LabelLogLevelInfo": "信息", | ||||
|   "LabelLogLevelWarn": "警告", | ||||
|   "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", | ||||
|   "LabelLowestPriority": "Lowest Priority", | ||||
|   "LabelMatchExistingUsersBy": "Match existing users by", | ||||
|   "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", | ||||
|   "LabelMediaPlayer": "媒体播放器", | ||||
|   "LabelMediaType": "媒体类型", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", | ||||
|   "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", | ||||
|   "LabelMetadataProvider": "元数据提供者", | ||||
|   "LabelMetaTag": "元数据标签", | ||||
|   "LabelMetaTags": "元标签", | ||||
|  | ||||
							
								
								
									
										214
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										214
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,12 +1,12 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "2.5.0", | ||||
|   "version": "2.6.0", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "audiobookshelf", | ||||
|       "version": "2.5.0", | ||||
|       "version": "2.6.0", | ||||
|       "license": "GPL-3.0", | ||||
|       "dependencies": { | ||||
|         "axios": "^0.27.2", | ||||
| @ -15,6 +15,7 @@ | ||||
|         "express-session": "^1.17.3", | ||||
|         "graceful-fs": "^4.2.10", | ||||
|         "htmlparser2": "^8.0.1", | ||||
|         "lru-cache": "^10.0.3", | ||||
|         "node-tone": "^1.0.1", | ||||
|         "nodemailer": "^6.9.2", | ||||
|         "openid-client": "^5.6.1", | ||||
| @ -603,6 +604,17 @@ | ||||
|         "node-pre-gyp": "bin/node-pre-gyp" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { | ||||
|       "version": "5.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", | ||||
| @ -641,6 +653,18 @@ | ||||
|         "semver": "^7.3.5" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@npmcli/fs/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@npmcli/fs/node_modules/semver": { | ||||
|       "version": "7.5.3", | ||||
|       "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", | ||||
| @ -1126,6 +1150,18 @@ | ||||
|         "node": ">= 10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/cacache/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/caching-transform": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", | ||||
| @ -2619,6 +2655,18 @@ | ||||
|         "node": ">=8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/istanbul-lib-report/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/istanbul-lib-report/node_modules/make-dir": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", | ||||
| @ -2783,6 +2831,17 @@ | ||||
|         "npm": ">=6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jsonwebtoken/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jsonwebtoken/node_modules/ms": { | ||||
|       "version": "2.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | ||||
| @ -2917,14 +2976,11 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "version": "10.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz", | ||||
|       "integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==", | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|         "node": "14 || >=16.14" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/make-dir": { | ||||
| @ -2976,6 +3032,18 @@ | ||||
|         "node": ">= 10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/make-fetch-happen/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/media-typer": { | ||||
|       "version": "0.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", | ||||
| @ -3552,6 +3620,18 @@ | ||||
|         "node": "^12.13.0 || ^14.15.0 || >=16.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/node-gyp/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/node-gyp/node_modules/nopt": { | ||||
|       "version": "5.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", | ||||
| @ -3841,6 +3921,17 @@ | ||||
|         "url": "https://github.com/sponsors/panva" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/openid-client/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/p-limit": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", | ||||
| @ -4386,6 +4477,17 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/sequelize/node_modules/lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "dependencies": { | ||||
|         "yallist": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/sequelize/node_modules/ms": { | ||||
|       "version": "2.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", | ||||
| @ -5777,6 +5879,14 @@ | ||||
|         "tar": "^6.1.11" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "nopt": { | ||||
|           "version": "5.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", | ||||
| @ -5805,6 +5915,15 @@ | ||||
|         "semver": "^7.3.5" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "semver": { | ||||
|           "version": "7.5.3", | ||||
|           "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", | ||||
| @ -6192,6 +6311,17 @@ | ||||
|         "ssri": "^8.0.1", | ||||
|         "tar": "^6.0.2", | ||||
|         "unique-filename": "^1.1.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "caching-transform": { | ||||
| @ -7291,6 +7421,15 @@ | ||||
|           "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "make-dir": { | ||||
|           "version": "4.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", | ||||
| @ -7408,6 +7547,14 @@ | ||||
|         "semver": "^7.5.4" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "ms": { | ||||
|           "version": "2.1.3", | ||||
|           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | ||||
| @ -7529,12 +7676,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "lru-cache": { | ||||
|       "version": "6.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|       "requires": { | ||||
|         "yallist": "^4.0.0" | ||||
|       } | ||||
|       "version": "10.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz", | ||||
|       "integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==" | ||||
|     }, | ||||
|     "make-dir": { | ||||
|       "version": "3.1.0", | ||||
| @ -7573,6 +7717,17 @@ | ||||
|         "promise-retry": "^2.0.1", | ||||
|         "socks-proxy-agent": "^6.0.0", | ||||
|         "ssri": "^8.0.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "media-typer": { | ||||
| @ -8000,6 +8155,15 @@ | ||||
|             "wide-align": "^1.1.5" | ||||
|           } | ||||
|         }, | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "nopt": { | ||||
|           "version": "5.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", | ||||
| @ -8220,6 +8384,16 @@ | ||||
|         "lru-cache": "^6.0.0", | ||||
|         "object-hash": "^2.2.0", | ||||
|         "oidc-token-hash": "^5.0.3" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "p-limit": { | ||||
| @ -8588,6 +8762,14 @@ | ||||
|             "ms": "2.1.2" | ||||
|           } | ||||
|         }, | ||||
|         "lru-cache": { | ||||
|           "version": "6.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|           "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", | ||||
|           "requires": { | ||||
|             "yallist": "^4.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "ms": { | ||||
|           "version": "2.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", | ||||
| @ -9305,4 +9487,4 @@ | ||||
|       "dev": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "2.5.0", | ||||
|   "version": "2.6.0", | ||||
|   "buildNumber": 1, | ||||
|   "description": "Self-hosted audiobook and podcast server", | ||||
|   "main": "index.js", | ||||
| @ -42,6 +42,7 @@ | ||||
|     "express-session": "^1.17.3", | ||||
|     "graceful-fs": "^4.2.10", | ||||
|     "htmlparser2": "^8.0.1", | ||||
|     "lru-cache": "^10.0.3", | ||||
|     "node-tone": "^1.0.1", | ||||
|     "nodemailer": "^6.9.2", | ||||
|     "openid-client": "^5.6.1", | ||||
| @ -60,4 +61,4 @@ | ||||
|     "nyc": "^15.1.0", | ||||
|     "sinon": "^17.0.1" | ||||
|   } | ||||
| } | ||||
| } | ||||
| @ -187,7 +187,7 @@ class Auth { | ||||
|    * @param {import('express').Response} res | ||||
|    */ | ||||
|   paramsToCookies(req, res) { | ||||
|     if (req.query.isRest?.toLowerCase() == "true") { | ||||
|     if (req.query.isRest?.toLowerCase() == 'true') { | ||||
|       // store the isRest flag to the is_rest cookie 
 | ||||
|       res.cookie('is_rest', req.query.isRest.toLowerCase(), { | ||||
|         maxAge: 120000, // 2 min
 | ||||
| @ -195,7 +195,7 @@ class Auth { | ||||
|       }) | ||||
|     } else { | ||||
|       // no isRest-flag set -> set is_rest cookie to false
 | ||||
|       res.cookie('is_rest', "false", { | ||||
|       res.cookie('is_rest', 'false', { | ||||
|         maxAge: 120000, // 2 min
 | ||||
|         httpOnly: true | ||||
|       }) | ||||
| @ -323,7 +323,8 @@ class Auth { | ||||
| 
 | ||||
|         req.session[sessionKey] = { | ||||
|           ...req.session[sessionKey], | ||||
|           ...pick(params, 'nonce', 'state', 'max_age', 'response_type') | ||||
|           ...pick(params, 'nonce', 'state', 'max_age', 'response_type'), | ||||
|           mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later
 | ||||
|         } | ||||
| 
 | ||||
|         // Now get the URL to direct to
 | ||||
| @ -565,6 +566,69 @@ class Auth { | ||||
|       Source: global.Source | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    *  | ||||
|    * @param {string} password  | ||||
|    * @param {*} user  | ||||
|    * @returns {boolean} | ||||
|    */ | ||||
|   comparePassword(password, user) { | ||||
|     if (user.type === 'root' && !password && !user.pash) return true | ||||
|     if (!password || !user.pash) return false | ||||
|     return bcrypt.compare(password, user.pash) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * User changes their password from request | ||||
|    *  | ||||
|    * @param {import('express').Request} req  | ||||
|    * @param {import('express').Response} res  | ||||
|    */ | ||||
|   async userChangePassword(req, res) { | ||||
|     let { password, newPassword } = req.body | ||||
|     newPassword = newPassword || '' | ||||
|     const matchingUser = req.user | ||||
| 
 | ||||
|     // Only root can have an empty password
 | ||||
|     if (matchingUser.type !== 'root' && !newPassword) { | ||||
|       return res.json({ | ||||
|         error: 'Invalid new password - Only root can have an empty password' | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     // Check password match
 | ||||
|     const compare = await this.comparePassword(password, matchingUser) | ||||
|     if (!compare) { | ||||
|       return res.json({ | ||||
|         error: 'Invalid password' | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     let pw = '' | ||||
|     if (newPassword) { | ||||
|       pw = await this.hashPass(newPassword) | ||||
|       if (!pw) { | ||||
|         return res.json({ | ||||
|           error: 'Hash failed' | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     matchingUser.pash = pw | ||||
| 
 | ||||
|     const success = await Database.updateUser(matchingUser) | ||||
|     if (success) { | ||||
|       Logger.info(`[Auth] User "${matchingUser.username}" changed password`) | ||||
|       res.json({ | ||||
|         success: true | ||||
|       }) | ||||
|     } else { | ||||
|       res.json({ | ||||
|         error: 'Unknown error' | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports = Auth | ||||
| @ -5,6 +5,7 @@ class Logger { | ||||
|   constructor() { | ||||
|     this.isDev = process.env.NODE_ENV !== 'production' | ||||
|     this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE | ||||
|     this.hideDevLogs = process.env.HIDE_DEV_LOGS === undefined ? !this.isDev : process.env.HIDE_DEV_LOGS === '1' | ||||
|     this.socketListeners = [] | ||||
| 
 | ||||
|     this.logManager = null | ||||
| @ -92,7 +93,7 @@ class Logger { | ||||
|    * @param  {...any} args | ||||
|    */ | ||||
|   dev(...args) { | ||||
|     if (!this.isDev || process.env.HIDE_DEV_LOGS === '1') return | ||||
|     if (this.hideDevLogs) return | ||||
|     console.log(`[${this.timestamp}] DEV:`, ...args) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -32,13 +32,13 @@ const PodcastManager = require('./managers/PodcastManager') | ||||
| const AudioMetadataMangaer = require('./managers/AudioMetadataManager') | ||||
| const RssFeedManager = require('./managers/RssFeedManager') | ||||
| const CronManager = require('./managers/CronManager') | ||||
| const ApiCacheManager = require('./managers/ApiCacheManager') | ||||
| const LibraryScanner = require('./scanner/LibraryScanner') | ||||
| 
 | ||||
| //Import the main Passport and Express-Session library
 | ||||
| const passport = require('passport') | ||||
| const expressSession = require('express-session') | ||||
| 
 | ||||
| 
 | ||||
| class Server { | ||||
|   constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { | ||||
|     this.Port = PORT | ||||
| @ -73,6 +73,7 @@ class Server { | ||||
|     this.audioMetadataManager = new AudioMetadataMangaer() | ||||
|     this.rssFeedManager = new RssFeedManager() | ||||
|     this.cronManager = new CronManager(this.podcastManager) | ||||
|     this.apiCacheManager = new ApiCacheManager() | ||||
| 
 | ||||
|     // Routers
 | ||||
|     this.apiRouter = new ApiRouter(this) | ||||
| @ -117,6 +118,7 @@ class Server { | ||||
| 
 | ||||
|     const libraries = await Database.libraryModel.getAllOldLibraries() | ||||
|     await this.cronManager.init(libraries) | ||||
|     this.apiCacheManager.init() | ||||
| 
 | ||||
|     if (Database.serverSettings.scannerDisableWatcher) { | ||||
|       Logger.info(`[Server] Watcher is disabled`) | ||||
| @ -138,11 +140,13 @@ class Server { | ||||
|      * The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests | ||||
|      * so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint | ||||
|      * @see https://ionicframework.com/docs/troubleshooting/cors
 | ||||
|      *  | ||||
|      * Running in development allows cors to allow testing the mobile apps in the browser  | ||||
|      */ | ||||
|     app.use((req, res, next) => { | ||||
|       if (req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) { | ||||
|       if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) { | ||||
|         const allowedOrigins = ['capacitor://localhost', 'http://localhost'] | ||||
|         if (allowedOrigins.some(o => o === req.get('origin'))) { | ||||
|         if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) { | ||||
|           res.header('Access-Control-Allow-Origin', req.get('origin')) | ||||
|           res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') | ||||
|           res.header('Access-Control-Allow-Headers', '*') | ||||
|  | ||||
| @ -192,9 +192,9 @@ class SocketAuthority { | ||||
| 
 | ||||
|     this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions)) | ||||
| 
 | ||||
|     // Update user lastSeen
 | ||||
|     // Update user lastSeen without firing sequelize bulk update hooks
 | ||||
|     user.lastSeen = Date.now() | ||||
|     await Database.updateUser(user) | ||||
|     await Database.userModel.updateFromOld(user, false) | ||||
| 
 | ||||
|     const initialPayload = { | ||||
|       userId: client.user.id, | ||||
|  | ||||
							
								
								
									
										54
									
								
								server/managers/ApiCacheManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								server/managers/ApiCacheManager.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| const { LRUCache } = require('lru-cache') | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class ApiCacheManager { | ||||
| 
 | ||||
|   defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => (item.body.length + JSON.stringify(item.headers).length) } | ||||
|   defaultTtlOptions = { ttl: 30 * 60 * 1000 } | ||||
| 
 | ||||
|   constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) { | ||||
|     this.cache = cache | ||||
|     this.ttlOptions = ttlOptions | ||||
|   } | ||||
| 
 | ||||
|   init(database = Database) { | ||||
|     let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy'] | ||||
|     hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook))) | ||||
|   } | ||||
| 
 | ||||
|   clear(model, hook) { | ||||
|     Logger.debug(`[ApiCacheManager] ${model.constructor.name}.${hook}: Clearing cache`) | ||||
|     this.cache.clear() | ||||
|   } | ||||
| 
 | ||||
|   get middleware() { | ||||
|     return (req, res, next) => { | ||||
|       const key = { user: req.user.username, url: req.url } | ||||
|       const stringifiedKey = JSON.stringify(key) | ||||
|       Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`) | ||||
|       const cached = this.cache.get(stringifiedKey) | ||||
|       if (cached) { | ||||
|         Logger.debug(`[ApiCacheManager] Cache hit: ${stringifiedKey}`) | ||||
|         res.set(cached.headers) | ||||
|         res.status(cached.statusCode) | ||||
|         res.send(cached.body) | ||||
|         return | ||||
|       } | ||||
|       res.originalSend = res.send | ||||
|       res.send = (body) => { | ||||
|         Logger.debug(`[ApiCacheManager] Cache miss: ${stringifiedKey}`) | ||||
|         const cached = { body, headers: res.getHeaders(), statusCode: res.statusCode } | ||||
|         if (key.url.search(/^\/libraries\/.*?\/personalized/) !== -1) { | ||||
|           Logger.debug(`[ApiCacheManager] Caching with ${this.ttlOptions.ttl} ms TTL`) | ||||
|           this.cache.set(stringifiedKey, cached, this.ttlOptions) | ||||
|         } else { | ||||
|           this.cache.set(stringifiedKey, cached) | ||||
|         } | ||||
|         res.originalSend(body) | ||||
|       } | ||||
|       next() | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = ApiCacheManager | ||||
| @ -99,11 +99,13 @@ class User extends Model { | ||||
|    * Update User from old user model | ||||
|    *  | ||||
|    * @param {oldUser} oldUser  | ||||
|    * @param {boolean} [hooks=true] Run before / after bulk update hooks? | ||||
|    * @returns {Promise<boolean>} | ||||
|    */ | ||||
|   static updateFromOld(oldUser) { | ||||
|   static updateFromOld(oldUser, hooks = true) { | ||||
|     const user = this.getFromOld(oldUser) | ||||
|     return this.update(user, { | ||||
|       hooks: !!hooks, | ||||
|       where: { | ||||
|         id: user.id | ||||
|       } | ||||
|  | ||||
| @ -9,7 +9,7 @@ class LibrarySettings { | ||||
|     this.autoScanCronExpression = null | ||||
|     this.audiobooksOnly = false | ||||
|     this.hideSingleBookSeries = false // Do not show series that only have 1 book 
 | ||||
|     this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|     this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
| 
 | ||||
|     if (settings) { | ||||
|       this.construct(settings) | ||||
| @ -28,7 +28,7 @@ class LibrarySettings { | ||||
|       this.metadataPrecedence = [...settings.metadataPrecedence] | ||||
|     } else { | ||||
|       // Added in v2.4.5
 | ||||
|       this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|       this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -48,6 +48,7 @@ class ApiRouter { | ||||
|     this.cronManager = Server.cronManager | ||||
|     this.notificationManager = Server.notificationManager | ||||
|     this.emailManager = Server.emailManager | ||||
|     this.apiCacheManager = Server.apiCacheManager | ||||
| 
 | ||||
|     this.router = express() | ||||
|     this.router.disable('x-powered-by') | ||||
| @ -58,6 +59,7 @@ class ApiRouter { | ||||
|     //
 | ||||
|     // Library Routes
 | ||||
|     //
 | ||||
|     this.router.get(/^\/libraries/, this.apiCacheManager.middleware) | ||||
|     this.router.post('/libraries', LibraryController.create.bind(this)) | ||||
|     this.router.get('/libraries', LibraryController.findAll.bind(this)) | ||||
|     this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this)) | ||||
|  | ||||
| @ -18,6 +18,7 @@ const BookFinder = require('../finders/BookFinder') | ||||
| 
 | ||||
| const LibraryScan = require("./LibraryScan") | ||||
| const OpfFileScanner = require('./OpfFileScanner') | ||||
| const NfoFileScanner = require('./NfoFileScanner') | ||||
| const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') | ||||
| 
 | ||||
| /** | ||||
| @ -593,7 +594,7 @@ class BookScanner { | ||||
|     } | ||||
| 
 | ||||
|     const bookMetadataSourceHandler = new BookScanner.BookMetadataSourceHandler(bookMetadata, audioFiles, libraryItemData, libraryScan, existingLibraryItemId) | ||||
|     const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|     const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|     libraryScan.addLog(LogLevel.DEBUG, `"${bookMetadata.title}" Getting metadata with precedence [${metadataPrecedence.join(', ')}]`) | ||||
|     for (const metadataSource of metadataPrecedence) { | ||||
|       if (bookMetadataSourceHandler[metadataSource]) { | ||||
| @ -649,6 +650,14 @@ class BookScanner { | ||||
|       AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Metadata from .nfo file | ||||
|      */ | ||||
|     async nfoFile() { | ||||
|       if (!this.libraryItemData.metadataNfoLibraryFile) return | ||||
|       await NfoFileScanner.scanBookNfoFile(this.libraryItemData.metadataNfoLibraryFile, this.bookMetadata) | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Description from desc.txt and narrator from reader.txt | ||||
|      */ | ||||
|  | ||||
| @ -132,6 +132,11 @@ class LibraryItemScanData { | ||||
|     return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.opf') | ||||
|   } | ||||
| 
 | ||||
|   /** @type {LibraryItem.LibraryFileObject} */ | ||||
|   get metadataNfoLibraryFile() { | ||||
|     return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.nfo') | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    *  | ||||
|    * @param {LibraryItem} existingLibraryItem  | ||||
|  | ||||
							
								
								
									
										48
									
								
								server/scanner/NfoFileScanner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								server/scanner/NfoFileScanner.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| const { parseNfoMetadata } = require('../utils/parsers/parseNfoMetadata') | ||||
| const { readTextFile } = require('../utils/fileUtils') | ||||
| 
 | ||||
| class NfoFileScanner { | ||||
|   constructor() { } | ||||
| 
 | ||||
|   /** | ||||
|    * Parse metadata from .nfo file found in library scan and update bookMetadata | ||||
|    *  | ||||
|    * @param {import('../models/LibraryItem').LibraryFileObject} nfoLibraryFileObj  | ||||
|    * @param {Object} bookMetadata  | ||||
|    */ | ||||
|   async scanBookNfoFile(nfoLibraryFileObj, bookMetadata) { | ||||
|     const nfoText = await readTextFile(nfoLibraryFileObj.metadata.path) | ||||
|     const nfoMetadata = nfoText ? await parseNfoMetadata(nfoText) : null | ||||
|     if (nfoMetadata) { | ||||
|       for (const key in nfoMetadata) { | ||||
|         if (key === 'tags') { // Add tags only if tags are empty
 | ||||
|           if (nfoMetadata.tags.length) { | ||||
|             bookMetadata.tags = nfoMetadata.tags | ||||
|           } | ||||
|         } else if (key === 'genres') { // Add genres only if genres are empty
 | ||||
|           if (nfoMetadata.genres.length) { | ||||
|             bookMetadata.genres = nfoMetadata.genres | ||||
|           } | ||||
|         } else if (key === 'authors') { | ||||
|           if (nfoMetadata.authors?.length) { | ||||
|             bookMetadata.authors = nfoMetadata.authors | ||||
|           } | ||||
|         } else if (key === 'narrators') { | ||||
|           if (nfoMetadata.narrators?.length) { | ||||
|             bookMetadata.narrators = nfoMetadata.narrators | ||||
|           } | ||||
|         } else if (key === 'series') { | ||||
|           if (nfoMetadata.series) { | ||||
|             bookMetadata.series = [{ | ||||
|               name: nfoMetadata.series, | ||||
|               sequence: nfoMetadata.sequence || null | ||||
|             }] | ||||
|           } | ||||
|         } else if (nfoMetadata[key] && key !== 'sequence') { | ||||
|           bookMetadata[key] = nfoMetadata[key] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = new NfoFileScanner() | ||||
							
								
								
									
										100
									
								
								server/utils/parsers/parseNfoMetadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								server/utils/parsers/parseNfoMetadata.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| function parseNfoMetadata(nfoText) { | ||||
|   if (!nfoText) return null | ||||
|   const lines = nfoText.split(/\r?\n/) | ||||
|   const metadata = {} | ||||
|   let insideBookDescription = false | ||||
|   lines.forEach(line => { | ||||
|     if (line.search(/^\s*book description\s*$/i) !== -1) { | ||||
|       insideBookDescription = true | ||||
|       return | ||||
|     } | ||||
|     if (insideBookDescription) { | ||||
|       if (line.search(/^\s*=+\s*$/i) !== -1) return | ||||
|       metadata.description = metadata.description || '' | ||||
|       metadata.description += line + '\n' | ||||
|       return | ||||
|     } | ||||
|     const match = line.match(/^(.*?):(.*)$/) | ||||
|     if (match) { | ||||
|       const key = match[1].toLowerCase().trim() | ||||
|       const value = match[2].trim() | ||||
|       if (!value) return | ||||
|       switch (key) { | ||||
|         case 'title': | ||||
|           { | ||||
|             const titleMatch = value.match(/^(.*?):(.*)$/) | ||||
|             if (titleMatch) { | ||||
|               metadata.title = titleMatch[1].trim() | ||||
|               metadata.subtitle = titleMatch[2].trim() | ||||
|             } else { | ||||
|               metadata.title = value | ||||
|             } | ||||
|           } | ||||
|           break | ||||
|         case 'author': | ||||
|           metadata.authors = value.split(/\s*,\s*/).filter(v => v) | ||||
|           break | ||||
|         case 'narrator': | ||||
|         case 'read by': | ||||
|           metadata.narrators = value.split(/\s*,\s*/).filter(v => v) | ||||
|           break | ||||
|         case 'series name': | ||||
|           metadata.series = value | ||||
|           break | ||||
|         case 'genre': | ||||
|           metadata.genres = value.split(/\s*,\s*/).filter(v => v) | ||||
|           break | ||||
|         case 'tags': | ||||
|           metadata.tags = value.split(/\s*,\s*/).filter(v => v) | ||||
|           break | ||||
|         case 'copyright': | ||||
|         case 'audible.com release': | ||||
|         case 'audiobook copyright': | ||||
|         case 'book copyright': | ||||
|         case 'recording copyright': | ||||
|         case 'release date': | ||||
|         case 'date': | ||||
|           { | ||||
|             const year = extractYear(value) | ||||
|             if (year) { | ||||
|               metadata.publishedYear = year | ||||
|             } | ||||
|           } | ||||
|           break | ||||
|         case 'position in series': | ||||
|           metadata.sequence = value | ||||
|           break | ||||
|         case 'unabridged': | ||||
|           metadata.abridged = value.toLowerCase() === 'yes' ? false : true | ||||
|           break | ||||
|         case 'abridged': | ||||
|           metadata.abridged = value.toLowerCase() === 'no' ? false : true | ||||
|           break | ||||
|         case 'publisher': | ||||
|           metadata.publisher = value | ||||
|           break | ||||
|         case 'asin': | ||||
|           metadata.asin = value | ||||
|           break | ||||
|         case 'isbn': | ||||
|         case 'isbn-10': | ||||
|         case 'isbn-13': | ||||
|           metadata.isbn = value | ||||
|           break | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   // Trim leading/trailing whitespace for description
 | ||||
|   if (metadata.description) { | ||||
|     metadata.description = metadata.description.trim() | ||||
|   } | ||||
| 
 | ||||
|   return metadata | ||||
| } | ||||
| module.exports = { parseNfoMetadata } | ||||
| 
 | ||||
| function extractYear(str) { | ||||
|   const match = str.match(/\d{4}/g) | ||||
|   return match ? match[match.length - 1] : null | ||||
| } | ||||
							
								
								
									
										97
									
								
								test/server/managers/ApiCacheManager.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								test/server/managers/ApiCacheManager.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | ||||
| // Import dependencies and modules for testing
 | ||||
| const { expect } = require('chai') | ||||
| const sinon = require('sinon') | ||||
| const ApiCacheManager = require('../../../server/managers/ApiCacheManager') | ||||
| 
 | ||||
| describe('ApiCacheManager', () => { | ||||
|   let cache | ||||
|   let req | ||||
|   let res | ||||
|   let next | ||||
|   let manager | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     cache = { get: sinon.stub(), set: sinon.spy() }  | ||||
|     req = { user: { username: 'testUser' }, url: '/test-url' } | ||||
|     res = { send: sinon.spy(), getHeaders: sinon.stub(), statusCode: 200, status: sinon.spy(), set: sinon.spy() } | ||||
|     next = sinon.spy() | ||||
|   }) | ||||
| 
 | ||||
|   describe('middleware', () => { | ||||
|     it('should send cached data if available', () => { | ||||
|       // Arrange
 | ||||
|       const cachedData = { body: 'cached data', headers: { 'content-type': 'application/json' }, statusCode: 200 } | ||||
|       cache.get.returns(cachedData) | ||||
|       const key = JSON.stringify({ user: req.user.username, url: req.url }) | ||||
|       manager = new ApiCacheManager(cache) | ||||
| 
 | ||||
|       // Act
 | ||||
|       manager.middleware(req, res, next) | ||||
| 
 | ||||
|       // Assert
 | ||||
|       expect(cache.get.calledOnce).to.be.true | ||||
|       expect(cache.get.calledWith(key)).to.be.true | ||||
|       expect(res.set.calledOnce).to.be.true | ||||
|       expect(res.set.calledWith(cachedData.headers)).to.be.true | ||||
|       expect(res.status.calledOnce).to.be.true | ||||
|       expect(res.status.calledWith(cachedData.statusCode)).to.be.true | ||||
|       expect(res.send.calledOnce).to.be.true | ||||
|       expect(res.send.calledWith(cachedData.body)).to.be.true | ||||
|       expect(res.originalSend).to.be.undefined | ||||
|       expect(next.called).to.be.false | ||||
|       expect(cache.set.called).to.be.false | ||||
|     }) | ||||
| 
 | ||||
|     it('should cache and send response if data is not cached', () => { | ||||
|       // Arrange
 | ||||
|       cache.get.returns(null) | ||||
|       const headers = { 'content-type': 'application/json' } | ||||
|       res.getHeaders.returns(headers) | ||||
|       const body = 'response data' | ||||
|       const statusCode = 200 | ||||
|       const responseData = { body, headers, statusCode } | ||||
|       const key = JSON.stringify({ user: req.user.username, url: req.url }) | ||||
|       manager = new ApiCacheManager(cache) | ||||
| 
 | ||||
|       // Act
 | ||||
|       manager.middleware(req, res, next) | ||||
|       res.send(body) | ||||
| 
 | ||||
|       // Assert
 | ||||
|       expect(cache.get.calledOnce).to.be.true | ||||
|       expect(cache.get.calledWith(key)).to.be.true | ||||
|       expect(next.calledOnce).to.be.true | ||||
|       expect(cache.set.calledOnce).to.be.true | ||||
|       expect(cache.set.calledWith(key, responseData)).to.be.true | ||||
|       expect(res.originalSend.calledOnce).to.be.true | ||||
|       expect(res.originalSend.calledWith(body)).to.be.true | ||||
|     }) | ||||
| 
 | ||||
|     it('should cache personalized response with 30 minutes TTL', () => { | ||||
|       // Arrange
 | ||||
|       cache.get.returns(null) | ||||
|       const headers = { 'content-type': 'application/json' } | ||||
|       res.getHeaders.returns(headers) | ||||
|       const body = 'personalized data' | ||||
|       const statusCode = 200 | ||||
|       const responseData = { body, headers, statusCode } | ||||
|       req.url = '/libraries/id/personalized' | ||||
|       const key = JSON.stringify({ user: req.user.username, url: req.url }) | ||||
|       const ttlOptions = { ttl: 30 * 60 * 1000 } | ||||
|       manager = new ApiCacheManager(cache, ttlOptions) | ||||
| 
 | ||||
|       // Act
 | ||||
|       manager.middleware(req, res, next) | ||||
|       res.send(body) | ||||
| 
 | ||||
|       // Assert
 | ||||
|       expect(cache.get.calledOnce).to.be.true | ||||
|       expect(cache.get.calledWith(key)).to.be.true | ||||
|       expect(next.calledOnce).to.be.true | ||||
|       expect(cache.set.calledOnce).to.be.true | ||||
|       expect(cache.set.calledWith(key, responseData, ttlOptions)).to.be.true | ||||
|       expect(res.originalSend.calledOnce).to.be.true | ||||
|       expect(res.originalSend.calledWith(body)).to.be.true | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										123
									
								
								test/server/utils/parsers/parseNfoMetadata.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								test/server/utils/parsers/parseNfoMetadata.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| const chai = require('chai') | ||||
| const expect = chai.expect | ||||
| const { parseNfoMetadata } = require('../../../../server/utils/parsers/parseNfoMetadata') | ||||
| 
 | ||||
| describe('parseNfoMetadata', () => { | ||||
|   it('returns null if nfoText is empty', () => { | ||||
|     const result = parseNfoMetadata('') | ||||
|     expect(result).to.be.null | ||||
|   }) | ||||
| 
 | ||||
|   it('parses title', () => { | ||||
|     const nfoText = 'Title: The Great Gatsby' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.title).to.equal('The Great Gatsby') | ||||
|   }) | ||||
| 
 | ||||
|   it('parses title with subtitle', () => { | ||||
|     const nfoText = 'Title: The Great Gatsby: A Novel' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.title).to.equal('The Great Gatsby') | ||||
|     expect(result.subtitle).to.equal('A Novel') | ||||
|   }) | ||||
| 
 | ||||
|   it('parses authors', () => { | ||||
|     const nfoText = 'Author: F. Scott Fitzgerald' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.authors).to.deep.equal(['F. Scott Fitzgerald']) | ||||
|   }) | ||||
| 
 | ||||
|   it('parses multiple authors', () => { | ||||
|     const nfoText = 'Author: John Steinbeck, Ernest Hemingway' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.authors).to.deep.equal(['John Steinbeck', 'Ernest Hemingway']) | ||||
|   }) | ||||
| 
 | ||||
|   it('parses narrators', () => { | ||||
|     const nfoText = 'Read by: Jake Gyllenhaal' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.narrators).to.deep.equal(['Jake Gyllenhaal']) | ||||
|   }) | ||||
| 
 | ||||
|   it('parses multiple narrators', () => { | ||||
|     const nfoText = 'Read by: Jake Gyllenhaal, Kate Winslet' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.narrators).to.deep.equal(['Jake Gyllenhaal', 'Kate Winslet']) | ||||
|   }) | ||||
| 
 | ||||
|   it('parses series name', () => { | ||||
|     const nfoText = 'Series Name: Harry Potter' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.series).to.equal('Harry Potter') | ||||
|   }) | ||||
| 
 | ||||
|   it('parses genre', () => { | ||||
|     const nfoText = 'Genre: Fiction' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.genres).to.deep.equal(['Fiction']) | ||||
|   }) | ||||
| 
 | ||||
|   it('parses multiple genres', () => { | ||||
|     const nfoText = 'Genre: Fiction, Historical' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.genres).to.deep.equal(['Fiction', 'Historical']) | ||||
|   }) | ||||
| 
 | ||||
|   it('parses tags', () => { | ||||
|     const nfoText = 'Tags: mystery, thriller' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.tags).to.deep.equal(['mystery', 'thriller']) | ||||
|   }) | ||||
| 
 | ||||
|   it('parses year from various date fields', () => { | ||||
|     const nfoText = 'Release Date: 2021-05-01\nBook Copyright: 2021\nRecording Copyright: 2021' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.publishedYear).to.equal('2021') | ||||
|   }) | ||||
| 
 | ||||
|   it('parses position in series', () => { | ||||
|     const nfoText = 'Position in Series: 2' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.sequence).to.equal('2') | ||||
|   }) | ||||
| 
 | ||||
|   it('parses abridged flag', () => { | ||||
|     const nfoText = 'Abridged: No' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.abridged).to.be.false | ||||
| 
 | ||||
|     const nfoText2 = 'Unabridged: Yes' | ||||
|     const result2 = parseNfoMetadata(nfoText2) | ||||
|     expect(result2.abridged).to.be.false | ||||
|   }) | ||||
| 
 | ||||
|   it('parses publisher', () => { | ||||
|     const nfoText = 'Publisher: Penguin Random House' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.publisher).to.equal('Penguin Random House') | ||||
|   }) | ||||
| 
 | ||||
|   it('parses ASIN', () => { | ||||
|     const nfoText = 'ASIN: B08X5JZJLH' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.asin).to.equal('B08X5JZJLH') | ||||
|   }) | ||||
| 
 | ||||
|   it('parses description', () => { | ||||
|     const nfoText = 'Book Description\n=========\nThis is a book.\n It\'s good' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.description).to.equal('This is a book.\n It\'s good') | ||||
|   }) | ||||
| 
 | ||||
|   it('no value', () => { | ||||
|     const nfoText = 'Title:' | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.title).to.be.undefined | ||||
|   }) | ||||
| 
 | ||||
|   it('no year value', () => { | ||||
|     const nfoText = "Date:0" | ||||
|     const result = parseNfoMetadata(nfoText) | ||||
|     expect(result.publishedYear).to.be.undefined | ||||
|   }) | ||||
| }) | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user