Compare commits

..

44 Commits

Author SHA1 Message Date
advplyr 29752798f3 Version bump v2.30.0 2025-10-08 10:34:34 -05:00
advplyr 8c86ca4ea5 Merge pull request #4729 from mikiher/build-win-no-compress
Add a script to build an uncompressed windows executable
2025-10-08 10:08:21 -05:00
mikiher 00c62fa494 Add a script to build an uncompressed windows executable 2025-10-08 17:42:00 +03:00
advplyr 6c7f3c7e77 Merge pull request #4695 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-10-07 16:25:14 -05:00
Jan-Eric Myhrgren aec8acbdd7 Translated using Weblate (Swedish)
Currently translated at 97.2% (1131 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-10-07 12:02:09 +02:00
Юра Климович 6e19ad7777 Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-10-07 12:02:08 +02:00
Grzegorz Orlowski 3aa95fec11 Translated using Weblate (Polish)
Currently translated at 88.2% (1026 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-10-07 12:02:05 +02:00
Ahetek 37dd46d31f Translated using Weblate (Polish)
Currently translated at 88.2% (1026 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-10-07 12:02:03 +02:00
Petter Schaug-Pettersen 54a996634e Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.4% (1052 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-10-07 12:02:02 +02:00
Oğuz Ersen 54a5e368c2 Translated using Weblate (Turkish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-10-05 17:02:05 +02:00
FiendFEARing 2d313851d2 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-10-05 17:02:04 +02:00
Максим Горпиніч eb00b19457 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-10-05 17:02:03 +02:00
Kabika82 bbae9acc2d Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-10-05 17:02:02 +02:00
Milo Ivir a4e8f01f0e Translated using Weblate (Croatian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-10-05 17:02:01 +02:00
SmileFate 6bdf402da8 Translated using Weblate (Turkish)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-10-03 21:43:22 +00:00
DR 80b0e3546e Translated using Weblate (Hebrew)
Currently translated at 72.9% (847 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2025-10-03 21:43:21 +00:00
B0rax 161f3cb177 Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-10-03 21:43:21 +00:00
Jan 4a4d4a8f17 Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-10-03 21:43:20 +00:00
Petri Hämäläinen b21046027c Translated using Weblate (Finnish)
Currently translated at 95.5% (1109 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-10-03 21:43:19 +00:00
max grakov 3a163e1746 Translated using Weblate (Russian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-10-03 21:43:18 +00:00
Amirhossein Ghorbanmehr 3c4e80f1c1 Translated using Weblate (Persian)
Currently translated at 2.2% (26 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fa/
2025-10-03 21:43:18 +00:00
SmileFate 2f3036faba Translated using Weblate (Turkish)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-10-03 21:43:17 +00:00
advplyr 3934461c46 Added translation using Weblate (Persian) 2025-10-03 21:43:16 +00:00
advplyr 123351e08a Merge pull request #4716 from mikiher/async-cover-search
Async Cover Search
2025-10-03 16:43:05 -05:00
advplyr 1280ddfe74 ui/ux disable inputs while cover search in progress, add padding on empty state texts 2025-10-03 16:39:36 -05:00
mikiher 7e89b97a6d Tidy up cover search console logging and error toasts 2025-10-03 09:08:17 +03:00
mikiher 20de2ea388 Add "Best" option to book cover search 2025-10-03 08:23:53 +03:00
mikiher dbb5ee79ac Revert removal of audiobookcovers provider 2025-10-03 08:20:56 +03:00
mikiher c6dabd2620 Shorten timeout and error message for remaining providers 2025-10-02 22:23:12 +03:00
mikiher 26f949b9ba Remove audiobookcovers from provider list 2025-10-02 22:14:48 +03:00
mikiher 7630dbdcb7 Replace cover search with streaming version 2025-10-02 13:30:03 +03:00
mikiher a164c17d38 Reduce provider timout to 10 secs, Shorten error message 2025-10-02 13:26:05 +03:00
advplyr 03da194953 Update for nextjs client, pass all remaining requests through to nextjs 2025-09-28 09:41:15 -05:00
advplyr e040396b20 Merge pull request #4656 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-09-22 09:41:08 -05:00
Phil Jope bcbec67fec Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-09-20 23:02:02 +02:00
Vito0912 1543021685 Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-09-20 23:02:01 +02:00
Salmanegr 577e6aaec9 Translated using Weblate (Arabic)
Currently translated at 95.4% (1108 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2025-09-19 08:02:02 +00:00
Milo Ivir 77579acfd4 Translated using Weblate (Croatian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-09-10 22:24:06 +00:00
Yuta Imada 9ca98ca750 Translated using Weblate (Japanese)
Currently translated at 15.5% (180 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-09-10 22:24:05 +00:00
Satanowski feb225d3a6 Translated using Weblate (Polish)
Currently translated at 82.3% (956 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-09-10 22:24:05 +00:00
Yuta Imada e501aa4f1e Translated using Weblate (Japanese)
Currently translated at 15.4% (179 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-09-10 22:24:04 +00:00
peter cerny 104f6e6c58 Translated using Weblate (Slovak)
Currently translated at 99.9% (1160 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-09-10 22:24:03 +00:00
jhonthan 552d8ae3b8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 67.7% (787 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-09-10 22:24:03 +00:00
advplyr a41e9bae5d Merge pull request #4664 from advplyr/episode_download_fallback
Fix issue with episode downloads without streams
2025-09-10 17:23:53 -05:00
35 changed files with 1710 additions and 208 deletions
+145 -19
View File
@@ -51,19 +51,21 @@
<form @submit.prevent="submitSearchForm">
<div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
<div class="w-48 grow p-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
<ui-dropdown v-model="provider" :items="providers" :disabled="searchInProgress" :label="$strings.LabelProvider" small />
</div>
<div class="w-72 grow p-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
<ui-text-input-with-label v-model="searchTitle" :disabled="searchInProgress" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div>
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 grow p-1">
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
<ui-text-input-with-label v-model="searchAuthor" :disabled="searchInProgress" :label="$strings.LabelAuthor" />
</div>
<ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
<ui-btn v-if="!searchInProgress" class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
<ui-btn v-else class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="button" color="bg-error" @click.prevent="cancelCurrentSearch">{{ $strings.ButtonCancel }}</ui-btn>
</div>
</form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
<p v-if="searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageLoading }}</p>
<p v-else-if="!searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
@@ -105,7 +107,10 @@ export default {
showLocalCovers: false,
previewUpload: null,
selectedFile: null,
provider: 'google'
provider: 'google',
currentSearchRequestId: null,
searchInProgress: false,
socketListenersActive: false
}
},
watch: {
@@ -129,7 +134,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return [{ text: 'All', value: 'all' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders]
return [{ text: 'Best', value: 'best' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders, { text: 'All', value: 'all' }]
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
@@ -186,6 +191,9 @@ export default {
_file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}`
return _file
})
},
socket() {
return this.$root.socket
}
},
methods: {
@@ -235,7 +243,19 @@ export default {
this.searchTitle = this.mediaMetadata.title || ''
this.searchAuthor = this.mediaMetadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
else {
// Migrate from 'all' to 'best' (only once)
const migrationKey = 'book-cover-provider-migrated'
const currentProvider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
if (!localStorage.getItem(migrationKey) && currentProvider === 'all') {
localStorage.setItem('book-cover-provider', 'best')
localStorage.setItem(migrationKey, 'true')
this.provider = 'best'
} else {
this.provider = currentProvider
}
}
},
removeCover() {
if (!this.coverPath) {
@@ -291,22 +311,116 @@ export default {
console.error('PersistProvider', error)
}
},
generateRequestId() {
return `cover-search-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
},
addSocketListeners() {
if (!this.socket || this.socketListenersActive) return
this.socket.on('cover_search_result', this.handleSearchResult)
this.socket.on('cover_search_complete', this.handleSearchComplete)
this.socket.on('cover_search_error', this.handleSearchError)
this.socket.on('cover_search_provider_error', this.handleProviderError)
this.socket.on('cover_search_cancelled', this.handleSearchCancelled)
this.socket.on('disconnect', this.handleSocketDisconnect)
this.socketListenersActive = true
},
removeSocketListeners() {
if (!this.socket || !this.socketListenersActive) return
this.socket.off('cover_search_result', this.handleSearchResult)
this.socket.off('cover_search_complete', this.handleSearchComplete)
this.socket.off('cover_search_error', this.handleSearchError)
this.socket.off('cover_search_provider_error', this.handleProviderError)
this.socket.off('cover_search_cancelled', this.handleSearchCancelled)
this.socket.off('disconnect', this.handleSocketDisconnect)
this.socketListenersActive = false
},
handleSearchResult(data) {
if (data.requestId !== this.currentSearchRequestId) return
// Add new covers to the list (avoiding duplicates)
const newCovers = data.covers.filter((cover) => !this.coversFound.includes(cover))
this.coversFound.push(...newCovers)
},
handleSearchComplete(data) {
if (data.requestId !== this.currentSearchRequestId) return
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleSearchError(data) {
if (data.requestId !== this.currentSearchRequestId) return
console.error('[Cover Search] Search error:', data.error)
this.$toast.error(this.$strings.ToastCoverSearchFailed)
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleProviderError(data) {
if (data.requestId !== this.currentSearchRequestId) return
console.warn(`[Cover Search] Provider ${data.provider} failed:`, data.error)
},
handleSearchCancelled(data) {
if (data.requestId !== this.currentSearchRequestId) return
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleSocketDisconnect() {
// If we were in the middle of a search, cancel it (server can't send results anymore)
if (this.searchInProgress && this.currentSearchRequestId) {
this.searchInProgress = false
this.currentSearchRequestId = null
}
},
cancelCurrentSearch() {
if (!this.currentSearchRequestId || !this.socket?.connected) {
console.error('[Cover Search] Socket not connected')
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
return
}
this.socket.emit('cancel_cover_search', this.currentSearchRequestId)
this.currentSearchRequestId = null
this.searchInProgress = false
},
async submitSearchForm() {
if (!this.socket?.connected) {
console.error('[Cover Search] Socket not connected')
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
return
}
// Cancel any existing search
if (this.searchInProgress) {
this.cancelCurrentSearch()
}
// Store provider in local storage
this.persistProvider()
this.isProcessing = true
const searchQuery = this.getSearchQuery()
const results = await this.$axios
.$get(`/api/search/covers?${searchQuery}`)
.then((res) => res.results)
.catch((error) => {
console.error('Failed', error)
return []
})
this.coversFound = results
this.isProcessing = false
// Setup socket listeners if not already done
this.addSocketListeners()
// Clear previous results
this.coversFound = []
this.hasSearched = true
this.searchInProgress = true
// Generate unique request ID
const requestId = this.generateRequestId()
this.currentSearchRequestId = requestId
// Emit search request via WebSocket
this.socket.emit('search_covers', {
requestId,
title: this.searchTitle,
author: this.searchAuthor || '',
provider: this.provider,
podcast: this.isPodcast
})
},
setCover(coverFile) {
this.isProcessing = true
@@ -320,6 +434,18 @@ export default {
this.isProcessing = false
})
}
},
mounted() {
// Setup socket listeners when component is mounted
this.addSocketListeners()
},
beforeDestroy() {
// Cancel any ongoing search when component is destroyed
if (this.searchInProgress) {
this.cancelCurrentSearch()
}
// Remove socket listeners
this.removeSocketListeners()
}
}
</script>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
"version": "2.29.0",
"version": "2.30.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
"version": "2.29.0",
"version": "2.30.0",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.29.0",
"version": "2.30.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
+1
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "إضافة",
"ButtonAddApiKey": "إضافة مفتاح واجهة برمجة التطبيقات",
"ButtonAddChapters": "إضافة الفصول",
"ButtonAddDevice": "إضافة جهاز",
"ButtonAddLibrary": "إضافة مكتبة",
+13 -8
View File
@@ -55,7 +55,7 @@
"ButtonNext": "Vor",
"ButtonNextChapter": "Nächstes Kapitel",
"ButtonNextItemInQueue": "Das nächste Element in der Warteschlange",
"ButtonOk": "Ok",
"ButtonOk": "OK",
"ButtonOpenFeed": "Feed öffnen",
"ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pausieren",
@@ -75,7 +75,7 @@
"ButtonQuickMatch": "Schnellabgleich",
"ButtonReScan": "Neu scannen",
"ButtonRead": "Lesen",
"ButtonReadLess": "weniger Anzeigen",
"ButtonReadLess": "Weniger Anzeigen",
"ButtonReadMore": "Mehr Anzeigen",
"ButtonRefresh": "Neu Laden",
"ButtonRemove": "Entfernen",
@@ -104,7 +104,7 @@
"ButtonStartM4BEncode": "M4B-Kodierung starten",
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
"ButtonStats": "Statistiken",
"ButtonSubmit": "Ok",
"ButtonSubmit": "Absenden",
"ButtonTest": "Test",
"ButtonUnlinkOpenId": "OpenID trennen",
"ButtonUpload": "Hochladen",
@@ -116,7 +116,7 @@
"ButtonViewAll": "Alles anzeigen",
"ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und oder den Autor zu aktualisieren",
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und/oder den Autor zu aktualisieren.",
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
"HeaderAccount": "Konto",
"HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen",
@@ -138,13 +138,13 @@
"HeaderCustomMessageOnLogin": "Benutzerdefinierte Nachricht für die Anmeldung",
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadatenanbieter",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Warteschlange",
"HeaderEbookFiles": "E-Buch-Dateien",
"HeaderDownloadQueue": "Download-Warteschlange",
"HeaderEbookFiles": "E-Book-Dateien",
"HeaderEmail": "E-Mail",
"HeaderEmailSettings": "E-Mail-Einstellungen",
"HeaderEpisodes": "Episoden",
"HeaderEreaderDevices": "E-Reader Geräte",
"HeaderEreaderSettings": "Einstellungen zum Lesen",
"HeaderEreaderSettings": "E-Reader-Einstellungen",
"HeaderFiles": "Dateien",
"HeaderFindChapters": "Kapitel suchen",
"HeaderIgnoredFiles": "Ignorierte Dateien",
@@ -378,6 +378,7 @@
"LabelFilterByUser": "Nach Benutzern filtern",
"LabelFindEpisodes": "Episoden suchen",
"LabelFinished": "Beendet",
"LabelFinishedDate": "Beendet {0}",
"LabelFolder": "Ordner",
"LabelFolders": "Verzeichnisse",
"LabelFontBold": "Fett",
@@ -435,7 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Keine {0}",
"LabelLibraryItem": "Bibliothekseintrag",
"LabelLibraryName": "Bibliotheksname",
"LabelLibrarySortByProgress": "Fortschritt aktualisiert",
"LabelLibrarySortByProgress": "Fortschritt: Zuletzt aktualisiert",
"LabelLibrarySortByProgressFinished": "Fortschritt: Beendet",
"LabelLibrarySortByProgressStarted": "Fortschritt: Gestartet",
"LabelLimit": "Begrenzung",
"LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören",
@@ -635,6 +638,7 @@
"LabelStartTime": "Startzeit",
"LabelStarted": "Gestartet",
"LabelStartedAt": "Gestartet am",
"LabelStartedDate": "Angefangen am {0}",
"LabelStatsAudioTracks": "Audiodateien",
"LabelStatsAuthors": "Autoren",
"LabelStatsBestDay": "Bester Tag",
@@ -1096,6 +1100,7 @@
"ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert",
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
"ToastPodcastCreateSuccess": "Podcast erstellt",
"ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert",
"ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds",
"ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden",
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
+2
View File
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
"ToastCollectionRemoveSuccess": "Collection removed",
"ToastCollectionUpdateSuccess": "Collection updated",
"ToastConnectionNotAvailable": "Connection not available. Please try again later",
"ToastCoverSearchFailed": "Cover search failed",
"ToastCoverUpdateFailed": "Cover update failed",
"ToastDateTimeInvalidOrIncomplete": "Date and time is invalid or incomplete",
"ToastDeleteFileFailed": "Failed to delete file",
+28
View File
@@ -0,0 +1,28 @@
{
"ButtonAdd": "افزودن",
"ButtonAuthors": "ناشر",
"ButtonBack": "بازگشت",
"ButtonCancel": "انصراف",
"ButtonClearFilter": "حذف صافی",
"ButtonCloseFeed": "بستن فید",
"ButtonCollections": "مجموعه ها",
"ButtonCreate": "ساختن",
"ButtonDelete": "حذف",
"ButtonHome": "خانه",
"ButtonIssues": "مشکلات",
"ButtonLatest": "جدیدترین",
"ButtonLibrary": "کتابخانه",
"ButtonOk": "تایید",
"ButtonOpenFeed": "باز کردن فید",
"ButtonPause": "توقف",
"ButtonPlay": "پخش",
"ButtonPlaylists": "لیست پخش",
"ButtonRead": "خواندن",
"ButtonReadLess": "خواندن کمتر",
"ButtonReadMore": "خواندن بیشتر",
"ButtonRemove": "حذف",
"ButtonSave": "ذخیره",
"ButtonSearch": "جستجو",
"ButtonSeries": "مجموعه",
"ButtonSubmit": "ثبت"
}
+10
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "Lisää",
"ButtonAddApiKey": "Lisää API avain",
"ButtonAddChapters": "Lisää lukuja",
"ButtonAddDevice": "Lisää laite",
"ButtonAddLibrary": "Lisää kirjasto",
@@ -20,6 +21,7 @@
"ButtonChooseAFolder": "Valitse kansio",
"ButtonChooseFiles": "Valitse tiedostot",
"ButtonClearFilter": "Poista suodatus",
"ButtonClose": "Sulje",
"ButtonCloseFeed": "Sulje syöte",
"ButtonCloseSession": "Sulje Avoin Sessio",
"ButtonCollections": "Kokoelmat",
@@ -119,11 +121,13 @@
"HeaderAccount": "Tili",
"HeaderAddCustomMetadataProvider": "Lisää mukautettu metadata tarjoaja",
"HeaderAdvanced": "Edistynyt",
"HeaderApiKeys": "API avaimet",
"HeaderAppriseNotificationSettings": "Apprise-ilmoitusasetukset",
"HeaderAudioTracks": "Ääniraidat",
"HeaderAudiobookTools": "Äänikirjojen tiedostonhallintatyökalut",
"HeaderAuthentication": "Todennus",
"HeaderBackups": "Varmuuskopiot",
"HeaderBulkChapterModal": "Lisää useita kappaleita",
"HeaderChangePassword": "Vaihda salasana",
"HeaderChapters": "Luvut",
"HeaderChooseAFolder": "Valitse kansio",
@@ -162,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "Metadatan tärkeysjärjestys",
"HeaderMetadataToEmbed": "Sisällytettävä metadata",
"HeaderNewAccount": "Uusi tili",
"HeaderNewApiKey": "Uusi API avain",
"HeaderNewLibrary": "Uusi kirjasto",
"HeaderNotificationCreate": "Luo ilmoitus",
"HeaderNotificationUpdate": "Päivitä ilmoitus",
@@ -194,6 +199,7 @@
"HeaderSettingsExperimental": "Kokeelliset ominaisuudet",
"HeaderSettingsGeneral": "Yleiset",
"HeaderSettingsScanner": "Skannaaja",
"HeaderSettingsSecurity": "Turvallisuus",
"HeaderSettingsWebClient": "Webasiakasohjelma",
"HeaderSleepTimer": "Uniajastin",
"HeaderStatsLargestItems": "Suurimmat kohteet",
@@ -205,6 +211,7 @@
"HeaderTableOfContents": "Sisällysluettelo",
"HeaderTools": "Työkalut",
"HeaderUpdateAccount": "Päivitä tili",
"HeaderUpdateApiKey": "Päivitä API avain",
"HeaderUpdateAuthor": "Päivitä tekijä",
"HeaderUpdateDetails": "Päivitä yksityiskohdat",
"HeaderUpdateLibrary": "Päivitä kirjasto",
@@ -234,6 +241,8 @@
"LabelAllUsersExcludingGuests": "Kaikki käyttäjät vieraita lukuun ottamatta",
"LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat",
"LabelAlreadyInYourLibrary": "Jo kirjastossasi",
"LabelApiKeyCreated": "API avain \"{0}\" luotu onnistuneesti.",
"LabelApiKeyCreatedDescription": "Varmista, että kopioit API avaimen. Sitä ei näytetä enää tämän jälkeen.",
"LabelApiToken": "Sovellusliittymätunnus",
"LabelAppend": "Lisää loppuun",
"LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)",
@@ -283,6 +292,7 @@
"LabelContinueListening": "Jatka kuuntelua",
"LabelContinueReading": "Jatka lukemista",
"LabelContinueSeries": "Jatka sarjoja",
"LabelCorsAllowed": "Salli CORS Origins",
"LabelCover": "Kansikuva",
"LabelCoverImageURL": "Kansikuvan URL-osoite",
"LabelCoverProvider": "Kansikuvan tarjoaja",
+1 -1
View File
@@ -348,7 +348,7 @@
"LabelExample": "דוגמה",
"LabelExpandSeries": "הרחב סדרה",
"LabelExpandSubSeries": "הרחב תת סדרה",
"LabelExplicit": "בוטה",
"LabelExplicit": "מפורש",
"LabelExplicitChecked": "בוטה (מסומן)",
"LabelExplicitUnchecked": "לא בוטה (לא מסומן)",
"LabelExportOPML": "ייצוא OPML",
+5 -3
View File
@@ -377,8 +377,8 @@
"LabelFilename": "Naziv datoteke",
"LabelFilterByUser": "Filtriraj po korisniku",
"LabelFindEpisodes": "Pronađi nastavke",
"LabelFinished": "Dovršeno",
"LabelFinishedDate": "Dovršeno {0}",
"LabelFinished": "Završeno",
"LabelFinishedDate": "Završeno {0}",
"LabelFolder": "Mapa",
"LabelFolders": "Mape",
"LabelFontBold": "Podebljano",
@@ -884,7 +884,7 @@
"MessageRemoveEpisodes": "Ukloni {0} nastavaka",
"MessageRemoveFromPlayerQueue": "Ukloni iz redoslijeda izvođenja",
"MessageRemoveUserWarning": "Sigurno želite trajno izbrisati korisnika \"{0}\"?",
"MessageReportBugsAndContribute": "Prijavite pogreške, zatražite funkcionalnosti i doprinesite na",
"MessageReportBugsAndContribute": "Prijavite pogreške, zatražite značajke i doprinesite na",
"MessageResetChaptersConfirm": "Sigurno želite vratiti poglavlja na prethodno stanje i poništiti učinjene promjene?",
"MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu",
"MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.<br /><br />Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati. <br /><br />Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.",
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Neuspješno dodavanje stavki u zbirku",
"ToastCollectionRemoveSuccess": "Zbirka izbrisana",
"ToastCollectionUpdateSuccess": "Zbirka ažurirana",
"ToastConnectionNotAvailable": "Veza nije dostupna. Pokušaj ponovo kasnije",
"ToastCoverSearchFailed": "Pretraga naslovnice neuspjela",
"ToastCoverUpdateFailed": "Ažuriranje naslovnice nije uspjelo",
"ToastDateTimeInvalidOrIncomplete": "Datum i vrijeme su neispravni ili nepotpuni",
"ToastDeleteFileFailed": "Brisanje datoteke nije uspjelo",
+2
View File
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "A tétel(ek) hozzáadása gyűjteményhez sikertelen",
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
"ToastConnectionNotAvailable": "A kapcsolat nem elérhető. Kérem, próbálkozzon később",
"ToastCoverSearchFailed": "A borítók keresése sikertelen",
"ToastCoverUpdateFailed": "A borító frissítése nem sikerült",
"ToastDateTimeInvalidOrIncomplete": "A dátum és az időpont érvénytelen vagy hiányos",
"ToastDeleteFileFailed": "Nem sikerült törölni a fájlt",
+14
View File
@@ -11,15 +11,19 @@
"ButtonApplyChapters": "チャプターを確定する",
"ButtonAuthors": "作者",
"ButtonBack": "戻る",
"ButtonBatchEditPopulateFromExisting": "既存のものから取り込む",
"ButtonBatchEditPopulateMapDetails": "チャプター情報を読み込む",
"ButtonBrowseForFolder": "フォルダーを選択する",
"ButtonCancel": "キャンセル",
"ButtonCancelEncode": "エンコードを取り消す",
"ButtonChangeRootPassword": "Rootのパスワードを変更する",
"ButtonCheckAndDownloadNewEpisodes": "新しいエピソードを確認してダウンロード",
"ButtonChooseAFolder": "フォルダーを選ぶ",
"ButtonChooseFiles": "ファイルを選ぶ",
"ButtonClearFilter": "絞り込みを解除",
"ButtonClose": "閉じる",
"ButtonCloseFeed": "フィードを閉じる",
"ButtonCloseSession": "開いているセッションを閉じる",
"ButtonCollections": "コレクション",
"ButtonConfigureScanner": "スキャナーの設定",
"ButtonCreate": "作成",
@@ -30,6 +34,8 @@
"ButtonEditChapters": "チャプターの編集",
"ButtonEditPodcast": "ポッドキャストの編集",
"ButtonEnable": "オンにする",
"ButtonFireAndFail": "エラーを無視して実行",
"ButtonFireOnTest": "テストを実行",
"ButtonForceReScan": "強制的に再スキャンする",
"ButtonFullPath": "絶対パス",
"ButtonHide": "非表示",
@@ -40,12 +46,18 @@
"ButtonLatest": "最新",
"ButtonLibrary": "ライブラリー",
"ButtonLogout": "ログアウト",
"ButtonLookup": "参照",
"ButtonManageTracks": "トラックの管理",
"ButtonMapChapterTitles": "チャプターのタイトルを割り当て",
"ButtonMatchAllAuthors": "すべての作者と紐付け",
"ButtonMatchBooks": "本と紐付け",
"ButtonNevermind": "中止",
"ButtonNext": "次",
"ButtonNextChapter": "次のチャプター",
"ButtonNextItemInQueue": "キューの中の次のアイテム",
"ButtonOk": "はい",
"ButtonOpenFeed": "フィードを開く",
"ButtonOpenManager": "管理画面を開く",
"ButtonPause": "一時停止",
"ButtonPlay": "再生",
"ButtonPlayAll": "全て再生",
@@ -53,11 +65,13 @@
"ButtonPlaylists": "プレイリスト",
"ButtonPrevious": "先",
"ButtonPreviousChapter": "前のチャプター",
"ButtonProbeAudioFile": "オーディオファイルを解析",
"ButtonPurgeAllCache": "全てのキャッシュを削除",
"ButtonPurgeItemsCache": "項目のキャッシュを削除",
"ButtonQueueAddItem": "次に再生する",
"ButtonQueueRemoveItem": "次に再生から削除",
"ButtonQuickEmbed": "クイック埋め込み",
"ButtonQuickEmbedMetadata": "メタデータの埋め込み",
"ButtonReScan": "再スキャン",
"ButtonRead": "読む",
"ButtonReadLess": "閉じる",
+2 -1
View File
@@ -11,7 +11,7 @@
"ButtonApplyChapters": "Bruk kapittel",
"ButtonAuthors": "Forfattere",
"ButtonBack": "Tilbake",
"ButtonBatchEditPopulateFromExisting": "Opprett fra eksisterende",
"ButtonBatchEditPopulateFromExisting": "Fyll ut fra eksisterende",
"ButtonBatchEditPopulateMapDetails": "Legg til detaljer",
"ButtonBrowseForFolder": "Bla gjennom mappe",
"ButtonCancel": "Avbryt",
@@ -941,6 +941,7 @@
"ToastCollectionItemsAddFailed": "Feil med å legge til element(er)",
"ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateSuccess": "samlingupdated",
"ToastCoverSearchFailed": "Finner ikke bokomslag",
"ToastCoverUpdateFailed": "Oppdatering av bilde feilet",
"ToastDeleteFileFailed": "Kunne ikke slette fil",
"ToastDeleteFileSuccess": "Fil slettet",
+85 -1
View File
@@ -127,6 +127,7 @@
"HeaderAudiobookTools": "Narzędzia do zarządzania audiobookami",
"HeaderAuthentication": "Uwierzytelnianie",
"HeaderBackups": "Kopie zapasowe",
"HeaderBulkChapterModal": "Dodaj wiele rozdziałów",
"HeaderChangePassword": "Zmień hasło",
"HeaderChapters": "Rozdziały",
"HeaderChooseAFolder": "Wybierz folder",
@@ -199,6 +200,7 @@
"HeaderSettingsExperimental": "Funkcje eksperymentalne",
"HeaderSettingsGeneral": "Ogólne",
"HeaderSettingsScanner": "Skanowanie",
"HeaderSettingsSecurity": "Bezpieczeństwo",
"HeaderSettingsWebClient": "Klient webowy",
"HeaderSleepTimer": "Wyłącznik czasowy",
"HeaderStatsLargestItems": "Największe pozycje",
@@ -242,6 +244,8 @@
"LabelAlreadyInYourLibrary": "Już istnieje w twojej bibliotece",
"LabelApiKeyCreated": "Klucz API \"{0}\" został pomyślnie utworzony.",
"LabelApiKeyCreatedDescription": "Pamiętaj o skopiowaniu klucza API, ponieważ nie będziesz już mógł go zobaczyć.",
"LabelApiKeyUser": "Wykonaj w imieniu innego użytkownika",
"LabelApiKeyUserDescription": "Ten klucz API będzie miał te same uprawnienia co użytkownik, w którego imieniu ma być używany. Wpisy w logach będą identyczne z tymi, wywołanymi przez samego użytkownika.",
"LabelApiToken": "API Token",
"LabelAppend": "Dołącz",
"LabelAudioBitrate": "Audio Bitrate (np. 128k)",
@@ -304,6 +308,7 @@
"LabelDeleteFromFileSystemCheckbox": "Usuń z systemu plików (odznacz, aby usunąć tylko z bazy danych)",
"LabelDescription": "Opis",
"LabelDeselectAll": "Odznacz wszystko",
"LabelDetectedPattern": "Wykryty schemat:",
"LabelDevice": "Urządzenie",
"LabelDeviceInfo": "Informacja o urządzeniu",
"LabelDeviceIsAvailableTo": "Urządzenie jest dostępne do...",
@@ -353,6 +358,8 @@
"LabelExample": "Przykład",
"LabelExpandSeries": "Rozwiń serie",
"LabelExpandSubSeries": "Rozwiń podserie",
"LabelExpired": "Przeterminowane",
"LabelExpiresAt": "Wygasa w",
"LabelExpiresInSeconds": "Wygasa za (sekund)",
"LabelExpiresNever": "Nigdy",
"LabelExplicit": "Nieprzyzwoite",
@@ -370,6 +377,7 @@
"LabelFilterByUser": "Filtruj według danego użytkownika",
"LabelFindEpisodes": "Znajdź odcinki",
"LabelFinished": "Zakończone",
"LabelFinishedDate": "Ukończone {0}",
"LabelFolder": "Katalog",
"LabelFolders": "Foldery",
"LabelFontBold": "Pogrubiony",
@@ -427,7 +435,9 @@
"LabelLibraryFilterSublistEmpty": "Brak {0}",
"LabelLibraryItem": "Element biblioteki",
"LabelLibraryName": "Nazwa biblioteki",
"LabelLibrarySortByProgress": "Postęp zaktualizowany",
"LabelLibrarySortByProgress": "Postęp: Ostatnio zaktualizowane",
"LabelLibrarySortByProgressFinished": "Postęp: Ukończone",
"LabelLibrarySortByProgressStarted": "Postęp: Rozpoczęte",
"LabelLimit": "Limit",
"LabelLineSpacing": "Odstęp między wierszami",
"LabelListenAgain": "Słuchaj ponownie",
@@ -436,6 +446,7 @@
"LabelLogLevelWarn": "Ostrzeżenie",
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
"LabelLowestPriority": "Najniższy priorytet",
"LabelMatchConfidence": "Zaufanie",
"LabelMatchExistingUsersBy": "Dopasuje istniejących użytkowników poprzez",
"LabelMatchExistingUsersByDescription": "Służy do łączenia istniejących użytkowników. Po połączeniu użytkownicy zostaną dopasowani za pomocą unikalnego identyfikatora od dostawcy SSO",
"LabelMaxEpisodesToDownload": "Maksymalna liczba odcinków do pobrania. Użyj 0, aby wyłączyć ograniczenie.",
@@ -465,6 +476,7 @@
"LabelNewestAuthors": "Najnowsi autorzy",
"LabelNewestEpisodes": "Najnowsze odcinki",
"LabelNextBackupDate": "Data kolejnej kopii zapasowej",
"LabelNextChapters": "Następny odcinek:",
"LabelNextScheduledRun": "Następne uruchomienie",
"LabelNoApiKeys": "Brak kluczy API",
"LabelNoCustomMetadataProviders": "Brak niestandardowych dostawców metadanych",
@@ -482,6 +494,7 @@
"LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień",
"LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.",
"LabelNumberOfBooks": "Liczba książek",
"LabelNumberOfChapters": "Liczba rozdziałów:",
"LabelNumberOfEpisodes": "# Odcinków",
"LabelOpenIDAdvancedPermsClaimDescription": "Nazwa deklaracji OpenID zawierającej zaawansowane uprawnienia do działań użytkownika w aplikacji, które będą miały zastosowanie do ról innych niż administracyjne (<b>jeśli skonfigurowano</b>). Jeśli deklaracja nie zostanie uwzględniona w odpowiedzi, dostęp do ABS zostanie zablokowany. Brak jednej opcji zostanie uznany za <code>fałsz</code>. Upewnij się, że deklaracja dostawcy tożsamości jest zgodna z oczekiwaną strukturą:",
"LabelOpenIDClaims": "Pozostaw poniższe opcje puste, aby wyłączyć zaawansowane przypisywanie grup i uprawnień. Automatycznie zostanie przypisana grupa „Użytkownik”.",
@@ -559,9 +572,11 @@
"LabelSelectUsers": "Wybór użytkowników",
"LabelSendEbookToDevice": "Wyślij ebook do...",
"LabelSequence": "Kolejność",
"LabelSerial": "Seria",
"LabelSeries": "Serie",
"LabelSeriesName": "Nazwy serii",
"LabelSeriesProgress": "Postęp w serii",
"LabelServerLogLevel": "Poziom logowania servera",
"LabelServerYearReview": "Podsumowanie serwera w roku ({0})",
"LabelSetEbookAsPrimary": "Ustaw jako pierwszy",
"LabelSetEbookAsSupplementary": "Ustaw jako dodatkowy",
@@ -605,6 +620,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana",
"LabelSettingsTimeFormat": "Format czasu",
"LabelShare": "Udostępnij",
"LabelShareDownloadableHelp": "Użytkownicy mogą przy pomocy linka ściągnąć archiwum ZIP pozycji biblioteki",
"LabelShareOpen": "Otwórz udział",
"LabelShareURL": "Link do udziału",
"LabelShowAll": "Pokaż wszystko",
@@ -619,6 +635,7 @@
"LabelStartTime": "Czas rozpoczęcia",
"LabelStarted": "Rozpoczęty",
"LabelStartedAt": "Rozpoczęto",
"LabelStartedDate": "Rozpoczęto {0}",
"LabelStatsAudioTracks": "Ścieżki audio",
"LabelStatsAuthors": "Autorzy",
"LabelStatsBestDay": "Najlepszy dzień",
@@ -641,11 +658,14 @@
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
"LabelTagsNotAccessibleToUser": "Znaczniki niedostępne dla użytkownika",
"LabelTasks": "Uruchomione zadania",
"LabelTextEditorBulletedList": "Lista punktowana",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Lista numerowana",
"LabelTextEditorUnlink": "Usuń link",
"LabelTheme": "Kompozycja",
"LabelThemeDark": "Ciemny",
"LabelThemeLight": "Jasny",
"LabelThemeSepia": "Sepia",
"LabelTimeDurationXHours": "{0} godzin",
"LabelTimeDurationXMinutes": "{0} minuty",
"LabelTimeDurationXSeconds": "{0} sekundy",
@@ -668,7 +688,12 @@
"LabelTrackFromFilename": "Ścieżka z nazwy pliku",
"LabelTrackFromMetadata": "Ścieżka z metadanych",
"LabelTracks": "Ścieżki",
"LabelTracksMultiTrack": "Wielościeżkowy",
"LabelTracksNone": "Brak ścieżek",
"LabelTracksSingleTrack": "Pojedyncza ścieżka",
"LabelTrailer": "Zwiastun",
"LabelType": "Typ",
"LabelUnabridged": "Pełna wersja",
"LabelUndo": "Wycofaj",
"LabelUnknown": "Nieznany",
"LabelUnknownPublishDate": "Nieznana data publikacji",
@@ -681,8 +706,10 @@
"LabelUploaderDragAndDropFilesOnly": "Przeciągnij i upuść pliki",
"LabelUploaderDropFiles": "Puść pliki",
"LabelUploaderItemFetchMetadataHelp": "Automatycznie pobierz tytuł, autora i serie",
"LabelUseAdvancedOptions": "Opcje zaawansowane",
"LabelUseChapterTrack": "Użyj ścieżki rozdziału",
"LabelUseFullTrack": "Użycie ścieżki rozdziału",
"LabelUseZeroForUnlimited": "Użyj 0, aby wyłączyć ograniczenia",
"LabelUser": "Użytkownik",
"LabelUsername": "Nazwa użytkownika",
"LabelValue": "Wartość",
@@ -701,8 +728,11 @@
"LabelYourBookmarks": "Twoje zakładki",
"LabelYourPlaylists": "Twoje playlisty",
"LabelYourProgress": "Twój postęp",
"MessageAddToPlayerQueue": "Dodaj do kolejki odtwarzania",
"MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> albo innego rozwiązania, które obsługuje schemat zapytań Apprise. <br />URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem <code>http://192.168.1.1:8337</code> to wpisany tutaj URL powinien mieć postać: <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Upewnij się, że używasz ASIN z poprawnego regionu Audible, nie Amazona.",
"MessageAuthenticationLegacyTokenWarning": "Starsze tokeny API zostaną w przyszłości usunięte. Zamiast nich należy używać <a href=\"/config/api-keys\">kluczy API</a>.",
"MessageAuthenticationOIDCChangesRestart": "Zrestartuj serwer aby zastosować zmiany w OIDC.",
"MessageAuthenticationSecurityMessage": "Uwierzytelnianie zostało ulepszone ze względów bezpieczeństwa. Wszyscy użytkownicy muszą się ponownie zalogować.",
"MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w <code>/metadata/items</code> & <code>/metadata/authors</code>. Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.",
"MessageBackupsLocationEditNote": "Uwaga: Zmiana lokalizacji kopii zapasowej nie przenosi ani nie modyfikuje istniejących kopii zapasowych",
@@ -716,6 +746,7 @@
"MessageBookshelfNoResultsForFilter": "Nie znaleziono żadnych pozycji przy aktualnym filtrowaniu \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Brak wyników zapytania",
"MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii",
"MessageBulkChapterPattern": "Ile rozdziałów chcesz dodać przy pomocy tego wzorca numeracji?",
"MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka",
"MessageChapterErrorFirstNotZero": "Pierwszy rozdział musi rozpoczynać się na 0",
"MessageChapterErrorStartGteDuration": "Nieprawidłowy czas rozpoczęcia, musi być krótszy niż długość audiobooka",
@@ -778,6 +809,7 @@
"MessageFeedURLWillBe": "URL kanału: {0}",
"MessageFetching": "Pobieranie...",
"MessageForceReScanDescription": "przeskanuje wszystkie pliki ponownie, jak przy świeżym skanowaniu. Tagi ID3 plików audio, pliki OPF i pliki tekstowe będą skanowane jak nowe.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} słucha</strong> na {1}",
"MessageImportantNotice": "Ważna informacja!",
"MessageInsertChapterBelow": "Wstaw rozdział poniżej",
"MessageInvalidAsin": "Nieprawidłowy ASIN",
@@ -848,13 +880,34 @@
"MessageResetChaptersConfirm": "Czy na pewno chcesz zresetować rozdziały i cofnąć wprowadzone zmiany?",
"MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu",
"MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisanie bazy danych w folderze /config oraz okładek w folderze /metadata/items & /metadata/authors.<br /><br />Kopie zapasowe nie modyfikują żadnego pliku w folderach z plikami audio. Jeśli włączyłeś ustawienia serwera, aby przechowywać okładki i metadane w folderach biblioteki, to nie są one zapisywane w kopii zapasowej lub nadpisywane<br /><br />Wszyscy klienci korzystający z Twojego serwera będą automatycznie odświeżani.",
"MessageScheduleLibraryScanNote": "W przypadku większości użytkowników zaleca się pozostawienie tej funkcji wyłączonej i włączenie opcji monitorowania folderów. Monitor folderów automatycznie wykrywa zmiany w folderach biblioteki. Monitor folderów nie działa w przypadku wszystkich systemów plików (np. NFS), dlatego zamiast niego można używać zaplanowanych skanowań biblioteki.",
"MessageScheduleRunEveryWeekdayAtTime": "Uruchom w każdy {0} o {1}",
"MessageSearchResultsFor": "Wyniki wyszukiwania dla",
"MessageSelected": "{0} wybranych",
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
"MessageSetChaptersFromTracksDescription": "Ustaw rozdziały, używając każdego pliku audio jako rozdziału, a tytuł rozdziału jako nazwy pliku audio.",
"MessageShareExpirationWillBe": "Czas udostępniania <strong>{0}</strong>",
"MessageShareExpiresIn": "Wygaśnie za {0}",
"MessageShareURLWillBe": "Udostępnione pod linkiem <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?",
"MessageTaskAudioFileNotWritable": "Plik audio \"{0}\" jest niemodyfikowalny",
"MessageTaskCanceledByUser": "Zadanie anulowane przez użytkownika",
"MessageTaskDownloadingEpisodeDescription": "Ściąganie odcinka \"{0}\"",
"MessageTaskEmbeddingMetadata": "Wbudowywanie medatanych",
"MessageTaskEmbeddingMetadataDescription": "Wbudowywanie metadanych do audiobooka \"{0}\"",
"MessageTaskEncodingM4b": "Kodowanie M4B",
"MessageTaskEncodingM4bDescription": "Konwersja audiobooka \"{0}\" do pojedynczego pliku m4b",
"MessageTaskFailed": "Niepowodzenie",
"MessageTaskFailedToBackupAudioFile": "Nieudana próba wykonania kopii zapasowego pliku audio \"{0}\"",
"MessageTaskFailedToCreateCacheDirectory": "Nie udało się utworzyć katalogu cache",
"MessageTaskFailedToEmbedMetadataInFile": "Nie udało się wbudować metadanych do pliku \"{0}\"",
"MessageTaskFailedToWriteMetadataFile": "Niepowodzenie zapisania pliku metadanych",
"MessageTaskNoFilesToScan": "Brak plików do skanowania",
"MessageTaskScanItemsAdded": "Dodano {0}",
"MessageTaskScanItemsMissing": "Brakuje {0}",
"MessageTaskScanItemsUpdated": "Zaktualizowano {0}",
"MessageTaskScanNoChangesNeeded": "Brak zmian",
"MessageTaskTargetDirectoryNotWritable": "Brak prawa zapisu do folderu docelowego",
"MessageThinking": "Myślę...",
"MessageUploaderItemFailed": "Nie udało się przesłać",
"MessageUploaderItemSuccess": "Przesłanie powiodło się!",
@@ -912,6 +965,21 @@
"ToastBookmarkRemoveSuccess": "Zakładka została usunięta",
"ToastCollectionRemoveSuccess": "Kolekcja usunięta",
"ToastCollectionUpdateSuccess": "Zaktualizowano kolekcję",
"ToastCoverSearchFailed": "Nieudane wyszukiwanie okładki",
"ToastCoverUpdateFailed": "Nieudana aktualizacja okładki",
"ToastDateTimeInvalidOrIncomplete": "Niepoprawna data i czas",
"ToastDeleteFileFailed": "Usunięcie pliku nie powiodło się",
"ToastDeleteFileSuccess": "Plik został usunięty",
"ToastDeviceAddFailed": "Nieudana próba dodania urządzenia",
"ToastDeviceNameAlreadyExists": "Czytnik z taką nazwą już istnieje",
"ToastDeviceTestEmailFailed": "NIeudana próba wysłania testowego maila",
"ToastDeviceTestEmailSuccess": "Testowy email został wysłany",
"ToastEmailSettingsUpdateSuccess": "Ustawienia email zaktualizowane",
"ToastEpisodeDownloadQueueClearSuccess": "Wyczyszczono kolejkę epizodów do ściągnięcia",
"ToastEpisodeUpdateSuccess": "Zaktualizowano {0} odcinków",
"ToastInvalidImageUrl": "Nieprawidłowy URL obrazu",
"ToastInvalidUrl": "Nieprawidłowy URL",
"ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe",
"ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę",
"ToastItemDetailsUpdateSuccess": "Zaktualizowano szczegóły",
"ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako ukończone",
@@ -925,12 +993,28 @@
"ToastLibraryScanFailedToStart": "Nie udało się rozpocząć skanowania",
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
"ToastMatchAllAuthorsFailed": "Nie udało się dopasować wszystkich autorów",
"ToastMustHaveAtLeastOnePath": "Musi mieć przynajmniej jedną ścieżkę",
"ToastNameEmailRequired": "Nazwa i email są wymagane",
"ToastNameRequired": "Imię jest wymagane",
"ToastNewApiKeyUserError": "Trzeba wybrać użytkownika",
"ToastNewEpisodesFound": "Znaleziono {0} nowych odcinków",
"ToastNewUserCreatedFailed": "Nie udało się utworzyć konta: \"{0}\"",
"ToastNewUserCreatedSuccess": "Utworzono nowe konto",
"ToastNewUserLibraryError": "Trzeba wybrać co najmniej jedną bibliotekę",
"ToastNewUserPasswordError": "Hasło jest wymagane, jedynie użytkownik \"root\" może posiadać puste hasło",
"ToastNewUserTagError": "Trzeba wybrać chociaż jeden tag",
"ToastNewUserUsernameError": "Wprowadź nazwę użytkownika",
"ToastNoNewEpisodesFound": "Nie znaleziono nowych odcinków",
"ToastNoRSSFeed": "Podcast nie posiada RSS Feed",
"ToastNotificationFailedMaximum": "Maks. ilość nieudanych prób musi być >= 0",
"ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
"ToastPlaylistCreateSuccess": "Playlista utworzona",
"ToastPlaylistRemoveSuccess": "Playlista usunięta",
"ToastPlaylistUpdateSuccess": "Playlista zaktualizowana",
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
"ToastPodcastEpisodeUpdated": "Zaktualizowano odcinki",
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
"ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji",
+1
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "Adicionar",
"ButtonAddApiKey": "Adicionar Chave API",
"ButtonAddChapters": "Adicionar Capítulos",
"ButtonAddDevice": "Adicionar Dispositivo",
"ButtonAddLibrary": "Adicionar Biblioteca",
+5 -3
View File
@@ -436,9 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Нет {0}",
"LabelLibraryItem": "Элемент библиотеки",
"LabelLibraryName": "Имя библиотеки",
"LabelLibrarySortByProgress": "Прогресс обновлён",
"LabelLibrarySortByProgressFinished": "Дата завершения",
"LabelLibrarySortByProgressStarted": "Дата начала",
"LabelLibrarySortByProgress": "Прогресс: Последнее обновление",
"LabelLibrarySortByProgressFinished": "Прогресс: Завершено",
"LabelLibrarySortByProgressStarted": "Прогресс: Начато",
"LabelLimit": "Лимит",
"LabelLineSpacing": "Межстрочный интервал",
"LabelListenAgain": "Послушать снова",
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Не удалось добавить элемент(ы) в коллекцию",
"ToastCollectionRemoveSuccess": "Коллекция удалена",
"ToastCollectionUpdateSuccess": "Коллекция обновлена",
"ToastConnectionNotAvailable": "Подключение недоступно. Пожалуйста попробуйте позже",
"ToastCoverSearchFailed": "Ошибка поиска обложки",
"ToastCoverUpdateFailed": "Не удалось обновить обложку",
"ToastDateTimeInvalidOrIncomplete": "Дата и время указаны неверно или не до конца",
"ToastDeleteFileFailed": "Не удалось удалить файл",
+27 -1
View File
@@ -638,6 +638,7 @@
"LabelStartTime": "Čas spustenia",
"LabelStarted": "Začaté",
"LabelStartedAt": "Začaté v",
"LabelStartedDate": "Začaté {0}",
"LabelStatsAudioTracks": "Zvukové stopy",
"LabelStatsAuthors": "Autori",
"LabelStatsBestDay": "Najlepší deň",
@@ -667,6 +668,7 @@
"LabelTheme": "Téma",
"LabelThemeDark": "Tmavá",
"LabelThemeLight": "Svetlá",
"LabelThemeSepia": "Sépia",
"LabelTimeBase": "Časová základňa",
"LabelTimeDurationXHours": "{0} hodín",
"LabelTimeDurationXMinutes": "{0} minút",
@@ -735,7 +737,9 @@
"MessageAddToPlayerQueue": "Pridať do zoznamu prehrávania",
"MessageAppriseDescription": "Aby ste mohli používať túto funkciumusíte mať k dispozícii inštanciu <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> alebo inú, ktorá dokáže spracovávať rovnaké požiadavky/requesty.<br/>Apprise URL musí byť úplná URL určená na zasielanie notifikácií, tj. ak napr. vaša APi beží na <code>http://192.168.1.1:8337</code>, vložte do daného poľa <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Uistite sa, že používate ASIN zo správneho regiónu Audible, nie Amazonu.",
"MessageAuthenticationLegacyTokenWarning": "Zastaralé API toleny budú v budúcnosti odstránené. Použite miesto nich <a href=\"/config/api-keys\">API kľúče</a>.",
"MessageAuthenticationOIDCChangesRestart": "Reštartujte svoj server po uložení, aby mohli byť použité zmeny OIDC.",
"MessageAuthenticationSecurityMessage": "Overovanie bolo kvôli bezpečnosti vylepšené. Všetci používatelia sa musia znova prihlásiť.",
"MessageBackupsDescription": "Zálohy pokrývajú používateľov, ich aktuálne stavy počúvania, detaily položiek knižnice, nastavenia servera a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>neobsahujú</strong> súbory v priečinkoch vašich knižníc.",
"MessageBackupsLocationEditNote": "Poznámka: Zmena umiestnenia záloh nepresunie ani nezmení existujúce zálohy",
"MessageBackupsLocationNoEditNote": "Poznámka: Umietnenie záloh je nastavené prostredníctvom premennej prostredia a nie je ho možné zmeniť z tohto miesta.",
@@ -749,6 +753,7 @@
"MessageBookshelfNoResultsForFilter": "Žiadny výsledok filtrovania \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Žiadne výsledky dopytu",
"MessageBookshelfNoSeries": "Nemáte žiadne série",
"MessageBulkChapterPattern": "Koľko ďalších kapitol si želáte pridať s týmto spôsobom číslovania?",
"MessageChapterEndIsAfter": "Koniec kapitoly je až za koncom vašej audioknihy",
"MessageChapterErrorFirstNotZero": "Prvá kapitola musí začínať na 0",
"MessageChapterErrorStartGteDuration": "Neplatný čas začiatku musí byť menší ako celkové trvanie audioknihy",
@@ -757,6 +762,7 @@
"MessageChaptersNotFound": "Kapitoly nenájdené",
"MessageCheckingCron": "Kontrola cron-u...",
"MessageConfirmCloseFeed": "Ste si istý, že chcete zavrieť tento zdroj?",
"MessageConfirmDeleteApiKey": "Ste si istý, že chcete zmazať API kľúč \"{0}\"?",
"MessageConfirmDeleteBackup": "Ste si istý, že chcete zmazať zálohu {0}?",
"MessageConfirmDeleteDevice": "Ste si istý, že chcete zmazať zariadenie čítačky e-kníh \"{0}\"?",
"MessageConfirmDeleteFile": "Týmto odstránite súbor z vášho súborového systému. Ste si istý?",
@@ -810,6 +816,8 @@
"MessageFeedURLWillBe": "URL zdroja bude {0}",
"MessageFetching": "Získavam...",
"MessageForceReScanDescription": "preskenuje všetky súbory ako pri prvom skenovaní. ID3 štítky zvukových súborov, OPF súbory a textové súbory budú nanovo naskenované.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} počúvajúcich</strong> na {1}",
"MessageHeatmapNoListeningSessions": "Žiadne relácie počúvania na {0}",
"MessageImportantNotice": "Dôležité upozornenie!",
"MessageInsertChapterBelow": "Vložte kapitolu nižšie",
"MessageInvalidAsin": "Neplatné ASIN",
@@ -949,6 +957,7 @@
"NotificationOnRSSFeedDisabledDescription": "Spustí sa, keď je automatické sťahovanie epizód pozastavené z dôvodu veľkého počtu zlyhaní",
"NotificationOnRSSFeedFailedDescription": "Spustí sa v prípade, keď zlyhá požiadavka RSS zdroja na automatické stiahnutie epizódy",
"NotificationOnTestDescription": "Udalosť určená na testovanie systému notifikácií",
"PlaceholderBulkChapterInput": "Zadajte názov kapitoly alebo použite číslovanie (napr., 'Epizóda 1', 'Kapitola 10', '1.')",
"PlaceholderNewCollection": "Názov novej zbierky",
"PlaceholderNewFolderPath": "Umiestnenie nového priečinka",
"PlaceholderNewPlaylist": "Názov nového playlistu",
@@ -1002,8 +1011,12 @@
"ToastBookmarkCreateFailed": "Vytvorenie záložky zlyhalo",
"ToastBookmarkCreateSuccess": "Záložka pridaná",
"ToastBookmarkRemoveSuccess": "Záložka odstránená",
"ToastBulkChapterInvalidCount": "Zadajte číslo medzi 1 a 150",
"ToastCachePurgeFailed": "Vyčistenie vyrovnávacej pamäte zlyhalo",
"ToastCachePurgeSuccess": "Vyrovnávacia pamäť vyčistená",
"ToastChapterLocked": "Kapitola je zamknutá.",
"ToastChapterStartTimeAdjusted": "Čas začiatku kapitoly upravený o {0} sek.",
"ToastChaptersAllLocked": "Všetky kapitoly sú zamknuté. Odomknite niektoré kapitoly, aby ste posunuli ich časy.",
"ToastChaptersHaveErrors": "Kapitoly obsahujú chyby",
"ToastChaptersInvalidShiftAmountLast": "Neplatná hodnota veľkosti posunutia. Začiatok poslednej kapitoly by ležal za koncom audioknihy.",
"ToastChaptersInvalidShiftAmountStart": "Nesprávna hodnota posunutia. Prvá kapitola by mala nulovú alebo zápornú dĺžku a bola by nahradená nasledujúcou kapitolou. Navýšte čas začiatku druhej kapitoly.",
@@ -1028,6 +1041,8 @@
"ToastEpisodeDownloadQueueClearSuccess": "Poradie sťahovania bolo vyčistené",
"ToastEpisodeUpdateSuccess": "{0} epizód bolo aktualizovaných",
"ToastErrorCannotShare": "Na tomto zariadení nie je možné zdielať vybraným spôsobom",
"ToastFailedToCreate": "Vytvorenie zlyhalo",
"ToastFailedToDelete": "Zmazanie zlyhalo",
"ToastFailedToLoadData": "Načítanie údajov zlyhalo",
"ToastFailedToMatch": "Spárovanie zlyhalo",
"ToastFailedToShare": "Zdieľanie zlyhalo",
@@ -1035,6 +1050,7 @@
"ToastInvalidImageUrl": "Neplatná URL obrázku",
"ToastInvalidMaxEpisodesToDownload": "Neplatný maximálny počet epizód na stiahnutie",
"ToastInvalidUrl": "Neplatná URL",
"ToastInvalidUrls": "Jedna alebo viac URL liniek sú neplatné",
"ToastItemCoverUpdateSuccess": "Prebal položky bol aktualizovaný",
"ToastItemDeletedFailed": "Odstránenie položky zlyhalo",
"ToastItemDeletedSuccess": "Položka bola odstránená",
@@ -1059,6 +1075,7 @@
"ToastMustHaveAtLeastOnePath": "Musí mať aspoň jednu cestu umiestnenia",
"ToastNameEmailRequired": "Meno a e-mail sú povinné",
"ToastNameRequired": "Meno je povinné",
"ToastNewApiKeyUserError": "Musíte vybrať používateľa",
"ToastNewEpisodesFound": "Bolo nájdených {0} nových epizód",
"ToastNewUserCreatedFailed": "Vytvorenie účtu zlyhalo: \"{0}\"",
"ToastNewUserCreatedSuccess": "Nový účet bol vytvorený",
@@ -1083,6 +1100,7 @@
"ToastPlaylistUpdateSuccess": "Playlist bol aktualizovaný",
"ToastPodcastCreateFailed": "Vytvorenie podcastu zlyhalo",
"ToastPodcastCreateSuccess": "Podcast bol vytvorený",
"ToastPodcastEpisodeUpdated": "Epizóda bola aktualizovaná",
"ToastPodcastGetFeedFailed": "Získanie zdroja podcastu zlyhalo",
"ToastPodcastNoEpisodesInFeed": "Na RSS zdroji neboli nájdené žiadne epizódy",
"ToastPodcastNoRssFeed": "Podcast nemá RSS zdroj",
@@ -1133,5 +1151,13 @@
"ToastUserPasswordChangeSuccess": "Zmena hesla prebehla úspešne",
"ToastUserPasswordMismatch": "Heslá sa nezhodujú",
"ToastUserPasswordMustChange": "Nové heslo sa nesmie zhodovať so starým",
"ToastUserRootRequireName": "Musíte zadať používateľské meno root používateľa"
"ToastUserRootRequireName": "Musíte zadať používateľské meno root používateľa",
"TooltipAddChapters": "Pridať kapitolu(-y)",
"TooltipAddOneSecond": "Pridať 1 sekundu",
"TooltipAdjustChapterStart": "Kliknite, ak chcete zmeniť začiatočný čas",
"TooltipLockAllChapters": "Zamknúť všetky kapitoly",
"TooltipLockChapter": "Zamknúť kapitolu (Shift+klik pre skupinu)",
"TooltipSubtractOneSecond": "Odobrať 1 sekundu",
"TooltipUnlockAllChapters": "Odomknúť všetky kapitoly",
"TooltipUnlockChapter": "Odomknúť kapitolu (Shift+klik pre skupinu)"
}
+2
View File
@@ -1008,6 +1008,8 @@
"ToastCollectionItemsAddFailed": "Misslyckades med att addera böcker till samlingen",
"ToastCollectionRemoveSuccess": "Samlingen har raderats",
"ToastCollectionUpdateSuccess": "Samlingen har uppdaterats",
"ToastConnectionNotAvailable": "Uppkopplingen är inte tillgänglig. Var vänlig försök senare.",
"ToastCoverSearchFailed": "Sökningen efter omslag misslyckades",
"ToastCoverUpdateFailed": "Uppdatering av omslag misslyckades",
"ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett",
"ToastDeleteFileFailed": "Misslyckades att radera filen",
+971 -142
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Не вдалося додати елемент(и) до колекції",
"ToastCollectionRemoveSuccess": "Добірку видалено",
"ToastCollectionUpdateSuccess": "Добірку оновлено",
"ToastConnectionNotAvailable": "З’єднання недоступне. Спробуйте пізніше",
"ToastCoverSearchFailed": "Пошук обкладинки не вдався",
"ToastCoverUpdateFailed": "Не вдалося оновити обкладинку",
"ToastDateTimeInvalidOrIncomplete": "Дата й час недійсні або неповні",
"ToastDeleteFileFailed": "Не вдалося видалити файл",
+2
View File
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "项目添加到收藏夹失败",
"ToastCollectionRemoveSuccess": "收藏夹已删除",
"ToastCollectionUpdateSuccess": "收藏夹已更新",
"ToastConnectionNotAvailable": "连接不可用. 请稍后重试",
"ToastCoverSearchFailed": "封面搜索失败",
"ToastCoverUpdateFailed": "封面更新失败",
"ToastDateTimeInvalidOrIncomplete": "日期和时间无效或不完整",
"ToastDeleteFileFailed": "删除文件失败",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
"version": "2.29.0",
"version": "2.30.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
"version": "2.29.0",
"version": "2.30.0",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "2.29.0",
"version": "2.30.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
@@ -11,6 +11,7 @@
"client": "cd client && npm ci && npm run generate",
"prod": "npm run client && npm ci && node index.js",
"build-win": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
"build-win-no-compress": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf .",
"build-linux": "build/linuxpackager",
"docker": "docker buildx build --platform linux/amd64,linux/arm64 --push . -t advplyr/audiobookshelf",
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
+1 -2
View File
@@ -407,8 +407,7 @@ class Server {
const nextApp = next({ dev: Logger.isDev, dir: ReactClientPath })
const handle = nextApp.getRequestHandler()
await nextApp.prepare()
router.get('*', (req, res) => handle(req, res))
router.post('/internal-api/*', (req, res) => handle(req, res))
router.all('*', (req, res) => handle(req, res))
}
const unixSocketPrefix = 'unix/'
+104
View File
@@ -2,6 +2,7 @@ const SocketIO = require('socket.io')
const Logger = require('./Logger')
const Database = require('./Database')
const TokenManager = require('./auth/TokenManager')
const CoverSearchManager = require('./managers/CoverSearchManager')
/**
* @typedef SocketClient
@@ -180,6 +181,10 @@ class SocketAuthority {
// Scanning
socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId))
// Cover search streaming
socket.on('search_covers', (payload) => this.handleCoverSearch(socket, payload))
socket.on('cancel_cover_search', (requestId) => this.handleCancelCoverSearch(socket, requestId))
// Logs
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
@@ -200,6 +205,10 @@ class SocketAuthority {
const disconnectTime = Date.now() - _client.connected_at
Logger.info(`[SocketAuthority] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
// Cancel any active cover searches for this socket
this.cancelSocketCoverSearches(socket.id)
delete this.clients[socket.id]
}
})
@@ -300,5 +309,100 @@ class SocketAuthority {
Logger.debug('[SocketAuthority] Cancel scan', id)
this.Server.cancelLibraryScan(id)
}
/**
* Handle cover search request via WebSocket
* @param {SocketIO.Socket} socket
* @param {Object} payload
*/
async handleCoverSearch(socket, payload) {
const client = this.clients[socket.id]
if (!client?.user) {
Logger.error('[SocketAuthority] Unauthorized cover search request')
socket.emit('cover_search_error', {
requestId: payload.requestId,
error: 'Unauthorized'
})
return
}
const { requestId, title, author, provider, podcast } = payload
if (!requestId || !title) {
Logger.error('[SocketAuthority] Invalid cover search request')
socket.emit('cover_search_error', {
requestId,
error: 'Invalid request parameters'
})
return
}
Logger.info(`[SocketAuthority] User ${client.user.username} initiated cover search ${requestId}`)
// Callback for streaming results to client
const onResult = (result) => {
socket.emit('cover_search_result', {
requestId,
provider: result.provider,
covers: result.covers,
total: result.total
})
}
// Callback when search completes
const onComplete = () => {
Logger.info(`[SocketAuthority] Cover search ${requestId} completed`)
socket.emit('cover_search_complete', { requestId })
}
// Callback for provider errors
const onError = (provider, errorMessage) => {
socket.emit('cover_search_provider_error', {
requestId,
provider,
error: errorMessage
})
}
// Start the search
CoverSearchManager.startSearch(requestId, { title, author, provider, podcast }, onResult, onComplete, onError).catch((error) => {
Logger.error(`[SocketAuthority] Cover search ${requestId} failed:`, error)
socket.emit('cover_search_error', {
requestId,
error: error.message
})
})
}
/**
* Handle cancel cover search request
* @param {SocketIO.Socket} socket
* @param {string} requestId
*/
handleCancelCoverSearch(socket, requestId) {
const client = this.clients[socket.id]
if (!client?.user) {
Logger.error('[SocketAuthority] Unauthorized cancel cover search request')
return
}
Logger.info(`[SocketAuthority] User ${client.user.username} cancelled cover search ${requestId}`)
const cancelled = CoverSearchManager.cancelSearch(requestId)
if (cancelled) {
socket.emit('cover_search_cancelled', { requestId })
}
}
/**
* Cancel all cover searches associated with a socket (called on disconnect)
* @param {string} socketId
*/
cancelSocketCoverSearches(socketId) {
// Get all active search request IDs and cancel those that might belong to this socket
// Since we don't track socket-to-request mapping, we log this for debugging
// The client will handle reconnection gracefully
Logger.debug(`[SocketAuthority] Socket ${socketId} disconnected, any active searches will timeout`)
}
}
module.exports = new SocketAuthority()
+9 -1
View File
@@ -11,7 +11,7 @@ const { levenshteinDistance, levenshteinSimilarity, escapeRegExp, isValidASIN }
const htmlSanitizer = require('../utils/htmlSanitizer')
class BookFinder {
#providerResponseTimeout = 30000
#providerResponseTimeout = 10000
constructor() {
this.openLibrary = new OpenLibrary()
@@ -608,6 +608,14 @@ class BookFinder {
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
searchResults.push(...providerResults)
}
} else if (provider === 'best') {
// Best providers: google, fantlab, and audible.com
const bestProviders = ['google', 'fantlab', 'audible']
for (const providerString of bestProviders) {
const providerResults = await this.search(null, providerString, title, author, options)
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
searchResults.push(...providerResults)
}
} else {
searchResults = await this.search(null, provider, title, author, options)
}
+251
View File
@@ -0,0 +1,251 @@
const { setMaxListeners } = require('events')
const Logger = require('../Logger')
const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
/**
* Manager for handling streaming cover search across multiple providers
*/
class CoverSearchManager {
constructor() {
/** @type {Map<string, AbortController>} Map of requestId to AbortController */
this.activeSearches = new Map()
// Default timeout for each provider search
this.providerTimeout = 10000 // 10 seconds
// Set to 0 to disable the max listeners limit
// We need one listener per provider (15+) and may have multiple concurrent searches
this.maxListeners = 0
}
/**
* Start a streaming cover search
* @param {string} requestId - Unique identifier for this search request
* @param {Object} searchParams - Search parameters
* @param {string} searchParams.title - Title to search for
* @param {string} searchParams.author - Author to search for (optional)
* @param {string} searchParams.provider - Provider to search (or 'all')
* @param {boolean} searchParams.podcast - Whether this is a podcast search
* @param {Function} onResult - Callback for each result chunk
* @param {Function} onComplete - Callback when search completes
* @param {Function} onError - Callback for errors
*/
async startSearch(requestId, searchParams, onResult, onComplete, onError) {
if (this.activeSearches.has(requestId)) {
Logger.warn(`[CoverSearchManager] Search with requestId ${requestId} already exists`)
return
}
const abortController = new AbortController()
// Increase max listeners on this signal to accommodate parallel provider searches
// AbortSignal is an EventTarget, so we use the events module's setMaxListeners
setMaxListeners(this.maxListeners, abortController.signal)
this.activeSearches.set(requestId, abortController)
Logger.info(`[CoverSearchManager] Starting search ${requestId} with params:`, searchParams)
try {
const { title, author, provider, podcast } = searchParams
if (podcast) {
await this.searchPodcastCovers(requestId, title, abortController.signal, onResult, onError)
} else {
await this.searchBookCovers(requestId, provider, title, author, abortController.signal, onResult, onError)
}
if (!abortController.signal.aborted) {
onComplete()
}
} catch (error) {
if (error.name === 'AbortError') {
Logger.info(`[CoverSearchManager] Search ${requestId} was cancelled`)
} else {
Logger.error(`[CoverSearchManager] Search ${requestId} failed:`, error)
onError(error.message)
}
} finally {
this.activeSearches.delete(requestId)
}
}
/**
* Cancel an active search
* @param {string} requestId - Request ID to cancel
*/
cancelSearch(requestId) {
const abortController = this.activeSearches.get(requestId)
if (abortController) {
Logger.info(`[CoverSearchManager] Cancelling search ${requestId}`)
abortController.abort()
this.activeSearches.delete(requestId)
return true
}
return false
}
/**
* Search for podcast covers
*/
async searchPodcastCovers(requestId, title, signal, onResult, onError) {
try {
const results = await this.executeWithTimeout(() => PodcastFinder.findCovers(title), this.providerTimeout, signal)
if (signal.aborted) return
const covers = this.extractCoversFromResults(results)
if (covers.length > 0) {
onResult({
provider: 'itunes',
covers,
total: covers.length
})
}
} catch (error) {
if (error.name !== 'AbortError') {
Logger.error(`[CoverSearchManager] Podcast search failed:`, error)
onError('itunes', error.message)
}
}
}
/**
* Search for book covers across providers
*/
async searchBookCovers(requestId, provider, title, author, signal, onResult, onError) {
let providers = []
if (provider === 'all') {
providers = [...BookFinder.providers]
} else if (provider === 'best') {
// Best providers: google, fantlab, and audible.com
providers = ['google', 'fantlab', 'audible']
} else {
providers = [provider]
}
Logger.debug(`[CoverSearchManager] Searching ${providers.length} providers in parallel`)
// Search all providers in parallel
const searchPromises = providers.map(async (providerName) => {
if (signal.aborted) return
try {
const searchResults = await this.executeWithTimeout(() => BookFinder.search(null, providerName, title, author || ''), this.providerTimeout, signal)
if (signal.aborted) return
const covers = this.extractCoversFromResults(searchResults)
Logger.debug(`[CoverSearchManager] Found ${covers.length} covers from ${providerName}`)
if (covers.length > 0) {
onResult({
provider: providerName,
covers,
total: covers.length
})
}
} catch (error) {
if (error.name !== 'AbortError') {
Logger.warn(`[CoverSearchManager] Provider ${providerName} failed:`, error.message)
onError(providerName, error.message)
}
}
})
await Promise.allSettled(searchPromises)
}
/**
* Execute a promise with timeout and abort signal
*/
async executeWithTimeout(fn, timeout, signal) {
return new Promise(async (resolve, reject) => {
let abortHandler = null
let timeoutId = null
// Cleanup function to ensure we always remove listeners
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
if (abortHandler) {
signal.removeEventListener('abort', abortHandler)
abortHandler = null
}
}
// Set up timeout
timeoutId = setTimeout(() => {
cleanup()
const error = new Error('Provider timeout')
error.name = 'TimeoutError'
reject(error)
}, timeout)
// Check if already aborted
if (signal.aborted) {
cleanup()
const error = new Error('Search cancelled')
error.name = 'AbortError'
reject(error)
return
}
// Set up abort handler
abortHandler = () => {
cleanup()
const error = new Error('Search cancelled')
error.name = 'AbortError'
reject(error)
}
signal.addEventListener('abort', abortHandler)
try {
const result = await fn()
cleanup()
resolve(result)
} catch (error) {
cleanup()
reject(error)
}
})
}
/**
* Extract cover URLs from search results
*/
extractCoversFromResults(results) {
const covers = []
if (!Array.isArray(results)) return covers
results.forEach((result) => {
if (result.covers && Array.isArray(result.covers)) {
covers.push(...result.covers)
}
if (result.cover) {
covers.push(result.cover)
}
})
// Remove duplicates
return [...new Set(covers)]
}
/**
* Cancel all active searches (cleanup on server shutdown)
*/
cancelAllSearches() {
Logger.info(`[CoverSearchManager] Cancelling ${this.activeSearches.size} active searches`)
for (const [requestId, abortController] of this.activeSearches.entries()) {
abortController.abort()
}
this.activeSearches.clear()
}
}
module.exports = new CoverSearchManager()
+3 -3
View File
@@ -3,7 +3,7 @@ const Logger = require('../Logger')
const { isValidASIN } = require('../utils/index')
class Audible {
#responseTimeout = 30000
#responseTimeout = 10000
constructor() {
this.regionMap = {
@@ -106,7 +106,7 @@ class Audible {
return res.data
})
.catch((error) => {
Logger.error('[Audible] ASIN search error', error)
Logger.error('[Audible] ASIN search error', error.message)
return null
})
}
@@ -158,7 +158,7 @@ class Audible {
return Promise.all(res.data.products.map((result) => this.asinSearch(result.asin, region, timeout)))
})
.catch((error) => {
Logger.error('[Audible] query search error', error)
Logger.error('[Audible] query search error', error.message)
return []
})
}
+2 -2
View File
@@ -2,7 +2,7 @@ const axios = require('axios')
const Logger = require('../Logger')
class AudiobookCovers {
#responseTimeout = 30000
#responseTimeout = 10000
constructor() {}
@@ -24,7 +24,7 @@ class AudiobookCovers {
})
.then((res) => res?.data || [])
.catch((error) => {
Logger.error('[AudiobookCovers] Cover search error', error)
Logger.error('[AudiobookCovers] Cover search error', error.message)
return []
})
return items.map((item) => ({ cover: item.versions.png.original }))
+3 -3
View File
@@ -55,7 +55,7 @@ class Audnexus {
return this._processRequest(this.limiter(() => axios.get(authorRequestUrl)))
.then((res) => res.data || [])
.catch((error) => {
Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error)
Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error.message)
return []
})
}
@@ -82,7 +82,7 @@ class Audnexus {
return this._processRequest(this.limiter(() => axios.get(authorRequestUrl.toString())))
.then((res) => res.data)
.catch((error) => {
Logger.error(`[Audnexus] Author request failed for ${asin}`, error)
Logger.error(`[Audnexus] Author request failed for ${asin}`, error.message)
return null
})
}
@@ -158,7 +158,7 @@ class Audnexus {
return this._processRequest(this.limiter(() => axios.get(chaptersRequestUrl.toString())))
.then((res) => res.data)
.catch((error) => {
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error)
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error.message)
return null
})
}
+2 -2
View File
@@ -4,7 +4,7 @@ const Logger = require('../Logger')
const htmlSanitizer = require('../utils/htmlSanitizer')
class CustomProviderAdapter {
#responseTimeout = 30000
#responseTimeout = 10000
constructor() {}
@@ -61,7 +61,7 @@ class CustomProviderAdapter {
return res.data.matches
})
.catch((error) => {
Logger.error('[CustomMetadataProvider] Search error', error)
Logger.error('[CustomMetadataProvider] Search error', error.message)
return []
})
+4 -4
View File
@@ -2,7 +2,7 @@ const axios = require('axios')
const Logger = require('../Logger')
class FantLab {
#responseTimeout = 30000
#responseTimeout = 10000
// 7 - other
// 11 - essay
// 12 - article
@@ -48,7 +48,7 @@ class FantLab {
return res.data || []
})
.catch((error) => {
Logger.error('[FantLab] search error', error)
Logger.error('[FantLab] search error', error.message)
return []
})
@@ -77,7 +77,7 @@ class FantLab {
return resp.data || null
})
.catch((error) => {
Logger.error(`[FantLab] work info request for url "${url}" error`, error)
Logger.error(`[FantLab] work info request for url "${url}" error`, error.message)
return null
})
@@ -193,7 +193,7 @@ class FantLab {
return resp.data || null
})
.catch((error) => {
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error)
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error.message)
return null
})
+2 -2
View File
@@ -2,7 +2,7 @@ const axios = require('axios')
const Logger = require('../Logger')
class GoogleBooks {
#responseTimeout = 30000
#responseTimeout = 10000
constructor() {}
@@ -67,7 +67,7 @@ class GoogleBooks {
return res.data.items
})
.catch((error) => {
Logger.error('[GoogleBooks] Volume search error', error)
Logger.error('[GoogleBooks] Volume search error', error.message)
return []
})
return items.map((item) => this.cleanResult(item))
+2 -2
View File
@@ -1,7 +1,7 @@
const axios = require('axios').default
class OpenLibrary {
#responseTimeout = 30000
#responseTimeout = 10000
constructor() {
this.baseUrl = 'https://openlibrary.org'
@@ -23,7 +23,7 @@ class OpenLibrary {
return res.data
})
.catch((error) => {
console.error('Failed', error)
console.error('Failed', error.message)
return null
})
}
+2 -2
View File
@@ -28,7 +28,7 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
*/
class iTunes {
#responseTimeout = 30000
#responseTimeout = 10000
constructor() {}
@@ -63,7 +63,7 @@ class iTunes {
return response.data.results || []
})
.catch((error) => {
Logger.error(`[iTunes] search request error`, error)
Logger.error(`[iTunes] search request error`, error.message)
return []
})
}