From fbe9971a8b4fa3606ef8dbd8262a40ba03d40f3c Mon Sep 17 00:00:00 2001 From: votex001 Date: Wed, 3 Sep 2025 18:19:52 +0300 Subject: [PATCH 001/285] [fix] prevent duplicates in multi-selects --- client/components/ui/MultiSelect.vue | 2 +- client/components/ui/MultiSelectQueryInput.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index c7572ba50..3dcfb0495 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -278,7 +278,7 @@ export default { }) }, insertNewItem(item) { - this.selected.push(item) + if (!this.selected.includes(item)) this.selected.push(item) this.$emit('input', this.selected) this.$emit('newItem', item) this.textInput = null diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index 18abc66e7..4bc434cbd 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -287,7 +287,7 @@ export default { }) }, insertNewItem(item) { - this.selected.push(item) + if (!this.selected.find((i) => i.name === item.name)) this.selected.push(item) this.$emit('input', this.selected) this.$emit('newItem', item) this.textInput = null From 9ce6de3100bb2e0db1354f08c00c45085bcbb9d0 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:00:57 +0200 Subject: [PATCH 002/285] Added progress to upload --- client/components/cards/ItemUploadCard.vue | 56 +++++++++++++++++++++- client/components/ui/LoadingIndicator.vue | 9 +++- client/pages/upload/index.vue | 23 ++++++++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 40836b8e4..cb9dd5589 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -62,7 +62,24 @@
- + +
+ + {{ nonInteractionLabel }} + +
+ +
+
+ + {{ uploadProgressText }} + +
+
+
+
+
+
@@ -91,7 +108,11 @@ export default { isUploading: false, uploadFailed: false, uploadSuccess: false, - isFetchingMetadata: false + isFetchingMetadata: false, + uploadProgress: { + loaded: 0, + total: 0 + } } }, computed: { @@ -116,13 +137,44 @@ export default { } else if (this.isFetchingMetadata) { return this.$strings.LabelFetchingMetadata } + }, + uploadProgressPercent() { + if (this.uploadProgress.total === 0) return 0 + return Math.min(100, Math.round((this.uploadProgress.loaded / this.uploadProgress.total) * 100)) + }, + uploadProgressText() { + const loaded = this.formatBytes(this.uploadProgress.loaded) + const total = this.formatBytes(this.uploadProgress.total) + return `${this.uploadProgressPercent}% (${loaded} / ${total})` } }, methods: { + formatBytes(bytes) { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + }, setUploadStatus(status) { this.isUploading = status === 'uploading' this.uploadFailed = status === 'failed' this.uploadSuccess = status === 'success' + + if (status !== 'uploading') { + this.uploadProgress = { + loaded: 0, + total: 0 + } + } + }, + setUploadProgress(progress) { + if (this.isUploading && progress) { + this.uploadProgress = { + loaded: progress.loaded || 0, + total: progress.total || 0 + } + } }, titleUpdated() { this.error = '' diff --git a/client/components/ui/LoadingIndicator.vue b/client/components/ui/LoadingIndicator.vue index d984bf35e..3b4b6e049 100644 --- a/client/components/ui/LoadingIndicator.vue +++ b/client/components/ui/LoadingIndicator.vue @@ -1,5 +1,5 @@ @@ -23,6 +25,9 @@ export default { computed: { message() { return this.text || this.$strings.MessagePleaseWait + }, + hasSlotContent() { + return this.$slots.default && this.$slots.default.length > 0 } } } diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index eef05b608..63e133137 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -297,6 +297,15 @@ export default { ref.setUploadStatus(status) } }, + updateItemCardProgress(index, progress) { + var ref = this.$refs[`itemCard-${index}`] + if (ref && ref.length) ref = ref[0] + if (!ref) { + console.error('Book card ref not found', index, this.$refs) + } else { + ref.setUploadProgress(progress) + } + }, async uploadItem(item) { var form = new FormData() form.set('title', item.title) @@ -312,8 +321,20 @@ export default { form.set(`${index++}`, file) }) + const config = { + onUploadProgress: (progressEvent) => { + if (progressEvent.lengthComputable) { + const progress = { + loaded: progressEvent.loaded, + total: progressEvent.total + } + this.updateItemCardProgress(item.index, progress) + } + } + } + return this.$axios - .$post('/api/upload', form) + .$post('/api/upload', form, config) .then(() => true) .catch((error) => { console.error('Failed to upload item', error) From 3e876e3383ece4fdec80a74fb85d13a2a42fb55c Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 8 Oct 2025 14:24:50 -0500 Subject: [PATCH 003/285] Add Turkish to the language option --- client/plugins/i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 861091757..e4c179016 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -29,6 +29,7 @@ const languageCodeMap = { ru: { label: 'Русский', dateFnsLocale: 'ru' }, sl: { label: 'Slovenščina', dateFnsLocale: 'sl' }, sv: { label: 'Svenska', dateFnsLocale: 'sv' }, + tr: { label: 'Türkçe', dateFnsLocale: 'tr' }, uk: { label: 'Українська', dateFnsLocale: 'uk' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, From cafd92e206a07b50121feea67b75ac4959d99fbb Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 8 Oct 2025 14:52:14 -0500 Subject: [PATCH 004/285] Fix item edit modal show next/prev arrows when opening from Files or Match context menu item #4718 --- client/components/app/BookShelfRow.vue | 4 ++-- client/components/app/LazyBookshelf.vue | 4 ++-- client/components/cards/LazyBookCard.vue | 4 ++-- client/components/widgets/ItemSlider.vue | 4 ++-- client/mixins/bookshelfCardsHelpers.js | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/components/app/BookShelfRow.vue b/client/components/app/BookShelfRow.vue index 082f9fe37..fac89a70b 100644 --- a/client/components/app/BookShelfRow.vue +++ b/client/components/app/BookShelfRow.vue @@ -93,10 +93,10 @@ export default { editAuthor(author) { this.$store.commit('globals/showEditAuthorModal', author) }, - editItem(libraryItem) { + editItem(libraryItem, tab = 'details') { var itemIds = this.shelf.entities.map((e) => e.id) this.$store.commit('setBookshelfBookIds', itemIds) - this.$store.commit('showEditModal', libraryItem) + this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' }) }, editEpisode({ libraryItem, episode }) { this.$store.commit('setEpisodeTableEpisodeIds', [episode.id]) diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 854b61b24..4c72d0d78 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -232,11 +232,11 @@ export default { clearFilter() { this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' }) }, - editEntity(entity) { + editEntity(entity, tab = 'details') { if (this.entityName === 'items' || this.entityName === 'series-books') { const bookIds = this.entities.map((e) => e.id) this.$store.commit('setBookshelfBookIds', bookIds) - this.$store.commit('showEditModal', entity) + this.$store.commit('showEditModalOnTab', { libraryItem: entity, tab: tab || 'details' }) } else if (this.entityName === 'collections') { this.$store.commit('globals/setEditCollection', entity) } else if (this.entityName === 'playlists') { diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index fbb50bb14..d06f083ba 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -788,11 +788,11 @@ export default { }, showEditModalFiles() { // More menu func - this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'files' }) + this.$emit('edit', this.libraryItem, 'files') }, showEditModalMatch() { // More menu func - this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' }) + this.$emit('edit', this.libraryItem, 'match') }, sendToDevice(deviceName) { // More menu func diff --git a/client/components/widgets/ItemSlider.vue b/client/components/widgets/ItemSlider.vue index 880654a44..5b96e51b4 100644 --- a/client/components/widgets/ItemSlider.vue +++ b/client/components/widgets/ItemSlider.vue @@ -132,10 +132,10 @@ export default { editAuthor(author) { this.$store.commit('globals/showEditAuthorModal', author) }, - editItem(libraryItem) { + editItem(libraryItem, tab = 'details') { var itemIds = this.items.map((e) => e.id) this.$store.commit('setBookshelfBookIds', itemIds) - this.$store.commit('showEditModal', libraryItem) + this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' }) }, selectItem(payload) { this.$emit('selectEntity', payload) diff --git a/client/mixins/bookshelfCardsHelpers.js b/client/mixins/bookshelfCardsHelpers.js index fc8a41256..f1571f70e 100644 --- a/client/mixins/bookshelfCardsHelpers.js +++ b/client/mixins/bookshelfCardsHelpers.js @@ -118,8 +118,8 @@ export default { propsData: props, parent: this, created() { - this.$on('edit', (entity) => { - if (_this.editEntity) _this.editEntity(entity) + this.$on('edit', (entity, tab) => { + if (_this.editEntity) _this.editEntity(entity, tab) }) this.$on('select', ({ entity, shiftKey }) => { if (_this.selectEntity) _this.selectEntity(entity, shiftKey) From 37beb7b37c8ea27ee86b04a311008786ec25fcd0 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 8 Oct 2025 15:03:33 -0500 Subject: [PATCH 005/285] Disable chapter editor chapter play button when time is invalid #4691 --- client/pages/audiobook/_id/chapters.vue | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index cf254165b..4d6c0e41d 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -117,10 +117,10 @@ - @@ -594,6 +594,14 @@ export default { this.hasChanges = hasChanges }, + getAudioTrackForTime(time) { + if (typeof time !== 'number') { + return null + } + return this.tracks.find((at) => { + return time >= at.startOffset && time < at.startOffset + at.duration + }) + }, playChapter(chapter) { console.log('Play Chapter', chapter.id) if (this.selectedChapterId === chapter.id) { @@ -608,9 +616,12 @@ export default { this.destroyAudioEl() } - const audioTrack = this.tracks.find((at) => { - return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration - }) + const audioTrack = this.getAudioTrackForTime(chapter.start) + if (!audioTrack) { + console.error('No audio track found for chapter', chapter) + return + } + this.selectedChapter = chapter this.isLoadingChapter = true From 2592467d09146aae6d664466eee461fcd62f2298 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 8 Oct 2025 15:32:37 -0500 Subject: [PATCH 006/285] Fix: Always re-load libraries when changing users #4694 --- client/pages/login.vue | 1 + client/store/libraries.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index ef3827afe..60f31eca5 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -189,6 +189,7 @@ export default { require('@/plugins/chromecast.js').default(this) } + this.$store.commit('libraries/setLastLoad', 0) // Ensure libraries get loaded again when switching users this.$store.commit('libraries/setCurrentLibrary', { id: userDefaultLibraryId }) this.$store.commit('user/setUser', user) // Access token only returned from login, not authorize diff --git a/client/store/libraries.js b/client/store/libraries.js index 62c515ebf..115fb53bf 100644 --- a/client/store/libraries.js +++ b/client/store/libraries.js @@ -159,7 +159,7 @@ export const actions = { .$get(`/api/libraries`) .then((data) => { commit('set', data.libraries) - commit('setLastLoad') + commit('setLastLoad', new Date()) }) .catch((error) => { console.error('Failed', error) @@ -176,8 +176,8 @@ export const mutations = { setFoldersLastUpdate(state) { state.folderLastUpdate = Date.now() }, - setLastLoad(state) { - state.lastLoad = Date.now() + setLastLoad(state, date) { + state.lastLoad = date }, setLibraryIssues(state, val) { state.issues = val From 4224f44259da03000a924c3ed382fa358909ecb1 Mon Sep 17 00:00:00 2001 From: Finn Dittmar Date: Thu, 9 Oct 2025 08:49:13 +0200 Subject: [PATCH 007/285] Remove duplicate (and also wrong byte conversion) --- client/components/cards/ItemUploadCard.vue | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index cb9dd5589..adbf3adbe 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -143,19 +143,12 @@ export default { return Math.min(100, Math.round((this.uploadProgress.loaded / this.uploadProgress.total) * 100)) }, uploadProgressText() { - const loaded = this.formatBytes(this.uploadProgress.loaded) - const total = this.formatBytes(this.uploadProgress.total) + const loaded = this.$bytesPretty(this.uploadProgress.loaded) + const total = this.$bytesPretty(this.uploadProgress.total) return `${this.uploadProgressPercent}% (${loaded} / ${total})` } }, methods: { - formatBytes(bytes) { - if (bytes === 0) return '0 Bytes' - const k = 1024 - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] - }, setUploadStatus(status) { this.isUploading = status === 'uploading' this.uploadFailed = status === 'failed' From fc06aa2c788fd47957cffda3047eb2ef9f18c8ce Mon Sep 17 00:00:00 2001 From: Frank de Lange Date: Thu, 9 Oct 2025 16:02:02 +0200 Subject: [PATCH 008/285] Explicitly launch OpenID Connect authentication with ?autoLaunch=1 This change extends OIDC authentication by enabling explicit redirection to the OAuth provider when navigating to the login page with the manual override parameter (/login?autoLaunch=1). Use case: directly launch audiobookshelf from within e.g. Nextcloud using the external sites app (use something like https://abs.example.org/login?autoLaunch=1 as URL) while keeping the possibility to launch audiobookshelf using its built-in authentication mechanism. Assuming the username or mail address used in Nextcloud and audiobookshelf are identical the user will be logged in to his or her account no matter which method is used. --- client/pages/login.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 60f31eca5..8e5cde098 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -299,8 +299,8 @@ export default { } if (authMethods.includes('openid')) { - // Auto redirect unless query string ?autoLaunch=0 - if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') { + // Auto redirect unless query string ?autoLaunch=0 OR when explicity requested through ?autoLaunch=1 + if ((this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') || this.$route.query?.autoLaunch == '1') { window.location.href = this.openidAuthUri } From 797dba244892b3455a9506a44298446844a0f814 Mon Sep 17 00:00:00 2001 From: Frank de Lange Date: Fri, 10 Oct 2025 22:30:38 +0200 Subject: [PATCH 009/285] fix #1617 (useradd: user 'audiobookshelf' already exists) This change fixes the problem of failing upgrades on dpkg-based systems by reworking the check for whether the `audiobookshelf` user/group already exists. --- build/debian/DEBIAN/preinst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/debian/DEBIAN/preinst b/build/debian/DEBIAN/preinst index e30bc490c..241a47010 100644 --- a/build/debian/DEBIAN/preinst +++ b/build/debian/DEBIAN/preinst @@ -22,7 +22,7 @@ add_user() { declare -r descr="${4:-No description}" declare -r shell="${5:-/bin/false}" - if ! getent passwd | grep -q "^$user:"; then + if ! getent passwd "$user" 2>&1 >/dev/null; then echo "Creating system user: $user in $group with $descr and shell $shell" useradd $uid_flags --gid $group --no-create-home --system --shell $shell -c "$descr" $user fi @@ -39,7 +39,7 @@ add_group() { declare -r gid_flags="--gid $gid" fi - if ! getent group | grep -q "^$group:" ; then + if ! getent group "$group" 2>&1 >/dev/null; then echo "Creating system group: $group" groupadd $gid_flags --system $group fi From feed827223bf78e25f8ca015af57f76d46fecc3e Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 10 Oct 2025 18:00:37 -0500 Subject: [PATCH 010/285] Update settings update endpoint to validate allowedOrigins is array --- server/controllers/MiscController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index c779bdd63..490cb27d2 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -142,6 +142,9 @@ class MiscController { Logger.warn('Cannot disable iframe when ALLOW_IFRAME is enabled in environment') return res.status(400).send('Cannot disable iframe when ALLOW_IFRAME is enabled in environment') } + if (settingsUpdate.allowedOrigins && !Array.isArray(settingsUpdate.allowedOrigins)) { + return res.status(400).send('allowedOrigins must be an array') + } const madeUpdates = Database.serverSettings.update(settingsUpdate) if (madeUpdates) { From a87ea327151fcb7ec67ce981bb6a44878a9ab343 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 13 Oct 2025 09:50:01 -0500 Subject: [PATCH 011/285] Fix admin user unable to close sessions of other users #4746 --- server/controllers/SessionController.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 872f635de..7cc8d35e2 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -339,9 +339,9 @@ class SessionController { var playbackSession = this.playbackSessionManager.getSession(req.params.id) if (!playbackSession) return res.sendStatus(404) - if (playbackSession.userId !== req.user.id) { - Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`) - return res.sendStatus(404) + if (playbackSession.userId !== req.user.id && !req.user.isAdminOrUp) { + Logger.error(`[SessionController] Non-admin user "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`) + return res.sendStatus(403) } req.playbackSession = playbackSession From 4f30cbf2f652362f2fcb4ac1baa1af3104ad9fb5 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 14 Oct 2025 18:09:32 +0300 Subject: [PATCH 012/285] SearchController: New providers API, query param validation --- server/controllers/SearchController.js | 298 ++++++++++++++++++++++--- 1 file changed, 267 insertions(+), 31 deletions(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index bb3382f71..72f602d2a 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -6,6 +6,28 @@ const AuthorFinder = require('../finders/AuthorFinder') const Database = require('../Database') const { isValidASIN } = require('../utils') +// Provider name mappings for display purposes +const providerMap = { + all: 'All', + best: 'Best', + google: 'Google Books', + itunes: 'iTunes', + openlibrary: 'Open Library', + fantlab: 'FantLab.ru', + audiobookcovers: 'AudiobookCovers.com', + audible: 'Audible.com', + 'audible.ca': 'Audible.ca', + 'audible.uk': 'Audible.co.uk', + 'audible.au': 'Audible.com.au', + 'audible.fr': 'Audible.fr', + 'audible.de': 'Audible.de', + 'audible.jp': 'Audible.co.jp', + 'audible.it': 'Audible.it', + 'audible.in': 'Audible.in', + 'audible.es': 'Audible.es', + audnexus: 'Audnexus' +} + /** * @typedef RequestUserObject * @property {import('../models/User')} user @@ -16,6 +38,100 @@ const { isValidASIN } = require('../utils') class SearchController { constructor() {} + /** + * Validates that multiple parameters are strings + * @param {Object} params - Object with param names as keys and values to validate + * @param {string} methodName - Name of the calling method for logging + * @returns {{valid: boolean, error?: {status: number, message: string}}} + */ + static validateStringParams(params, methodName) { + for (const [key, value] of Object.entries(params)) { + if (typeof value !== 'string') { + Logger.error(`[SearchController] ${methodName}: Invalid ${key} parameter`) + return { + valid: false, + error: { + status: 400, + message: 'Invalid request query params' + } + } + } + } + return { valid: true } + } + + /** + * Validates that a required string parameter exists and is a string + * @param {any} value - Value to validate + * @param {string} paramName - Parameter name for logging + * @param {string} methodName - Name of the calling method for logging + * @returns {{valid: boolean, error?: {status: number, message: string}}} + */ + static validateRequiredString(value, paramName, methodName) { + if (!value || typeof value !== 'string') { + Logger.error(`[SearchController] ${methodName}: Invalid or missing ${paramName}`) + return { + valid: false, + error: { + status: 400, + message: `Invalid or missing ${paramName}` + } + } + } + return { valid: true } + } + + /** + * Validates and fetches a library item by ID + * @param {string} id - Library item ID + * @param {string} methodName - Name of the calling method for logging + * @returns {Promise<{valid: boolean, libraryItem?: any, error?: {status: number, message: string}}>} + */ + static async fetchAndValidateLibraryItem(id, methodName) { + const validation = SearchController.validateRequiredString(id, 'library item id', methodName) + if (!validation.valid) { + return validation + } + + const libraryItem = await Database.libraryItemModel.getExpandedById(id) + if (!libraryItem) { + Logger.error(`[SearchController] ${methodName}: Library item not found with id "${id}"`) + return { + valid: false, + error: { + status: 404, + message: 'Library item not found' + } + } + } + + return { valid: true, libraryItem } + } + + /** + * Maps custom metadata providers to standardized format + * @param {Array} providers - Array of custom provider objects + * @returns {Array<{value: string, text: string}>} + */ + static mapCustomProviders(providers) { + return providers.map((provider) => ({ + value: provider.getSlug(), + text: provider.name + })) + } + + /** + * Static helper method to format provider for client (for use in array methods) + * @param {string} providerValue - Provider identifier + * @returns {{value: string, text: string}} + */ + static formatProvider(providerValue) { + return { + value: providerValue, + text: providerMap[providerValue] || providerValue + } + } + /** * GET: /api/search/books * @@ -23,18 +139,19 @@ class SearchController { * @param {Response} res */ async findBooks(req, res) { - const id = req.query.id - const libraryItem = await Database.libraryItemModel.getExpandedById(id) const provider = req.query.provider || 'google' const title = req.query.title || '' const author = req.query.author || '' - if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') { - Logger.error(`[SearchController] findBooks: Invalid request query params`) - return res.status(400).send('Invalid request query params') - } + // Validate string parameters + const validation = SearchController.validateStringParams({ provider, title, author }, 'findBooks') + if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) - const results = await BookFinder.search(libraryItem, provider, title, author) + // Fetch and validate library item + const itemValidation = await SearchController.fetchAndValidateLibraryItem(req.query.id, 'findBooks') + if (!itemValidation.valid) return res.status(itemValidation.error.status).send(itemValidation.error.message) + + const results = await BookFinder.search(itemValidation.libraryItem, provider, title, author) res.json(results) } @@ -46,19 +163,23 @@ class SearchController { */ async findCovers(req, res) { const query = req.query - const podcast = query.podcast == 1 + const podcast = query.podcast === '1' || query.podcast === 1 + const title = query.title || '' + const author = query.author || '' + const provider = query.provider || 'google' - if (!query.title || typeof query.title !== 'string') { - Logger.error(`[SearchController] findCovers: Invalid title sent in query`) - return res.sendStatus(400) - } + // Validate required title + const titleValidation = SearchController.validateRequiredString(title, 'title', 'findCovers') + if (!titleValidation.valid) return res.status(titleValidation.error.status).send(titleValidation.error.message) + + // Validate other string parameters + const validation = SearchController.validateStringParams({ author, provider }, 'findCovers') + if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) let results = null - if (podcast) results = await PodcastFinder.findCovers(query.title) - else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || '') - res.json({ - results - }) + if (podcast) results = await PodcastFinder.findCovers(title) + else results = await BookFinder.findCovers(provider, title, author) + res.json({ results }) } /** @@ -71,14 +192,16 @@ class SearchController { async findPodcasts(req, res) { const term = req.query.term const country = req.query.country || 'us' - if (!term) { - Logger.error('[SearchController] Invalid request query param "term" is required') - return res.status(400).send('Invalid request query param "term" is required') - } - const results = await PodcastFinder.search(term, { - country - }) + // Validate required term + const termValidation = SearchController.validateRequiredString(term, 'term', 'findPodcasts') + if (!termValidation.valid) return res.status(termValidation.error.status).send(termValidation.error.message) + + // Validate country parameter + const validation = SearchController.validateStringParams({ country }, 'findPodcasts') + if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) + + const results = await PodcastFinder.search(term, { country }) res.json(results) } @@ -90,10 +213,10 @@ class SearchController { */ async findAuthor(req, res) { const query = req.query.q - if (!query || typeof query !== 'string') { - Logger.error(`[SearchController] findAuthor: Invalid query param`) - return res.status(400).send('Invalid query param') - } + + // Validate query parameter + const validation = SearchController.validateRequiredString(query, 'query', 'findAuthor') + if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) const author = await AuthorFinder.findAuthorByName(query) res.json(author) @@ -107,15 +230,128 @@ class SearchController { */ async findChapters(req, res) { const asin = req.query.asin - if (!isValidASIN(asin.toUpperCase())) { - return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' }) - } const region = (req.query.region || 'us').toLowerCase() + + // Validate ASIN parameter + const asinValidation = SearchController.validateRequiredString(asin, 'asin', 'findChapters') + if (!asinValidation.valid) return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' }) + + if (!isValidASIN(asin.toUpperCase())) return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' }) + + // Validate region parameter + const validation = SearchController.validateStringParams({ region }, 'findChapters') + if (!validation.valid) res.json({ error: 'Invalid region', stringKey: 'MessageInvalidRegion' }) + const chapterData = await BookFinder.findChapters(asin, region) if (!chapterData) { return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' }) } res.json(chapterData) } + + /** + * GET: /api/search/providers/podcasts/covers + * Get available podcast cover metadata providers + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getPodcastCoverProviders(req, res) { + // Podcast covers only use iTunes + const customProviders = await Database.customMetadataProviderModel.findAll({ + where: { + mediaType: 'podcast' + } + }) + + const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)] + + res.json({ providers }) + } + + /** + * GET: /api/search/providers/books/covers + * Get available book cover metadata providers + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getBookCoverProviders(req, res) { + // Book covers use all book providers + const customProviders = await Database.customMetadataProviderModel.findAll({ + where: { + mediaType: 'book' + } + }) + + const providers = [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders), SearchController.formatProvider('all')] + + res.json({ providers }) + } + + /** + * GET: /api/search/providers/books + * Get available book metadata providers + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getBookProviders(req, res) { + const customProviders = await Database.customMetadataProviderModel.findAll({ + where: { + mediaType: 'book' + } + }) + + // Filter out cover-only providers + const bookProviders = BookFinder.providers.filter((p) => p !== 'audiobookcovers') + + const providers = [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders)] + + res.json({ providers }) + } + + /** + * GET: /api/search/providers/podcasts + * Get available podcast metadata providers + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getPodcastProviders(req, res) { + const customProviders = await Database.customMetadataProviderModel.findAll({ + where: { + mediaType: 'podcast' + } + }) + + const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)] + + res.json({ providers }) + } + + /** + * GET: /api/search/providers/authors + * Get available author metadata providers + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getAuthorProviders(req, res) { + const providers = [SearchController.formatProvider('audnexus')] + res.json({ providers }) + } + + /** + * GET: /api/search/providers/chapters + * Get available chapter metadata providers + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getChapterProviders(req, res) { + const providers = [SearchController.formatProvider('audnexus')] + res.json({ providers }) + } } module.exports = new SearchController() From 1da3ab7fdc819ad71271098bba1f186de058fc7a Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 14 Oct 2025 18:10:12 +0300 Subject: [PATCH 013/285] ApiRouter: New provider API routes --- server/routers/ApiRouter.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 6446ecc80..c72aa143f 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -283,6 +283,12 @@ class ApiRouter { this.router.get('/search/podcast', SearchController.findPodcasts.bind(this)) this.router.get('/search/authors', SearchController.findAuthor.bind(this)) this.router.get('/search/chapters', SearchController.findChapters.bind(this)) + this.router.get('/search/providers/books', SearchController.getBookProviders.bind(this)) + this.router.get('/search/providers/books/covers', SearchController.getBookCoverProviders.bind(this)) + this.router.get('/search/providers/podcasts', SearchController.getPodcastProviders.bind(this)) + this.router.get('/search/providers/podcasts/covers', SearchController.getPodcastCoverProviders.bind(this)) + this.router.get('/search/providers/authors', SearchController.getAuthorProviders.bind(this)) + this.router.get('/search/providers/chapters', SearchController.getChapterProviders.bind(this)) // // Cache Routes (Admin and up) From ce4ff4f894ea5e924ac64429c3fa8fa772f210d0 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 15 Oct 2025 09:52:15 +0300 Subject: [PATCH 014/285] Client: Use new server providers API --- .../modals/BatchQuickMatchModel.vue | 2 +- client/components/modals/item/tabs/Cover.vue | 4 +- client/components/modals/item/tabs/Match.vue | 2 +- .../modals/libraries/EditLibrary.vue | 2 +- .../modals/libraries/LibrarySettings.vue | 2 +- client/layouts/default.vue | 24 ++- client/pages/config/index.vue | 3 +- client/pages/upload/index.vue | 2 +- client/store/libraries.js | 2 - client/store/scanners.js | 171 ++++++------------ 10 files changed, 90 insertions(+), 124 deletions(-) diff --git a/client/components/modals/BatchQuickMatchModel.vue b/client/components/modals/BatchQuickMatchModel.vue index 8bea68faf..138684b20 100644 --- a/client/components/modals/BatchQuickMatchModel.vue +++ b/client/components/modals/BatchQuickMatchModel.vue @@ -88,7 +88,7 @@ export default { }, providers() { if (this.isPodcast) return this.$store.state.scanners.podcastProviders - return this.$store.state.scanners.providers + return this.$store.state.scanners.bookProviders }, libraryProvider() { return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index b404a9abb..4ed734b1b 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -133,8 +133,8 @@ export default { } }, providers() { - if (this.isPodcast) return this.$store.state.scanners.podcastProviders - return [{ text: 'Best', value: 'best' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders, { text: 'All', value: 'all' }] + if (this.isPodcast) return this.$store.state.scanners.podcastCoverProviders + return this.$store.state.scanners.bookCoverProviders }, searchTitleLabel() { if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 3faa26b27..36cdd7a8e 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -319,7 +319,7 @@ export default { }, providers() { if (this.isPodcast) return this.$store.state.scanners.podcastProviders - return this.$store.state.scanners.providers + return this.$store.state.scanners.bookProviders }, searchTitleLabel() { if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN diff --git a/client/components/modals/libraries/EditLibrary.vue b/client/components/modals/libraries/EditLibrary.vue index 083fc5766..1e4d3990a 100644 --- a/client/components/modals/libraries/EditLibrary.vue +++ b/client/components/modals/libraries/EditLibrary.vue @@ -74,7 +74,7 @@ export default { }, providers() { if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders - return this.$store.state.scanners.providers + return this.$store.state.scanners.bookProviders } }, methods: { diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index d3b40de95..231cb5ec4 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -136,7 +136,7 @@ export default { }, providers() { if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders - return this.$store.state.scanners.providers + return this.$store.state.scanners.bookProviders }, maskAsFinishedWhenItems() { return [ diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 4b9729248..21c256bd3 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -371,11 +371,25 @@ export default { }, customMetadataProviderAdded(provider) { if (!provider?.id) return - this.$store.commit('scanners/addCustomMetadataProvider', provider) + // Refetch the appropriate provider types + if (provider.mediaType === 'book') { + this.$store.dispatch('scanners/fetchBookProviders') + this.$store.dispatch('scanners/fetchBookCoverProviders') + } else if (provider.mediaType === 'podcast') { + this.$store.dispatch('scanners/fetchPodcastProviders') + this.$store.dispatch('scanners/fetchPodcastCoverProviders') + } }, customMetadataProviderRemoved(provider) { if (!provider?.id) return - this.$store.commit('scanners/removeCustomMetadataProvider', provider) + // Refetch the appropriate provider types + if (provider.mediaType === 'book') { + this.$store.dispatch('scanners/fetchBookProviders') + this.$store.dispatch('scanners/fetchBookCoverProviders') + } else if (provider.mediaType === 'podcast') { + this.$store.dispatch('scanners/fetchPodcastProviders') + this.$store.dispatch('scanners/fetchPodcastCoverProviders') + } }, initializeSocket() { if (this.$root.socket) { @@ -612,6 +626,12 @@ export default { this.$store.dispatch('libraries/load') + // Fetch metadata providers + this.$store.dispatch('scanners/fetchBookProviders') + this.$store.dispatch('scanners/fetchBookCoverProviders') + this.$store.dispatch('scanners/fetchPodcastProviders') + this.$store.dispatch('scanners/fetchPodcastCoverProviders') + this.initLocalStorage() this.checkVersionUpdate() diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 3d030bb32..099ae9c4e 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -247,7 +247,8 @@ export default { return this.$store.state.serverSettings }, providers() { - return this.$store.state.scanners.providers + // Use book cover providers for the cover provider dropdown + return this.$store.state.scanners.bookCoverProviders || [] }, dateFormats() { return this.$store.state.globals.dateFormats diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index eef05b608..84d4d1478 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -155,7 +155,7 @@ export default { }, providers() { if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders - return this.$store.state.scanners.providers + return this.$store.state.scanners.bookProviders }, canFetchMetadata() { return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled diff --git a/client/store/libraries.js b/client/store/libraries.js index 115fb53bf..a824b6474 100644 --- a/client/store/libraries.js +++ b/client/store/libraries.js @@ -131,8 +131,6 @@ export const actions = { commit('setLibraryIssues', issues) commit('setLibraryFilterData', filterData) commit('setNumUserPlaylists', numUserPlaylists) - commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true }) - commit('setCurrentLibrary', { id: libraryId }) return data }) diff --git a/client/store/scanners.js b/client/store/scanners.js index 2d3d465ca..40a8d864b 100644 --- a/client/store/scanners.js +++ b/client/store/scanners.js @@ -1,126 +1,73 @@ export const state = () => ({ - providers: [ - { - text: 'Google Books', - value: 'google' - }, - { - text: 'Open Library', - value: 'openlibrary' - }, - { - text: 'iTunes', - value: 'itunes' - }, - { - text: 'Audible.com', - value: 'audible' - }, - { - text: 'Audible.ca', - value: 'audible.ca' - }, - { - text: 'Audible.co.uk', - value: 'audible.uk' - }, - { - text: 'Audible.com.au', - value: 'audible.au' - }, - { - text: 'Audible.fr', - value: 'audible.fr' - }, - { - text: 'Audible.de', - value: 'audible.de' - }, - { - text: 'Audible.co.jp', - value: 'audible.jp' - }, - { - text: 'Audible.it', - value: 'audible.it' - }, - { - text: 'Audible.co.in', - value: 'audible.in' - }, - { - text: 'Audible.es', - value: 'audible.es' - }, - { - text: 'FantLab.ru', - value: 'fantlab' - } - ], - podcastProviders: [ - { - text: 'iTunes', - value: 'itunes' - } - ], - coverOnlyProviders: [ - { - text: 'AudiobookCovers.com', - value: 'audiobookcovers' - } - ] + bookProviders: [], + podcastProviders: [], + bookCoverProviders: [], + podcastCoverProviders: [] }) export const getters = { - checkBookProviderExists: state => (providerValue) => { - return state.providers.some(p => p.value === providerValue) + checkBookProviderExists: (state) => (providerValue) => { + return state.bookProviders.some((p) => p.value === providerValue) }, - checkPodcastProviderExists: state => (providerValue) => { - return state.podcastProviders.some(p => p.value === providerValue) + checkPodcastProviderExists: (state) => (providerValue) => { + return state.podcastProviders.some((p) => p.value === providerValue) } } -export const actions = {} - -export const mutations = { - addCustomMetadataProvider(state, provider) { - if (provider.mediaType === 'book') { - if (state.providers.some(p => p.value === provider.slug)) return - state.providers.push({ - text: provider.name, - value: provider.slug - }) - } else { - if (state.podcastProviders.some(p => p.value === provider.slug)) return - state.podcastProviders.push({ - text: provider.name, - value: provider.slug - }) +export const actions = { + async fetchBookProviders({ commit }) { + try { + const response = await this.$axios.$get('/api/search/providers/books') + if (response?.providers) { + commit('setBookProviders', response.providers) + } + } catch (error) { + console.error('Failed to fetch book providers', error) } }, - removeCustomMetadataProvider(state, provider) { - if (provider.mediaType === 'book') { - state.providers = state.providers.filter(p => p.value !== provider.slug) - } else { - state.podcastProviders = state.podcastProviders.filter(p => p.value !== provider.slug) + async fetchPodcastProviders({ commit }) { + try { + const response = await this.$axios.$get('/api/search/providers/podcasts') + if (response?.providers) { + commit('setPodcastProviders', response.providers) + } + } catch (error) { + console.error('Failed to fetch podcast providers', error) } }, - setCustomMetadataProviders(state, providers) { - if (!providers?.length) return - - const mediaType = providers[0].mediaType - if (mediaType === 'book') { - // clear previous values, and add new values to the end - state.providers = state.providers.filter((p) => !p.value.startsWith('custom-')) - state.providers = [ - ...state.providers, - ...providers.map((p) => ({ - text: p.name, - value: p.slug - })) - ] - } else { - // Podcast providers not supported yet + async fetchBookCoverProviders({ commit }) { + try { + const response = await this.$axios.$get('/api/search/providers/books/covers') + if (response?.providers) { + commit('setBookCoverProviders', response.providers) + } + } catch (error) { + console.error('Failed to fetch book cover providers', error) + } + }, + async fetchPodcastCoverProviders({ commit }) { + try { + const response = await this.$axios.$get('/api/search/providers/podcasts/covers') + if (response?.providers) { + commit('setPodcastCoverProviders', response.providers) + } + } catch (error) { + console.error('Failed to fetch podcast cover providers', error) } } -} \ No newline at end of file +} + +export const mutations = { + setBookProviders(state, providers) { + state.bookProviders = providers + }, + setPodcastProviders(state, providers) { + state.podcastProviders = providers + }, + setBookCoverProviders(state, providers) { + state.bookCoverProviders = providers + }, + setPodcastCoverProviders(state, providers) { + state.podcastCoverProviders = providers + } +} From 888190a6be4c103b6eec3e5c0f97f678eda87900 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 15 Oct 2025 18:28:15 +0300 Subject: [PATCH 015/285] Fix codeQL failures --- server/controllers/SearchController.js | 25 +++++++++++++------------ server/finders/BookFinder.js | 7 +++++-- server/utils/index.js | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 72f602d2a..57538d2c0 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -4,7 +4,7 @@ const BookFinder = require('../finders/BookFinder') const PodcastFinder = require('../finders/PodcastFinder') const AuthorFinder = require('../finders/AuthorFinder') const Database = require('../Database') -const { isValidASIN } = require('../utils') +const { isValidASIN, getQueryParamAsString } = require('../utils') // Provider name mappings for display purposes const providerMap = { @@ -139,9 +139,10 @@ class SearchController { * @param {Response} res */ async findBooks(req, res) { - const provider = req.query.provider || 'google' - const title = req.query.title || '' - const author = req.query.author || '' + // Safely extract query parameters, rejecting arrays to prevent type confusion + const provider = getQueryParamAsString(req.query.provider, 'google') + const title = getQueryParamAsString(req.query.title, '') + const author = getQueryParamAsString(req.query.author, '') // Validate string parameters const validation = SearchController.validateStringParams({ provider, title, author }, 'findBooks') @@ -164,9 +165,9 @@ class SearchController { async findCovers(req, res) { const query = req.query const podcast = query.podcast === '1' || query.podcast === 1 - const title = query.title || '' - const author = query.author || '' - const provider = query.provider || 'google' + const title = getQueryParamAsString(query.title, '') + const author = getQueryParamAsString(query.author, '') + const provider = getQueryParamAsString(query.provider, 'google') // Validate required title const titleValidation = SearchController.validateRequiredString(title, 'title', 'findCovers') @@ -190,8 +191,8 @@ class SearchController { * @param {Response} res */ async findPodcasts(req, res) { - const term = req.query.term - const country = req.query.country || 'us' + const term = getQueryParamAsString(req.query.term) + const country = getQueryParamAsString(req.query.country, 'us') // Validate required term const termValidation = SearchController.validateRequiredString(term, 'term', 'findPodcasts') @@ -212,7 +213,7 @@ class SearchController { * @param {Response} res */ async findAuthor(req, res) { - const query = req.query.q + const query = getQueryParamAsString(req.query.q) // Validate query parameter const validation = SearchController.validateRequiredString(query, 'query', 'findAuthor') @@ -229,8 +230,8 @@ class SearchController { * @param {Response} res */ async findChapters(req, res) { - const asin = req.query.asin - const region = (req.query.region || 'us').toLowerCase() + const asin = getQueryParamAsString(req.query.asin) + const region = getQueryParamAsString(req.query.region, 'us').toLowerCase() // Validate ASIN parameter const asinValidation = SearchController.validateRequiredString(asin, 'asin', 'findChapters') diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index a6a6b07e6..6dc90c44f 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -402,7 +402,8 @@ class BookFinder { let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus) // Remove underscores and parentheses with their contents, and replace with a separator - const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, ' - ') + // Use negated character classes to prevent ReDoS vulnerability + const cleanTitle = title.replace(/\[[^\]]*\]|\([^)]*\)|{[^}]*}|_/g, ' - ') // Split title into hypen-separated parts const titleParts = cleanTitle.split(/ - | -|- /) for (const titlePart of titleParts) authorCandidates.add(titlePart) @@ -668,7 +669,9 @@ function cleanTitleForCompares(title, keepSubtitle = false) { let stripped = keepSubtitle ? title : stripSubtitle(title) // Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game") - let cleaned = stripped.replace(/ *\([^)]*\) */g, '') + // Use a safe two-pass approach to prevent ReDoS vulnerability + let cleaned = stripped.replace(/\([^)]*\)/g, '') // Remove parenthetical content + cleaned = cleaned.replace(/\s+/g, ' ').trim() // Clean up any resulting multiple spaces // Remove single quotes (i.e. "Ender's Game" becomes "Enders Game") cleaned = cleaned.replace(/'/g, '') diff --git a/server/utils/index.js b/server/utils/index.js index 369620276..4421fbade 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -277,3 +277,22 @@ module.exports.timestampToSeconds = (timestamp) => { } return null } + +/** + * Safely extracts a query parameter as a string, rejecting arrays to prevent type confusion + * Express query parameters can be arrays if the same parameter appears multiple times + * @example ?author=Smith => "Smith" + * @example ?author=Smith&author=Jones => null (array detected) + * + * @param {any} value - Query parameter value + * @param {string} defaultValue - Default value if undefined/null + * @returns {string|null} String value or null if invalid (array) + */ +module.exports.getQueryParamAsString = (value, defaultValue = '') => { + // Explicitly reject arrays to prevent type confusion + if (Array.isArray(value)) { + return null + } + // Return default for undefined/null, otherwise return the value + return value == null ? defaultValue : value +} From 3f6162f53c3246963cbbaaa1697043a3119cfd89 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 15 Oct 2025 18:54:29 +0300 Subject: [PATCH 016/285] CodeQL fix: limit parameter sizes --- server/finders/BookFinder.js | 9 +++++++-- server/utils/index.js | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 6dc90c44f..fe1a61027 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -385,6 +385,11 @@ class BookFinder { if (!title) return books + // Truncate excessively long inputs to prevent ReDoS attacks + const MAX_INPUT_LENGTH = 500 + title = title.substring(0, MAX_INPUT_LENGTH) + author = author?.substring(0, MAX_INPUT_LENGTH) || author + const isTitleAsin = isValidASIN(title.toUpperCase()) let actualTitleQuery = title @@ -402,7 +407,7 @@ class BookFinder { let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus) // Remove underscores and parentheses with their contents, and replace with a separator - // Use negated character classes to prevent ReDoS vulnerability + // Use negated character classes to prevent ReDoS vulnerability (input length validated at entry point) const cleanTitle = title.replace(/\[[^\]]*\]|\([^)]*\)|{[^}]*}|_/g, ' - ') // Split title into hypen-separated parts const titleParts = cleanTitle.split(/ - | -|- /) @@ -669,7 +674,7 @@ function cleanTitleForCompares(title, keepSubtitle = false) { let stripped = keepSubtitle ? title : stripSubtitle(title) // Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game") - // Use a safe two-pass approach to prevent ReDoS vulnerability + // Use negated character class to prevent ReDoS vulnerability (input length validated at entry point) let cleaned = stripped.replace(/\([^)]*\)/g, '') // Remove parenthetical content cleaned = cleaned.replace(/\s+/g, ' ').trim() // Clean up any resulting multiple spaces diff --git a/server/utils/index.js b/server/utils/index.js index 4421fbade..0661d14f8 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -286,13 +286,21 @@ module.exports.timestampToSeconds = (timestamp) => { * * @param {any} value - Query parameter value * @param {string} defaultValue - Default value if undefined/null - * @returns {string|null} String value or null if invalid (array) + * @param {number} maxLength - Optional maximum length (defaults to 10000 to prevent ReDoS attacks) + * @returns {string|null} String value or null if invalid (array or too long) */ -module.exports.getQueryParamAsString = (value, defaultValue = '') => { +module.exports.getQueryParamAsString = (value, defaultValue = '', maxLength = 1000) => { // Explicitly reject arrays to prevent type confusion if (Array.isArray(value)) { return null } - // Return default for undefined/null, otherwise return the value - return value == null ? defaultValue : value + // Return default for undefined/null + if (value == null) { + return defaultValue + } + // Reject excessively long strings to prevent ReDoS attacks + if (typeof value === 'string' && value.length > maxLength) { + return null + } + return value } From 0a82d6a41b5662ee681bdecf1e922824f427fda5 Mon Sep 17 00:00:00 2001 From: mikiher Date: Fri, 17 Oct 2025 08:11:03 +0300 Subject: [PATCH 017/285] CoverSearchManager: Fix broken podcast cover search --- server/managers/CoverSearchManager.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/managers/CoverSearchManager.js b/server/managers/CoverSearchManager.js index ddcaa23db..193176766 100644 --- a/server/managers/CoverSearchManager.js +++ b/server/managers/CoverSearchManager.js @@ -224,6 +224,9 @@ class CoverSearchManager { if (!Array.isArray(results)) return covers results.forEach((result) => { + if (typeof result === 'string') { + covers.push(result) + } if (result.covers && Array.isArray(result.covers)) { covers.push(...result.covers) } From 0a4de61eff00ae1f4ba102acf4cf2a744192e208 Mon Sep 17 00:00:00 2001 From: Finn Dittmar Date: Sun, 19 Oct 2025 09:22:12 +0200 Subject: [PATCH 018/285] Chnage Auth Expiry --- server/auth/TokenManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index d972b5345..faa6774a3 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -12,9 +12,9 @@ class TokenManager { constructor() { /** @type {number} Refresh token expiry in seconds */ - this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 7 * 24 * 60 * 60 // 7 days + this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 30 * 24 * 60 * 60 // 30 days /** @type {number} Access token expiry in seconds */ - this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 12 * 60 * 60 // 12 hours + this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 1 * 60 * 60 // 1 hour if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) { Logger.info(`[TokenManager] Refresh token expiry set from ENV variable to ${this.RefreshTokenExpiry} seconds`) From 0a8662d1983736ea36fb76a3bfd028f6e2586106 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 19 Oct 2025 10:53:27 +0300 Subject: [PATCH 019/285] Merge providers API into a single endpoint --- server/controllers/SearchController.js | 104 +++---------------------- server/routers/ApiRouter.js | 7 +- 2 files changed, 13 insertions(+), 98 deletions(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 57538d2c0..72d215f34 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -251,108 +251,28 @@ class SearchController { } /** - * GET: /api/search/providers/podcasts/covers - * Get available podcast cover metadata providers + * GET: /api/search/providers + * Get all available metadata providers * * @param {RequestWithUser} req * @param {Response} res */ - async getPodcastCoverProviders(req, res) { - // Podcast covers only use iTunes - const customProviders = await Database.customMetadataProviderModel.findAll({ - where: { - mediaType: 'podcast' - } - }) + async getAllProviders(req, res) { + const customProviders = await Database.customMetadataProviderModel.findAll() - const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)] + const customBookProviders = customProviders.filter((p) => p.mediaType === 'book') + const customPodcastProviders = customProviders.filter((p) => p.mediaType === 'podcast') - res.json({ providers }) - } - - /** - * GET: /api/search/providers/books/covers - * Get available book cover metadata providers - * - * @param {RequestWithUser} req - * @param {Response} res - */ - async getBookCoverProviders(req, res) { - // Book covers use all book providers - const customProviders = await Database.customMetadataProviderModel.findAll({ - where: { - mediaType: 'book' - } - }) - - const providers = [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders), SearchController.formatProvider('all')] - - res.json({ providers }) - } - - /** - * GET: /api/search/providers/books - * Get available book metadata providers - * - * @param {RequestWithUser} req - * @param {Response} res - */ - async getBookProviders(req, res) { - const customProviders = await Database.customMetadataProviderModel.findAll({ - where: { - mediaType: 'book' - } - }) - - // Filter out cover-only providers const bookProviders = BookFinder.providers.filter((p) => p !== 'audiobookcovers') - const providers = [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders)] + // Build minimized payload with custom providers merged in + const providers = { + books: [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders)], + booksCovers: [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders), SearchController.formatProvider('all')], + podcasts: [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customPodcastProviders)] + } res.json({ providers }) } - - /** - * GET: /api/search/providers/podcasts - * Get available podcast metadata providers - * - * @param {RequestWithUser} req - * @param {Response} res - */ - async getPodcastProviders(req, res) { - const customProviders = await Database.customMetadataProviderModel.findAll({ - where: { - mediaType: 'podcast' - } - }) - - const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)] - - res.json({ providers }) - } - - /** - * GET: /api/search/providers/authors - * Get available author metadata providers - * - * @param {RequestWithUser} req - * @param {Response} res - */ - async getAuthorProviders(req, res) { - const providers = [SearchController.formatProvider('audnexus')] - res.json({ providers }) - } - - /** - * GET: /api/search/providers/chapters - * Get available chapter metadata providers - * - * @param {RequestWithUser} req - * @param {Response} res - */ - async getChapterProviders(req, res) { - const providers = [SearchController.formatProvider('audnexus')] - res.json({ providers }) - } } module.exports = new SearchController() diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index c72aa143f..db04bf5ec 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -283,12 +283,7 @@ class ApiRouter { this.router.get('/search/podcast', SearchController.findPodcasts.bind(this)) this.router.get('/search/authors', SearchController.findAuthor.bind(this)) this.router.get('/search/chapters', SearchController.findChapters.bind(this)) - this.router.get('/search/providers/books', SearchController.getBookProviders.bind(this)) - this.router.get('/search/providers/books/covers', SearchController.getBookCoverProviders.bind(this)) - this.router.get('/search/providers/podcasts', SearchController.getPodcastProviders.bind(this)) - this.router.get('/search/providers/podcasts/covers', SearchController.getPodcastCoverProviders.bind(this)) - this.router.get('/search/providers/authors', SearchController.getAuthorProviders.bind(this)) - this.router.get('/search/providers/chapters', SearchController.getChapterProviders.bind(this)) + this.router.get('/search/providers', SearchController.getAllProviders.bind(this)) // // Cache Routes (Admin and up) From b01e7570d398b20e300c87a80d61a82aa903041b Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 19 Oct 2025 10:54:26 +0300 Subject: [PATCH 020/285] Remove custom providers from library filterdata request --- server/controllers/LibraryController.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index e63441f0b..55ef45690 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -221,13 +221,11 @@ class LibraryController { const includeArray = (req.query.include || '').split(',') if (includeArray.includes('filterdata')) { const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id) - const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType) return res.json({ filterdata, issues: filterdata.numIssues, numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), - customMetadataProviders, library: req.library.toOldJSON() }) } From 141211590f15c2ac6485e6ed09e8fe0f57a048f8 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 19 Oct 2025 11:39:10 +0300 Subject: [PATCH 021/285] Merge provider actions and mutations, add loaded state --- client/store/scanners.js | 67 ++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/client/store/scanners.js b/client/store/scanners.js index 40a8d864b..ccf7d9249 100644 --- a/client/store/scanners.js +++ b/client/store/scanners.js @@ -2,7 +2,8 @@ export const state = () => ({ bookProviders: [], podcastProviders: [], bookCoverProviders: [], - podcastCoverProviders: [] + podcastCoverProviders: [], + providersLoaded: false }) export const getters = { @@ -11,63 +12,49 @@ export const getters = { }, checkPodcastProviderExists: (state) => (providerValue) => { return state.podcastProviders.some((p) => p.value === providerValue) - } + }, + areProvidersLoaded: (state) => state.providersLoaded } export const actions = { - async fetchBookProviders({ commit }) { + async fetchProviders({ commit, state }) { + // Only fetch if not already loaded + if (state.providersLoaded) { + return + } + try { - const response = await this.$axios.$get('/api/search/providers/books') + const response = await this.$axios.$get('/api/search/providers') if (response?.providers) { - commit('setBookProviders', response.providers) + commit('setAllProviders', response.providers) } } catch (error) { - console.error('Failed to fetch book providers', error) + console.error('Failed to fetch providers', error) } }, - async fetchPodcastProviders({ commit }) { - try { - const response = await this.$axios.$get('/api/search/providers/podcasts') - if (response?.providers) { - commit('setPodcastProviders', response.providers) - } - } catch (error) { - console.error('Failed to fetch podcast providers', error) + async refreshProviders({ commit, state }) { + // if providers are not loaded, do nothing - they will be fetched when required ( + if (!state.providersLoaded) { + return } - }, - async fetchBookCoverProviders({ commit }) { + try { - const response = await this.$axios.$get('/api/search/providers/books/covers') + const response = await this.$axios.$get('/api/search/providers') if (response?.providers) { - commit('setBookCoverProviders', response.providers) + commit('setAllProviders', response.providers) } } catch (error) { - console.error('Failed to fetch book cover providers', error) - } - }, - async fetchPodcastCoverProviders({ commit }) { - try { - const response = await this.$axios.$get('/api/search/providers/podcasts/covers') - if (response?.providers) { - commit('setPodcastCoverProviders', response.providers) - } - } catch (error) { - console.error('Failed to fetch podcast cover providers', error) + console.error('Failed to refresh providers', error) } } } export const mutations = { - setBookProviders(state, providers) { - state.bookProviders = providers - }, - setPodcastProviders(state, providers) { - state.podcastProviders = providers - }, - setBookCoverProviders(state, providers) { - state.bookCoverProviders = providers - }, - setPodcastCoverProviders(state, providers) { - state.podcastCoverProviders = providers + setAllProviders(state, providers) { + state.bookProviders = providers.books || [] + state.podcastProviders = providers.podcasts || [] + state.bookCoverProviders = providers.booksCovers || [] + state.podcastCoverProviders = providers.podcasts || [] // Use same as bookCovers since podcasts use iTunes only + state.providersLoaded = true } } From 816a47a4bac71ed4415e4923e99980a0150ce049 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 19 Oct 2025 11:40:40 +0300 Subject: [PATCH 022/285] Remove custom providers from library fetch action --- client/store/libraries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/store/libraries.js b/client/store/libraries.js index a824b6474..a6cf1dd39 100644 --- a/client/store/libraries.js +++ b/client/store/libraries.js @@ -117,7 +117,6 @@ export const actions = { const library = data.library const filterData = data.filterdata const issues = data.issues || 0 - const customMetadataProviders = data.customMetadataProviders || [] const numUserPlaylists = data.numUserPlaylists dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true }) From 166e0442a0e5763f62d5f82d76543fd13ec50510 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 19 Oct 2025 11:47:17 +0300 Subject: [PATCH 023/285] Remove providers prefetch, refresh on custom provider add/remove --- client/layouts/default.vue | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 21c256bd3..75753b214 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -371,25 +371,13 @@ export default { }, customMetadataProviderAdded(provider) { if (!provider?.id) return - // Refetch the appropriate provider types - if (provider.mediaType === 'book') { - this.$store.dispatch('scanners/fetchBookProviders') - this.$store.dispatch('scanners/fetchBookCoverProviders') - } else if (provider.mediaType === 'podcast') { - this.$store.dispatch('scanners/fetchPodcastProviders') - this.$store.dispatch('scanners/fetchPodcastCoverProviders') - } + // Refresh providers cache + this.$store.dispatch('scanners/refreshProviders') }, customMetadataProviderRemoved(provider) { if (!provider?.id) return - // Refetch the appropriate provider types - if (provider.mediaType === 'book') { - this.$store.dispatch('scanners/fetchBookProviders') - this.$store.dispatch('scanners/fetchBookCoverProviders') - } else if (provider.mediaType === 'podcast') { - this.$store.dispatch('scanners/fetchPodcastProviders') - this.$store.dispatch('scanners/fetchPodcastCoverProviders') - } + // Refresh providers cache + this.$store.dispatch('scanners/refreshProviders') }, initializeSocket() { if (this.$root.socket) { @@ -626,12 +614,6 @@ export default { this.$store.dispatch('libraries/load') - // Fetch metadata providers - this.$store.dispatch('scanners/fetchBookProviders') - this.$store.dispatch('scanners/fetchBookCoverProviders') - this.$store.dispatch('scanners/fetchPodcastProviders') - this.$store.dispatch('scanners/fetchPodcastCoverProviders') - this.initLocalStorage() this.checkVersionUpdate() From 538a5065a448336bbf29eccf65101728c1f9c53a Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 19 Oct 2025 18:57:27 +0300 Subject: [PATCH 024/285] Update providers users to fetch providers on demand --- .../modals/BatchQuickMatchModel.vue | 6 ++- client/components/modals/item/tabs/Cover.vue | 2 + client/components/modals/item/tabs/Match.vue | 53 ++++++++++++++----- .../modals/libraries/EditLibrary.vue | 2 + .../modals/libraries/LibrarySettings.vue | 5 -- client/pages/config/index.vue | 2 + client/pages/upload/index.vue | 2 + 7 files changed, 52 insertions(+), 20 deletions(-) diff --git a/client/components/modals/BatchQuickMatchModel.vue b/client/components/modals/BatchQuickMatchModel.vue index 138684b20..f6bcd9728 100644 --- a/client/components/modals/BatchQuickMatchModel.vue +++ b/client/components/modals/BatchQuickMatchModel.vue @@ -96,6 +96,9 @@ export default { }, methods: { init() { + // Fetch providers when modal is shown + this.$store.dispatch('scanners/fetchProviders') + // If we don't have a set provider (first open of dialog) or we've switched library, set // the selected provider to the current library default provider if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) { @@ -127,8 +130,7 @@ export default { this.show = false }) } - }, - mounted() {} + } } diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index 4ed734b1b..be17f9636 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -438,6 +438,8 @@ export default { mounted() { // Setup socket listeners when component is mounted this.addSocketListeners() + // Fetch providers if not already loaded + this.$store.dispatch('scanners/fetchProviders') }, beforeDestroy() { // Cancel any ongoing search when component is destroyed diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 36cdd7a8e..4b92f6cd8 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -2,7 +2,7 @@
-
+
@@ -253,6 +253,7 @@ export default { hasSearched: false, selectedMatch: null, selectedMatchOrig: null, + waitingForProviders: false, selectedMatchUsage: { title: true, subtitle: true, @@ -285,9 +286,19 @@ export default { handler(newVal) { if (newVal) this.init() } + }, + providersLoaded(isLoaded) { + // Complete initialization once providers are loaded + if (isLoaded && this.waitingForProviders) { + this.waitingForProviders = false + this.initProviderAndSearch() + } } }, computed: { + providersLoaded() { + return this.$store.getters['scanners/areProvidersLoaded'] + }, isProcessing: { get() { return this.processing @@ -478,6 +489,24 @@ export default { this.checkboxToggled() }, + initProviderAndSearch() { + // Set provider based on media type + if (this.isPodcast) { + this.provider = 'itunes' + } else { + this.provider = this.getDefaultBookProvider() + } + + // Prefer using ASIN if set and using audible provider + if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) { + this.searchTitle = this.libraryItem.media.metadata.asin + this.searchAuthor = '' + } + + if (this.searchTitle) { + this.submitSearch() + } + }, init() { this.clearSelectedMatch() this.initSelectedMatchUsage() @@ -495,19 +524,13 @@ export default { } this.searchTitle = this.libraryItem.media.metadata.title this.searchAuthor = this.libraryItem.media.metadata.authorName || '' - if (this.isPodcast) this.provider = 'itunes' - else { - this.provider = this.getDefaultBookProvider() - } - // Prefer using ASIN if set and using audible provider - if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) { - this.searchTitle = this.libraryItem.media.metadata.asin - this.searchAuthor = '' - } - - if (this.searchTitle) { - this.submitSearch() + // Wait for providers to be loaded before setting provider and searching + if (this.providersLoaded || this.isPodcast) { + this.waitingForProviders = false + this.initProviderAndSearch() + } else { + this.waitingForProviders = true } }, selectMatch(match) { @@ -637,6 +660,10 @@ export default { this.selectedMatch = null this.selectedMatchOrig = null } + }, + mounted() { + // Fetch providers if not already loaded + this.$store.dispatch('scanners/fetchProviders') } } diff --git a/client/components/modals/libraries/EditLibrary.vue b/client/components/modals/libraries/EditLibrary.vue index 1e4d3990a..c805f79b0 100644 --- a/client/components/modals/libraries/EditLibrary.vue +++ b/client/components/modals/libraries/EditLibrary.vue @@ -156,6 +156,8 @@ export default { }, mounted() { this.init() + // Fetch providers if not already loaded + this.$store.dispatch('scanners/fetchProviders') } } diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index 231cb5ec4..7cfc22017 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -104,7 +104,6 @@ export default { }, data() { return { - provider: null, useSquareBookCovers: false, enableWatcher: false, skipMatchingMediaWithAsin: false, @@ -134,10 +133,6 @@ export default { isPodcastLibrary() { return this.mediaType === 'podcast' }, - providers() { - if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders - return this.$store.state.scanners.bookProviders - }, maskAsFinishedWhenItems() { return [ { diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 099ae9c4e..b8cf3cff2 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -417,6 +417,8 @@ export default { }, mounted() { this.initServerSettings() + // Fetch providers if not already loaded (for cover provider dropdown) + this.$store.dispatch('scanners/fetchProviders') } } diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index 84d4d1478..73ebef9c6 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -394,6 +394,8 @@ export default { this.setMetadataProvider() this.setDefaultFolder() + // Fetch providers if not already loaded + this.$store.dispatch('scanners/fetchProviders') window.addEventListener('dragenter', this.dragenter) window.addEventListener('dragleave', this.dragleave) window.addEventListener('dragover', this.dragover) From b1921e70349394bd46577a2125afd69077ebd606 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 19 Oct 2025 17:25:18 -0500 Subject: [PATCH 025/285] Fix podcast failed to parse rss feed when feed is not using CData in content:encoded #4757 --- server/utils/podcastUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index b62e024f0..2042a8e39 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -171,7 +171,7 @@ function extractEpisodeData(item) { // Full description with html if (item['content:encoded']) { - const rawDescription = (extractFirstArrayItem(item, 'content:encoded') || '').trim() + const rawDescription = (extractFirstArrayItemString(item, 'content:encoded') || '').trim() episode.description = htmlSanitizer.sanitize(rawDescription) } From fd593caafc0a7ffc2ce27ae2d649b6163c0c3f67 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 21 Oct 2025 09:38:35 +0300 Subject: [PATCH 026/285] SearchController: simplify query param validation logic --- server/controllers/SearchController.js | 212 ++++++++++--------------- server/utils/index.js | 49 ++++-- 2 files changed, 124 insertions(+), 137 deletions(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 72d215f34..9bb6e397d 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -4,7 +4,7 @@ const BookFinder = require('../finders/BookFinder') const PodcastFinder = require('../finders/PodcastFinder') const AuthorFinder = require('../finders/AuthorFinder') const Database = require('../Database') -const { isValidASIN, getQueryParamAsString } = require('../utils') +const { isValidASIN, getQueryParamAsString, ValidationError, NotFoundError } = require('../utils') // Provider name mappings for display purposes const providerMap = { @@ -39,73 +39,17 @@ class SearchController { constructor() {} /** - * Validates that multiple parameters are strings - * @param {Object} params - Object with param names as keys and values to validate - * @param {string} methodName - Name of the calling method for logging - * @returns {{valid: boolean, error?: {status: number, message: string}}} - */ - static validateStringParams(params, methodName) { - for (const [key, value] of Object.entries(params)) { - if (typeof value !== 'string') { - Logger.error(`[SearchController] ${methodName}: Invalid ${key} parameter`) - return { - valid: false, - error: { - status: 400, - message: 'Invalid request query params' - } - } - } - } - return { valid: true } - } - - /** - * Validates that a required string parameter exists and is a string - * @param {any} value - Value to validate - * @param {string} paramName - Parameter name for logging - * @param {string} methodName - Name of the calling method for logging - * @returns {{valid: boolean, error?: {status: number, message: string}}} - */ - static validateRequiredString(value, paramName, methodName) { - if (!value || typeof value !== 'string') { - Logger.error(`[SearchController] ${methodName}: Invalid or missing ${paramName}`) - return { - valid: false, - error: { - status: 400, - message: `Invalid or missing ${paramName}` - } - } - } - return { valid: true } - } - - /** - * Validates and fetches a library item by ID + * Fetches a library item by ID * @param {string} id - Library item ID * @param {string} methodName - Name of the calling method for logging - * @returns {Promise<{valid: boolean, libraryItem?: any, error?: {status: number, message: string}}>} + * @returns {Promise} */ - static async fetchAndValidateLibraryItem(id, methodName) { - const validation = SearchController.validateRequiredString(id, 'library item id', methodName) - if (!validation.valid) { - return validation - } - + static async fetchLibraryItem(id) { const libraryItem = await Database.libraryItemModel.getExpandedById(id) if (!libraryItem) { - Logger.error(`[SearchController] ${methodName}: Library item not found with id "${id}"`) - return { - valid: false, - error: { - status: 404, - message: 'Library item not found' - } - } + throw new NotFoundError(`library item "${id}" not found`) } - - return { valid: true, libraryItem } + return libraryItem } /** @@ -139,21 +83,25 @@ class SearchController { * @param {Response} res */ async findBooks(req, res) { - // Safely extract query parameters, rejecting arrays to prevent type confusion - const provider = getQueryParamAsString(req.query.provider, 'google') - const title = getQueryParamAsString(req.query.title, '') - const author = getQueryParamAsString(req.query.author, '') + try { + const query = req.query + const provider = getQueryParamAsString(query, 'provider', 'google') + const title = getQueryParamAsString(query, 'title', '') + const author = getQueryParamAsString(query, 'author', '') + const id = getQueryParamAsString(query, 'id', '', true) - // Validate string parameters - const validation = SearchController.validateStringParams({ provider, title, author }, 'findBooks') - if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) + // Fetch library item + const libraryItem = await SearchController.fetchLibraryItem(id) - // Fetch and validate library item - const itemValidation = await SearchController.fetchAndValidateLibraryItem(req.query.id, 'findBooks') - if (!itemValidation.valid) return res.status(itemValidation.error.status).send(itemValidation.error.message) - - const results = await BookFinder.search(itemValidation.libraryItem, provider, title, author) - res.json(results) + const results = await BookFinder.search(libraryItem, provider, title, author) + res.json(results) + } catch (error) { + Logger.error(`[SearchController] findBooks: ${error.message}`) + if (error instanceof ValidationError || error instanceof NotFoundError) { + return res.status(error.status).send(error.message) + } + return res.status(500).send('Internal server error') + } } /** @@ -163,24 +111,24 @@ class SearchController { * @param {Response} res */ async findCovers(req, res) { - const query = req.query - const podcast = query.podcast === '1' || query.podcast === 1 - const title = getQueryParamAsString(query.title, '') - const author = getQueryParamAsString(query.author, '') - const provider = getQueryParamAsString(query.provider, 'google') + try { + const query = req.query + const podcast = query.podcast === '1' || query.podcast === 1 + const title = getQueryParamAsString(query, 'title', '', true) + const author = getQueryParamAsString(query, 'author', '') + const provider = getQueryParamAsString(query, 'provider', 'google') - // Validate required title - const titleValidation = SearchController.validateRequiredString(title, 'title', 'findCovers') - if (!titleValidation.valid) return res.status(titleValidation.error.status).send(titleValidation.error.message) - - // Validate other string parameters - const validation = SearchController.validateStringParams({ author, provider }, 'findCovers') - if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) - - let results = null - if (podcast) results = await PodcastFinder.findCovers(title) - else results = await BookFinder.findCovers(provider, title, author) - res.json({ results }) + let results = null + if (podcast) results = await PodcastFinder.findCovers(title) + else results = await BookFinder.findCovers(provider, title, author) + res.json({ results }) + } catch (error) { + Logger.error(`[SearchController] findCovers: ${error.message}`) + if (error instanceof ValidationError) { + return res.status(error.status).send(error.message) + } + return res.status(500).send('Internal server error') + } } /** @@ -191,36 +139,42 @@ class SearchController { * @param {Response} res */ async findPodcasts(req, res) { - const term = getQueryParamAsString(req.query.term) - const country = getQueryParamAsString(req.query.country, 'us') + try { + const query = req.query + const term = getQueryParamAsString(query, 'term', '', true) + const country = getQueryParamAsString(query, 'country', 'us') - // Validate required term - const termValidation = SearchController.validateRequiredString(term, 'term', 'findPodcasts') - if (!termValidation.valid) return res.status(termValidation.error.status).send(termValidation.error.message) - - // Validate country parameter - const validation = SearchController.validateStringParams({ country }, 'findPodcasts') - if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) - - const results = await PodcastFinder.search(term, { country }) - res.json(results) + const results = await PodcastFinder.search(term, { country }) + res.json(results) + } catch (error) { + Logger.error(`[SearchController] findPodcasts: ${error.message}`) + if (error instanceof ValidationError) { + return res.status(error.status).send(error.message) + } + return res.status(500).send('Internal server error') + } } /** * GET: /api/search/authors + * Note: This endpoint is not currently used in the web client. * * @param {RequestWithUser} req * @param {Response} res */ async findAuthor(req, res) { - const query = getQueryParamAsString(req.query.q) + try { + const query = getQueryParamAsString(req.query, 'q', '', true) - // Validate query parameter - const validation = SearchController.validateRequiredString(query, 'query', 'findAuthor') - if (!validation.valid) return res.status(validation.error.status).send(validation.error.message) - - const author = await AuthorFinder.findAuthorByName(query) - res.json(author) + const author = await AuthorFinder.findAuthorByName(query) + res.json(author) + } catch (error) { + Logger.error(`[SearchController] findAuthor: ${error.message}`) + if (error instanceof ValidationError) { + return res.status(error.status).send(error.message) + } + return res.status(500).send('Internal server error') + } } /** @@ -230,24 +184,30 @@ class SearchController { * @param {Response} res */ async findChapters(req, res) { - const asin = getQueryParamAsString(req.query.asin) - const region = getQueryParamAsString(req.query.region, 'us').toLowerCase() + try { + const query = req.query + const asin = getQueryParamAsString(query, 'asin', '', true) + const region = getQueryParamAsString(req.query.region, 'us').toLowerCase() - // Validate ASIN parameter - const asinValidation = SearchController.validateRequiredString(asin, 'asin', 'findChapters') - if (!asinValidation.valid) return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' }) + if (!isValidASIN(asin.toUpperCase())) throw new ValidationError('asin', 'is invalid') - if (!isValidASIN(asin.toUpperCase())) return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' }) - - // Validate region parameter - const validation = SearchController.validateStringParams({ region }, 'findChapters') - if (!validation.valid) res.json({ error: 'Invalid region', stringKey: 'MessageInvalidRegion' }) - - const chapterData = await BookFinder.findChapters(asin, region) - if (!chapterData) { - return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' }) + const chapterData = await BookFinder.findChapters(asin, region) + if (!chapterData) { + return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' }) + } + res.json(chapterData) + } catch (error) { + Logger.error(`[SearchController] findChapters: ${error.message}`) + if (error instanceof ValidationError) { + if (error.paramName === 'asin') { + return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' }) + } + if (error.paramName === 'region') { + return res.json({ error: 'Invalid region', stringKey: 'MessageInvalidRegion' }) + } + } + return res.status(500).send('Internal server error') } - res.json(chapterData) } /** diff --git a/server/utils/index.js b/server/utils/index.js index 0661d14f8..c7700a783 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -278,29 +278,56 @@ module.exports.timestampToSeconds = (timestamp) => { return null } +class ValidationError extends Error { + constructor(paramName, message, status = 400) { + super(`Query parameter "${paramName}" ${message}`) + this.name = 'ValidationError' + this.paramName = paramName + this.status = status + } +} +module.exports.ValidationError = ValidationError + +class NotFoundError extends Error { + constructor(message, status = 404) { + super(message) + this.name = 'NotFoundError' + this.status = status + } +} +module.exports.NotFoundError = NotFoundError + /** * Safely extracts a query parameter as a string, rejecting arrays to prevent type confusion * Express query parameters can be arrays if the same parameter appears multiple times * @example ?author=Smith => "Smith" - * @example ?author=Smith&author=Jones => null (array detected) + * @example ?author=Smith&author=Jones => throws error * - * @param {any} value - Query parameter value + * @param {Object} query - Query object + * @param {string} paramName - Parameter name * @param {string} defaultValue - Default value if undefined/null + * @param {boolean} required - Whether the parameter is required * @param {number} maxLength - Optional maximum length (defaults to 10000 to prevent ReDoS attacks) - * @returns {string|null} String value or null if invalid (array or too long) + * @returns {string} String value + * @throws {ValidationError} If value is an array + * @throws {ValidationError} If value is too long + * @throws {ValidationError} If value is required but not provided */ -module.exports.getQueryParamAsString = (value, defaultValue = '', maxLength = 1000) => { +module.exports.getQueryParamAsString = (query, paramName, defaultValue = '', required = false, maxLength = 1000) => { + const value = query[paramName] + if (value === undefined || value === null) { + if (required) { + throw new ValidationError(paramName, 'is required') + } + return defaultValue + } // Explicitly reject arrays to prevent type confusion if (Array.isArray(value)) { - return null - } - // Return default for undefined/null - if (value == null) { - return defaultValue + throw new ValidationError(paramName, 'is an array') } // Reject excessively long strings to prevent ReDoS attacks if (typeof value === 'string' && value.length > maxLength) { - return null + throw new ValidationError(paramName, 'is too long') } - return value + return String(value) } From 57c7b123f05d416347829edf13ef10a798bd0fd5 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 21 Oct 2025 11:00:29 +0300 Subject: [PATCH 027/285] Fix codeQL error: Return json error object --- server/controllers/SearchController.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 9bb6e397d..5a16229cd 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -98,9 +98,9 @@ class SearchController { } catch (error) { Logger.error(`[SearchController] findBooks: ${error.message}`) if (error instanceof ValidationError || error instanceof NotFoundError) { - return res.status(error.status).send(error.message) + return res.status(error.status).json({ error: error.message }) } - return res.status(500).send('Internal server error') + return res.status(500).json({ error: 'Internal server error' }) } } @@ -125,9 +125,9 @@ class SearchController { } catch (error) { Logger.error(`[SearchController] findCovers: ${error.message}`) if (error instanceof ValidationError) { - return res.status(error.status).send(error.message) + return res.status(error.status).json({ error: error.message }) } - return res.status(500).send('Internal server error') + return res.status(500).json({ error: 'Internal server error' }) } } @@ -149,9 +149,9 @@ class SearchController { } catch (error) { Logger.error(`[SearchController] findPodcasts: ${error.message}`) if (error instanceof ValidationError) { - return res.status(error.status).send(error.message) + return res.status(error.status).json({ error: error.message }) } - return res.status(500).send('Internal server error') + return res.status(500).json({ error: 'Internal server error' }) } } @@ -171,9 +171,9 @@ class SearchController { } catch (error) { Logger.error(`[SearchController] findAuthor: ${error.message}`) if (error instanceof ValidationError) { - return res.status(error.status).send(error.message) + return res.status(error.status).json({ error: error.message }) } - return res.status(500).send('Internal server error') + return res.status(500).json({ error: 'Internal server error' }) } } @@ -206,7 +206,7 @@ class SearchController { return res.json({ error: 'Invalid region', stringKey: 'MessageInvalidRegion' }) } } - return res.status(500).send('Internal server error') + return res.status(500).json({ error: 'Internal server error' }) } } From 6db6b862e6c5342564639e741ec01ae32deed8fa Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 21 Oct 2025 17:33:53 +0300 Subject: [PATCH 028/285] Upgrade codeql-actions to v3 --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2e5f4bced..809563018 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: '/language:${{matrix.language}}' From e684a8dc43cedcb5f7af003b54c09bfa2bffc3d4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 21 Oct 2025 17:22:10 -0500 Subject: [PATCH 029/285] Update JSDocs & auto-formatting of PodcastFinder --- server/controllers/SearchController.js | 2 +- server/finders/PodcastFinder.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 5a16229cd..f6f0ba475 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -42,7 +42,7 @@ class SearchController { * Fetches a library item by ID * @param {string} id - Library item ID * @param {string} methodName - Name of the calling method for logging - * @returns {Promise} + * @returns {Promise} */ static async fetchLibraryItem(id) { const libraryItem = await Database.libraryItemModel.getExpandedById(id) diff --git a/server/finders/PodcastFinder.js b/server/finders/PodcastFinder.js index abaf02ac6..40d6a5a00 100644 --- a/server/finders/PodcastFinder.js +++ b/server/finders/PodcastFinder.js @@ -7,9 +7,9 @@ class PodcastFinder { } /** - * - * @param {string} term - * @param {{country:string}} options + * + * @param {string} term + * @param {{country:string}} options * @returns {Promise} */ async search(term, options = {}) { @@ -20,12 +20,16 @@ class PodcastFinder { return results } + /** + * @param {string} term + * @returns {Promise} + */ async findCovers(term) { if (!term) return null Logger.debug(`[iTunes] Searching for podcast covers with term "${term}"`) - var results = await this.iTunesApi.searchPodcasts(term) + const results = await this.iTunesApi.searchPodcasts(term) if (!results) return [] - return results.map(r => r.cover).filter(r => r) + return results.map((r) => r.cover).filter((r) => r) } } -module.exports = new PodcastFinder() \ No newline at end of file +module.exports = new PodcastFinder() From f03b0915ebb5df1d642b6874ca28949be52a14b2 Mon Sep 17 00:00:00 2001 From: Hezha Date: Wed, 8 Oct 2025 22:54:01 +0200 Subject: [PATCH 030/285] Translated using Weblate (Arabic) Currently translated at 95.9% (1116 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/ --- client/strings/ar.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/strings/ar.json b/client/strings/ar.json index fb13ad52b..e3e13a859 100644 --- a/client/strings/ar.json +++ b/client/strings/ar.json @@ -21,7 +21,8 @@ "ButtonChooseAFolder": "اختر المجلد", "ButtonChooseFiles": "اختر الملفات", "ButtonClearFilter": "تصفية الفرز", - "ButtonCloseFeed": "إغلاق", + "ButtonClose": "إغلاق", + "ButtonCloseFeed": "إغلاق الموجز", "ButtonCloseSession": "إغلاق الجلسة المفتوحة", "ButtonCollections": "المجموعات", "ButtonConfigureScanner": "إعدادات الماسح الضوئي", @@ -120,11 +121,13 @@ "HeaderAccount": "الحساب", "HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص", "HeaderAdvanced": "متقدم", + "HeaderApiKeys": "مفاتيح API", "HeaderAppriseNotificationSettings": "إعدادات الإشعارات", "HeaderAudioTracks": "المقاطع الصوتية", "HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية", "HeaderAuthentication": "المصادقة", "HeaderBackups": "النسخ الاحتياطية", + "HeaderBulkChapterModal": "أضف فصولاً متعددة", "HeaderChangePassword": "تغيير كلمة المرور", "HeaderChapters": "الفصول", "HeaderChooseAFolder": "اختيار المجلد", @@ -163,6 +166,7 @@ "HeaderMetadataOrderOfPrecedence": "ترتيب أولوية البيانات الوصفية", "HeaderMetadataToEmbed": "البيانات الوصفية المراد تضمينها", "HeaderNewAccount": "حساب جديد", + "HeaderNewApiKey": "مفتاح API جديد", "HeaderNewLibrary": "مكتبة جديدة", "HeaderNotificationCreate": "إنشاء إشعار", "HeaderNotificationUpdate": "تحديث إشعار", @@ -196,6 +200,7 @@ "HeaderSettingsExperimental": "ميزات تجريبية", "HeaderSettingsGeneral": "عام", "HeaderSettingsScanner": "إعدادات المسح", + "HeaderSettingsSecurity": "الأمان", "HeaderSettingsWebClient": "عميل الويب", "HeaderSleepTimer": "مؤقت النوم", "HeaderStatsLargestItems": "أكبر العناصر حجماً", @@ -207,6 +212,7 @@ "HeaderTableOfContents": "جدول المحتويات", "HeaderTools": "أدوات", "HeaderUpdateAccount": "تحديث الحساب", + "HeaderUpdateApiKey": "تحديث مفتاح API", "HeaderUpdateAuthor": "تحديث المؤلف", "HeaderUpdateDetails": "تحديث التفاصيل", "HeaderUpdateLibrary": "تحديث المكتبة", @@ -236,6 +242,8 @@ "LabelAllUsersExcludingGuests": "جميع المستخدمين باستثناء الضيوف", "LabelAllUsersIncludingGuests": "جميع المستخدمين بما في ذلك الضيوف", "LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك", + "LabelApiKeyCreated": "تم إنشاء مفتاح API \"{0}\" بنجاح.", + "LabelApiKeyCreatedDescription": "تأكد من نسخ مفتاح API الآن، لن تتمكن من رؤيته مرة أخرى.", "LabelApiToken": "رمز API", "LabelAppend": "إلحاق", "LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)", From ee2d8d1f719178a50a70b6ecc2f3bd6c53dc329c Mon Sep 17 00:00:00 2001 From: pryszczoskor Date: Wed, 8 Oct 2025 23:02:06 +0200 Subject: [PATCH 031/285] 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/ --- client/strings/pl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 98335a21d..0b92dc89c 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -400,7 +400,7 @@ "LabelHours": "Godziny", "LabelIcon": "Ikona", "LabelImageURLFromTheWeb": "Link do obrazu w sieci", - "LabelInProgress": "W trakcie", + "LabelInProgress": "W toku", "LabelIncludeInTracklist": "Dołącz do listy odtwarzania", "LabelIncomplete": "Nieukończone", "LabelInterval": "Interwał", From 96693659bf3e0642be509ab8d6a27ba6868febf7 Mon Sep 17 00:00:00 2001 From: Coxe Date: Fri, 10 Oct 2025 21:11:49 +0200 Subject: [PATCH 032/285] Translated using Weblate (Danish) Currently translated at 96.9% (1127 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/ --- client/strings/da.json | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/client/strings/da.json b/client/strings/da.json index cb1351450..ab1444970 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -1001,13 +1001,14 @@ "ToastCollectionItemsAddFailed": "Genstand(e) tilføjet til kollektion fejlet", "ToastCollectionRemoveSuccess": "Samling fjernet", "ToastCollectionUpdateSuccess": "Samling opdateret", + "ToastConnectionNotAvailable": "Forbindelse mislykkedes. Prøv igen senere", "ToastCoverUpdateFailed": "Cover opdatering fejlede", - "ToastDateTimeInvalidOrIncomplete": "Dato og tid er forkert eller ufærdig", - "ToastDeleteFileFailed": "Slet fil fejlede", + "ToastDateTimeInvalidOrIncomplete": "Dato og tid er ugyldig eller ufærdig", + "ToastDeleteFileFailed": "Sletning af fil fejlede", "ToastDeleteFileSuccess": "Fil slettet", - "ToastDeviceAddFailed": "Fejlede at tilføje enhed", - "ToastDeviceNameAlreadyExists": "Elæser enhed med det navn eksistere allerede", - "ToastDeviceTestEmailFailed": "Fejlede at sende test mail", + "ToastDeviceAddFailed": "Tilføjelse af enhed Fejlede", + "ToastDeviceNameAlreadyExists": "E-læser enhed med det navn eksistere allerede", + "ToastDeviceTestEmailFailed": "Afsendelse af test mail fejlede", "ToastDeviceTestEmailSuccess": "Test mail sendt", "ToastEmailSettingsUpdateSuccess": "Mail indstillinger opdateret", "ToastEncodeCancelFailed": "Fejlede at afbryde indkodning", @@ -1017,21 +1018,23 @@ "ToastEpisodeUpdateSuccess": "{0} afsnit opdateret", "ToastErrorCannotShare": "Kan ikke dele på denne enhed", "ToastFailedToCreate": "Oprettelsen mislykkedes", - "ToastFailedToLoadData": "Fejlede at indlæse data", + "ToastFailedToDelete": "Sletning fejlede", + "ToastFailedToLoadData": "Indlæsning af data fejlede", "ToastFailedToMatch": "Fejlet match", - "ToastFailedToShare": "Fejlet deling", + "ToastFailedToShare": "Deling fejlede", "ToastFailedToUpdate": "Fejlet opdatering", - "ToastInvalidImageUrl": "Forkert billede URL", - "ToastInvalidMaxEpisodesToDownload": "Forkert maks afsnit at hente", - "ToastInvalidUrl": "Forkert URL", - "ToastItemCoverUpdateSuccess": "Varens omslag opdateret", - "ToastItemDeletedFailed": "Fejlede at slette genstand", + "ToastInvalidImageUrl": "Ugyldig billede URL", + "ToastInvalidMaxEpisodesToDownload": "Ugyldigt maks afsnit at hente", + "ToastInvalidUrl": "Ugyldig URL", + "ToastInvalidUrls": "En eller flere URLer er ugyldige", + "ToastItemCoverUpdateSuccess": "Omslag opdateret", + "ToastItemDeletedFailed": "Sletning af genstand fejlede", "ToastItemDeletedSuccess": "Genstand slettet", - "ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret", - "ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet", - "ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet", - "ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet", - "ToastItemMarkedAsNotFinishedSuccess": "Vare markeret som ikke afsluttet", + "ToastItemDetailsUpdateSuccess": "Detaljer opdateret", + "ToastItemMarkedAsFinishedFailed": "Markering som afsluttet mislykkedes", + "ToastItemMarkedAsFinishedSuccess": "Element markeret som afsluttet", + "ToastItemMarkedAsNotFinishedFailed": "Markering som ikke afsluttet mislykkedes", + "ToastItemMarkedAsNotFinishedSuccess": "Element markeret som ikke afsluttet", "ToastItemUpdateSuccess": "Genstand opdateret", "ToastLibraryCreateFailed": "Oprettelse af bibliotek mislykkedes", "ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet", From d59714d804c46f0027235394914f44879eb463d3 Mon Sep 17 00:00:00 2001 From: pmangro Date: Sat, 11 Oct 2025 00:07:00 +0200 Subject: [PATCH 033/285] Translated using Weblate (Portuguese (Brazil)) Currently translated at 74.2% (863 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/ --- client/strings/pt-br.json | 93 ++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 9604daaed..e31aa3f03 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -1,6 +1,6 @@ { "ButtonAdd": "Adicionar", - "ButtonAddApiKey": "Adicionar Chave API", + "ButtonAddApiKey": "Adicionar chave de API", "ButtonAddChapters": "Adicionar Capítulos", "ButtonAddDevice": "Adicionar Dispositivo", "ButtonAddLibrary": "Adicionar Biblioteca", @@ -21,6 +21,7 @@ "ButtonChooseAFolder": "Escolha uma pasta", "ButtonChooseFiles": "Escolha arquivos", "ButtonClearFilter": "Limpar Filtro", + "ButtonClose": "Fechar", "ButtonCloseFeed": "Fechar Feed", "ButtonCloseSession": "Fechar Sessão Aberta", "ButtonCollections": "Coleções", @@ -53,7 +54,7 @@ "ButtonNevermind": "Cancelar", "ButtonNext": "Próximo", "ButtonNextChapter": "Próximo Capítulo", - "ButtonNextItemInQueue": "Próximo Item da Fila", + "ButtonNextItemInQueue": "Próximo Item na Fila", "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Feed", "ButtonOpenManager": "Abrir Gerenciador", @@ -120,10 +121,13 @@ "HeaderAccount": "Conta", "HeaderAddCustomMetadataProvider": "Adicionar Provedor de Metadados Personalizado", "HeaderAdvanced": "Avançado", + "HeaderApiKeys": "Chaves de API", "HeaderAppriseNotificationSettings": "Configuração de notificações Apprise", "HeaderAudioTracks": "Trilhas de áudio", "HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks", "HeaderAuthentication": "Autenticação", + "HeaderBackups": "Backups", + "HeaderBulkChapterModal": "Adicionar vários capítulos", "HeaderChangePassword": "Trocar Senha", "HeaderChapters": "Capítulos", "HeaderChooseAFolder": "Escolha uma Pasta", @@ -136,6 +140,7 @@ "HeaderDetails": "Detalhes", "HeaderDownloadQueue": "Fila de Download", "HeaderEbookFiles": "Arquivos Ebook", + "HeaderEmail": "Email", "HeaderEmailSettings": "Configurações de Email", "HeaderEpisodes": "Episódios", "HeaderEreaderDevices": "Dispositivos Ereader", @@ -152,6 +157,8 @@ "HeaderLibraryStats": "Estatísticas da Biblioteca", "HeaderListeningSessions": "Sessões", "HeaderListeningStats": "Estatísticas", + "HeaderLogin": "Login", + "HeaderLogs": "Logs", "HeaderManageGenres": "Gerenciar Gêneros", "HeaderManageTags": "Gerenciar Etiquetas", "HeaderMapDetails": "Designar Detalhes", @@ -159,17 +166,23 @@ "HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados", "HeaderMetadataToEmbed": "Metadados a Serem Incluídos", "HeaderNewAccount": "Nova Conta", + "HeaderNewApiKey": "Nova chave de API", "HeaderNewLibrary": "Nova Biblioteca", + "HeaderNotificationCreate": "Criar Notificação", + "HeaderNotificationUpdate": "Atualizar Notificação", "HeaderNotifications": "Notificações", "HeaderOpenIDConnectAuthentication": "Autenticação via OpenID Connect", + "HeaderOpenListeningSessions": "Abrir Sessões de Escuta", "HeaderOpenRSSFeed": "Abrir Feed RSS", "HeaderOtherFiles": "Outros Arquivos", "HeaderPasswordAuthentication": "Autenticação por Senha", "HeaderPermissions": "Permissões", "HeaderPlayerQueue": "Fila do reprodutor", + "HeaderPlayerSettings": "Configurações do Reprodutor", "HeaderPlaylist": "Lista de Reprodução", "HeaderPlaylistItems": "Itens da lista de reprodução", "HeaderPodcastsToAdd": "Podcasts para Adicionar", + "HeaderPresets": "Valores predefinidos", "HeaderPreviewCover": "Visualização da Capa", "HeaderRSSFeedGeneral": "Detalhes RSS", "HeaderRSSFeedIsOpen": "Feed RSS está Aberto", @@ -178,6 +191,7 @@ "HeaderRemoveEpisodes": "Remover {0} Episódios", "HeaderSavedMediaProgress": "Progresso da gravação das mídias", "HeaderSchedule": "Programação", + "HeaderScheduleEpisodeDownloads": "Programar Download Automático de Episódios", "HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca", "HeaderSession": "Sessão", "HeaderSetBackupSchedule": "Definir Programação de Backup", @@ -186,6 +200,8 @@ "HeaderSettingsExperimental": "Funcionalidades experimentais", "HeaderSettingsGeneral": "Geral", "HeaderSettingsScanner": "Verificador", + "HeaderSettingsSecurity": "Segurança", + "HeaderSettingsWebClient": "Cliente Web", "HeaderSleepTimer": "Timer", "HeaderStatsLargestItems": "Maiores Itens", "HeaderStatsLongestItems": "Itens mais longos (hrs)", @@ -196,6 +212,7 @@ "HeaderTableOfContents": "Sumário", "HeaderTools": "Ferramentas", "HeaderUpdateAccount": "Atualizar Conta", + "HeaderUpdateApiKey": "Atualizar Chave de API", "HeaderUpdateAuthor": "Atualizar Autor", "HeaderUpdateDetails": "Atualizar Detalhes", "HeaderUpdateLibrary": "Atualizar Biblioteca", @@ -210,6 +227,7 @@ "LabelAccountTypeAdmin": "Administrador", "LabelAccountTypeGuest": "Convidado", "LabelAccountTypeUser": "Usuário", + "LabelActivities": "Atividades", "LabelActivity": "Atividade", "LabelAddToCollection": "Adicionar à Coleção", "LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção", @@ -219,11 +237,20 @@ "LabelAddedDate": "Adicionado {0}", "LabelAdminUsersOnly": "Apenas usuários administradores", "LabelAll": "Todos", + "LabelAllEpisodesDownloaded": "Todos os episódios baixados", "LabelAllUsers": "Todos Usuários", "LabelAllUsersExcludingGuests": "Todos usuários exceto convidados", "LabelAllUsersIncludingGuests": "Todos usuários incluindo convidados", "LabelAlreadyInYourLibrary": "Já na sua biblioteca", + "LabelApiKeyCreated": "Chave de API \"{0}\" criada com sucesso.", + "LabelApiKeyCreatedDescription": "Certifique-se de copiar a chave de API agora pois não será possível vê-la novamente.", + "LabelApiKeyUser": "Agir em nome do usuário", + "LabelApiKeyUserDescription": "Esta chave de API terá as mesmas permissões que o usuário em nome de quem ela está agindo. Isso aparecerá nos logs como se o usuário estivesse fazendo a solicitação.", + "LabelApiToken": "Token de API", "LabelAppend": "Acrescentar", + "LabelAudioBitrate": "Bitrate de áudio (por exemplo, 128k)", + "LabelAudioChannels": "Canais de áudio (1 ou 2)", + "LabelAudioCodec": "Codec de áudio", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (Nome Sobrenome)", "LabelAuthorLastFirst": "Autor (Sobrenome, Nome)", @@ -236,24 +263,31 @@ "LabelAutoRegister": "Registrar Automaticamente", "LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login", "LabelBackToUser": "Voltar para Usuário", + "LabelBackupAudioFiles": "Backup dos Arquivos de Áudio", "LabelBackupLocation": "Localização do Backup", - "LabelBackupsEnableAutomaticBackups": "Ativar backups automáticos", + "LabelBackupsEnableAutomaticBackups": "Backups automáticos", "LabelBackupsEnableAutomaticBackupsHelp": "Backups salvos em /metadata/backups", - "LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB)", + "LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB) (0 para ilimitado)", "LabelBackupsMaxBackupSizeHelp": "Como proteção contra uma configuração incorreta, backups darão erro se excederem o tamanho configurado.", "LabelBackupsNumberToKeep": "Número de backups para guardar", "LabelBackupsNumberToKeepHelp": "Apenas 1 backup será removido por vez, então, se já existem mais backups, você deve apagá-los manualmente.", + "LabelBitrate": "Bitrate", + "LabelBonus": "Bônus", "LabelBooks": "Livros", "LabelButtonText": "Texto do botão", "LabelByAuthor": "por {0}", "LabelChangePassword": "Trocar Senha", "LabelChannels": "Canais", + "LabelChapterCount": "{0} Capítulos", "LabelChapterTitle": "Título do Capítulo", "LabelChapters": "Capítulos", "LabelChaptersFound": "capítulos encontrados", "LabelClickForMoreInfo": "Clique para mais informações", + "LabelClickToUseCurrentValue": "Clique para usar o valor atual", "LabelClosePlayer": "Fechar Reprodutor", + "LabelCodec": "Codec", "LabelCollapseSeries": "Fechar Série", + "LabelCollapseSubSeries": "Fechar Sub Séries", "LabelCollection": "Coleção", "LabelCollections": "Coleções", "LabelComplete": "Concluído", @@ -261,17 +295,21 @@ "LabelContinueListening": "Continuar Escutando", "LabelContinueReading": "Continuar Lendo", "LabelContinueSeries": "Continuar Série", + "LabelCorsAllowed": "Origens Permitidas para CORS", "LabelCover": "Capa", "LabelCoverImageURL": "URL da Imagem da Capa", + "LabelCoverProvider": "Provedor de Capas", "LabelCreatedAt": "Criado em", "LabelCronExpression": "Expressão para o Cron", "LabelCurrent": "Atual", "LabelCurrently": "Atualmente:", "LabelCustomCronExpression": "Expressão personalizada para o Cron:", "LabelDatetime": "Data e Hora", + "LabelDays": "Dias", "LabelDeleteFromFileSystemCheckbox": "Apagar do sistema de arquivos (desmarcar para remover apenas da base de dados)", "LabelDescription": "Descrição", "LabelDeselectAll": "Desmarcar tudo", + "LabelDetectedPattern": "Padrão detectado:", "LabelDevice": "Dispositivo", "LabelDeviceInfo": "Informação do Dispositivo", "LabelDeviceIsAvailableTo": "Dispositivo está disponível para...", @@ -281,6 +319,7 @@ "LabelDiscover": "Descobrir", "LabelDownload": "Download", "LabelDownloadNEpisodes": "Download de {0} Episódios", + "LabelDownloadable": "Baixável", "LabelDuration": "Duração", "LabelDurationComparisonExactMatch": "(exato)", "LabelDurationComparisonLonger": "({0} maior)", @@ -289,6 +328,7 @@ "LabelEbook": "Ebook", "LabelEbooks": "Ebooks", "LabelEdit": "Editar", + "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Remetente", "LabelEmailSettingsRejectUnauthorized": "Rejeitar certificados não autorizados", "LabelEmailSettingsRejectUnauthorizedHelp": "Desativar a validação de certificados SSL pode expor sua conexão a riscos de segurança, como ataques \"man-in-the-middle\". Desative essa opção apenas se entender suas consequências e se puder confiar no servidor de email ao qual você está se conectando.", @@ -297,6 +337,15 @@ "LabelEmailSettingsTestAddress": "Endereço de teste", "LabelEmbeddedCover": "Capa Integrada", "LabelEnable": "Habilitar", + "LabelEncodingBackupLocation": "Um backup dos seus arquivos de áudio original será gravado em:", + "LabelEncodingChaptersNotEmbedded": "Capítulos não são integrados em audiobooks com várias trilhas.", + "LabelEncodingClearItemCache": "Certifique-se de, periodicamente, apagar os itens do cache.", + "LabelEncodingFinishedM4B": "O arquivo M4B final será colocado na sua pasta de audiobooks em:", + "LabelEncodingInfoEmbedded": "Os metadados serão integrados nas trilhas de áudio dentro da sua pasta de audiobooks.", + "LabelEncodingStartedNavigation": "Assim que a tarefa for iniciada você pode sair dessa página.", + "LabelEncodingTimeWarning": "A codificação pode durar até 30 minutos.", + "LabelEncodingWarningAdvancedSettings": "Aviso: não atualize essas configurações se não estiver familiarizado com as opções de codificação do ffmpeg.", + "LabelEncodingWatcherDisabled": "Se você desabilitou o monitoramento, será necessário fazer uma nova verificação deste audiobook depois.", "LabelEnd": "Fim", "LabelEpisode": "Episódio", "LabelEpisodeTitle": "Título do Episódio", @@ -329,6 +378,7 @@ "LabelHasEbook": "Tem ebook", "LabelHasSupplementaryEbook": "Tem ebook complementar", "LabelHighestPriority": "Prioridade mais alta", + "LabelHost": "Host", "LabelHour": "Hora", "LabelIcon": "Ícone", "LabelImageURLFromTheWeb": "URL da imagem na internet", @@ -345,6 +395,7 @@ "LabelIntervalEveryDay": "Todo dia", "LabelIntervalEveryHour": "Toda hora", "LabelInvert": "Inverter", + "LabelItem": "Item", "LabelLanguage": "Idioma", "LabelLanguageDefaultServer": "Idioma Padrão do Servidor", "LabelLanguages": "Idiomas", @@ -353,16 +404,20 @@ "LabelLastSeen": "Visto pela Última Vez", "LabelLastTime": "Progresso", "LabelLastUpdate": "Última Atualização", + "LabelLayout": "Layout", "LabelLayoutSinglePage": "Uma página", "LabelLayoutSplitPage": "Página dividida", "LabelLess": "Menos", "LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário", "LabelLibrary": "Biblioteca", + "LabelLibraryFilterSublistEmpty": "Sem {0}", "LabelLibraryItem": "Item da Biblioteca", "LabelLibraryName": "Nome da Biblioteca", "LabelLimit": "Limite", "LabelLineSpacing": "Espaçamento entre linhas", "LabelListenAgain": "Escutar novamente", + "LabelLogLevelDebug": "Debug", + "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Atenção", "LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data", "LabelLowestPriority": "Prioridade mais baixa", @@ -424,9 +479,12 @@ "LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})", "LabelPhotoPathURL": "Caminho/URL para Foto", "LabelPlayMethod": "Método de Reprodução", + "LabelPlayerChapterNumberMarker": "{0} de {1}", "LabelPlaylists": "Listas de Reprodução", + "LabelPodcast": "Podcast", "LabelPodcastSearchRegion": "Região de busca do podcast", "LabelPodcastType": "Tipo de Podcast", + "LabelPodcasts": "Podcasts", "LabelPort": "Porta", "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", "LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes e Google", @@ -435,14 +493,16 @@ "LabelProvider": "Fonte", "LabelPubDate": "Data de Publicação", "LabelPublishYear": "Ano de Publicação", + "LabelPublishedDate": "Publicado {0}", "LabelPublisher": "Editora", "LabelPublishers": "Editoras", "LabelRSSFeedCustomOwnerEmail": "E-mail do dono personalizado", "LabelRSSFeedCustomOwnerName": "Nome do dono personalizado", - "LabelRSSFeedOpen": "Feed RSS Aberto", + "LabelRSSFeedOpen": "Feed de RSS Aberto", "LabelRSSFeedPreventIndexing": "Impedir Indexação", "LabelRSSFeedSlug": "Slug do Feed RSS", "LabelRSSFeedURL": "URL do Feed RSS", + "LabelRandomly": "Aleatoriamente", "LabelRead": "Lido", "LabelReadAgain": "Ler novamente", "LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso", @@ -475,6 +535,8 @@ "LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira", "LabelSettingsChromecastSupport": "Suporte ao Chromecast", "LabelSettingsDateFormat": "Formato de data", + "LabelSettingsEnableWatcher": "Monitorar automaticamente alterações nas bibliotecas", + "LabelSettingsEnableWatcherForLibrary": "Monitorar automaticamente alterações na biblioteca", "LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor", "LabelSettingsEpubsAllowScriptedContent": "Permitir scripts em epubs", "LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que arquivos epub executem scripts. É recomendado manter essa configuração desativada, a não ser que confie na fonte dos arquivos epub.", @@ -507,6 +569,7 @@ "LabelShowSeconds": "Exibir segundos", "LabelSize": "Tamanho", "LabelSleepTimer": "Timer", + "LabelSlug": "Slug", "LabelStart": "Iniciar", "LabelStartTime": "Horário do Início", "LabelStarted": "Iniciado", @@ -534,6 +597,7 @@ "LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário", "LabelTasks": "Tarefas em Execuçào", "LabelTextEditorBulletedList": "Lista com marcadores", + "LabelTextEditorLink": "Link", "LabelTextEditorNumberedList": "Lista numerada", "LabelTextEditorUnlink": "Remover link", "LabelTheme": "Tema", @@ -580,15 +644,16 @@ "LabelViewBookmarks": "Ver marcadores", "LabelViewChapters": "Ver capítulos", "LabelViewQueue": "Ver fila do reprodutor", + "LabelVolume": "Volume", "LabelWeekdaysToRun": "Dias da semana para executar", - "LabelYearReviewHide": "Ocultar Retrospectiva Anual", - "LabelYearReviewShow": "Exibir Retrospectiva Anual", + "LabelYearReviewHide": "Ocultar Retrospectiva", + "LabelYearReviewShow": "Exibir Retrospectiva", "LabelYourAudiobookDuration": "Duração do seu audiobook", "LabelYourBookmarks": "Seus Marcadores", "LabelYourPlaylists": "Suas Listas de Reprodução", "LabelYourProgress": "Seu Progresso", "MessageAddToPlayerQueue": "Adicionar à lista do reprodutor", - "MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da API do Apprise em execução ou uma api que possa tratar esses mesmos chamados.
A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em http://192.168.1.1:8337 você deve utilizar http://192.168.1.1:8337/notify.", + "MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da API do Apprise em execução ou uma API que possa tratar esses mesmos chamados.
A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em http://192.168.1.1:8337 você deve utilizar http://192.168.1.1:8337/notify.", "MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em /metadata/items & /metadata/authors. Backups não incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.", "MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.", "MessageBookshelfNoCollections": "Você ainda não criou coleções", @@ -643,8 +708,8 @@ "MessageForceReScanDescription": "verificará todos os arquivos, como uma verificação nova. Etiquetas ID3 de arquivos de áudio, arquivos OPF e arquivos de texto serão tratados como novos.", "MessageImportantNotice": "Aviso Importante!", "MessageInsertChapterBelow": "Inserir capítulo abaixo", - "MessageItemsSelected": "{0} Itens Selecionados", - "MessageItemsUpdated": "{0} Itens Atualizados", + "MessageItemsSelected": "{0} itens selecionados", + "MessageItemsUpdated": "{0} itens atualizados", "MessageJoinUsOn": "Junte-se a nós", "MessageLoading": "Carregando...", "MessageLoadingFolders": "Carregando pastas...", @@ -692,6 +757,7 @@ "MessagePlayChapter": "Escutar o início do capítulo", "MessagePlaylistCreateFromCollection": "Criar uma lista de reprodução a partir da coleção", "MessagePodcastHasNoRSSFeedForMatching": "Podcast não tem uma URL do feed RSS para ser usada na consulta", + "MessagePodcastSearchField": "Digite um termo para a busca ou a URL de um feed RSS", "MessageQuickMatchDescription": "Preenche detalhes vazios do item & capa com o primeiro resultado de '{0}'. Não sobrescreve detalhes a não ser que a configuração 'Preferir metadados consultados' do servidor esteja ativa.", "MessageRemoveChapter": "Remover capítulo", "MessageRemoveEpisodes": "Remover {0} episódio(s)", @@ -723,12 +789,14 @@ "NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.", "NoteUploaderOnlyAudioFiles": "Ao subir apenas arquivos de áudio, cada arquivo será tratado como um audiobook distinto.", "NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar uma pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.", + "PlaceholderBulkChapterInput": "Digite o título de um capítulo ou use uma numeração (por exemplo, 'Episódio 1', 'Capítulo 10', '1.')", "PlaceholderNewCollection": "Novo nome da coleção", "PlaceholderNewFolderPath": "Novo caminho para a pasta", "PlaceholderNewPlaylist": "Novo nome da lista de reprodução", "PlaceholderSearch": "Buscar..", "PlaceholderSearchEpisode": "Buscar Episódio..", "ToastAccountUpdateSuccess": "Conta atualizada", + "ToastAppriseUrlRequired": "É preciso digitar uma URL Apprise", "ToastAuthorImageRemoveSuccess": "Imagem do autor removida", "ToastAuthorUpdateMerged": "Autor combinado", "ToastAuthorUpdateSuccess": "Autor atualizado", @@ -745,6 +813,7 @@ "ToastBookmarkCreateFailed": "Falha ao criar marcador", "ToastBookmarkCreateSuccess": "Marcador adicionado", "ToastBookmarkRemoveSuccess": "Marcador removido", + "ToastBulkChapterInvalidCount": "Digite um número entre 1 e 150", "ToastCachePurgeFailed": "Falha ao apagar o cache", "ToastCachePurgeSuccess": "Cache apagado com sucesso", "ToastChaptersHaveErrors": "Capítulos com erro", @@ -767,6 +836,7 @@ "ToastLibraryScanFailedToStart": "Falha ao iniciar verificação", "ToastLibraryScanStarted": "Verificação da biblioteca iniciada", "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada", + "ToastNewUserUsernameError": "Digite um nome de usuário", "ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução", "ToastPlaylistCreateSuccess": "Lista de reprodução criada", "ToastPlaylistRemoveSuccess": "Lista de reprodução removida", @@ -790,5 +860,6 @@ "ToastSortingPrefixesEmptyError": "É preciso ter pelo menos um prefixo de ordenação", "ToastSortingPrefixesUpdateSuccess": "Prefixos de ordenação atualizados ({0} item(ns))", "ToastUserDeleteFailed": "Falha ao apagar usuário", - "ToastUserDeleteSuccess": "Usuário apagado" + "ToastUserDeleteSuccess": "Usuário apagado", + "ToastUserRootRequireName": "É preciso entrar com um nome de usuário root" } From 85546b7dd790b1ef7fb8277b02bd08963053cc05 Mon Sep 17 00:00:00 2001 From: nlqog Date: Sun, 12 Oct 2025 19:40:49 +0200 Subject: [PATCH 034/285] Translated using Weblate (Portuguese (Brazil)) Currently translated at 76.6% (892 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/ --- client/strings/pt-br.json | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index e31aa3f03..53b9199e2 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -347,10 +347,14 @@ "LabelEncodingWarningAdvancedSettings": "Aviso: não atualize essas configurações se não estiver familiarizado com as opções de codificação do ffmpeg.", "LabelEncodingWatcherDisabled": "Se você desabilitou o monitoramento, será necessário fazer uma nova verificação deste audiobook depois.", "LabelEnd": "Fim", + "LabelEndOfChapter": "Fim do Capítulo", "LabelEpisode": "Episódio", "LabelEpisodeTitle": "Título do Episódio", "LabelEpisodeType": "Tipo do Episódio", + "LabelEpisodes": "Episódios", "LabelExample": "Exemplo", + "LabelExpired": "Expirado", + "LabelExpiresNever": "Nunca", "LabelExplicit": "Explícito", "LabelExplicitChecked": "Explícito (verificado)", "LabelExplicitUnchecked": "Não explícito (não verificado)", @@ -377,9 +381,11 @@ "LabelHardDeleteFile": "Apagar definitivamente", "LabelHasEbook": "Tem ebook", "LabelHasSupplementaryEbook": "Tem ebook complementar", + "LabelHideSubtitles": "Esconder Legendas", "LabelHighestPriority": "Prioridade mais alta", "LabelHost": "Host", "LabelHour": "Hora", + "LabelHours": "Horas", "LabelIcon": "Ícone", "LabelImageURLFromTheWeb": "URL da imagem na internet", "LabelInProgress": "Em Andamento", @@ -394,6 +400,7 @@ "LabelIntervalEvery6Hours": "A cada 6 horas", "LabelIntervalEveryDay": "Todo dia", "LabelIntervalEveryHour": "Toda hora", + "LabelIntervalEveryMinute": "A cada minuto", "LabelInvert": "Inverter", "LabelItem": "Item", "LabelLanguage": "Idioma", @@ -413,6 +420,8 @@ "LabelLibraryFilterSublistEmpty": "Sem {0}", "LabelLibraryItem": "Item da Biblioteca", "LabelLibraryName": "Nome da Biblioteca", + "LabelLibrarySortByProgress": "Última Atualização", + "LabelLibrarySortByProgressFinished": "Concluído", "LabelLimit": "Limite", "LabelLineSpacing": "Espaçamento entre linhas", "LabelListenAgain": "Escutar novamente", @@ -430,6 +439,7 @@ "LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa", "LabelMetadataProvider": "Fonte de Metadados", "LabelMinute": "Minuto", + "LabelMinutes": "Minutos", "LabelMissing": "Ausente", "LabelMissingEbook": "Ebook não existe", "LabelMissingSupplementaryEbook": "Ebook complementar não existe", @@ -445,6 +455,7 @@ "LabelNewestAuthors": "Novos Autores", "LabelNewestEpisodes": "Episódios mais recentes", "LabelNextBackupDate": "Data do próximo backup", + "LabelNextChapters": "Próximo capítulo será:", "LabelNextScheduledRun": "Próxima execução programada", "LabelNoCustomMetadataProviders": "Não existem fontes de metadados customizados", "LabelNoEpisodesSelected": "Nenhum episódio selecionado", @@ -467,8 +478,10 @@ "LabelOpenIDGroupClaimDescription": "Nome do claim OpenID contendo a lista de grupos do usuário, normalmente chamada de groups. Se configurada, a aplicação atribuirá automaticamente os perfis com base na participação do usuário nos grupos, contanto que os nomes desses grupos no claim, sem distinção entre maiúsculas e minúsculas, sejam 'admin', 'user' ou 'guest'. O claim deve conter uma lista e, se o usuário pertencer a múltiplos grupos, a aplicação atribuirá o perfil correspondendo ao maior nível de acesso. Se não houver correspondência a qualquer grupo, o acesso será negado.", "LabelOpenRSSFeed": "Abrir Feed RSS", "LabelOverwrite": "Sobrescrever", + "LabelPaginationPageXOfY": "Página {0} de {1}", "LabelPassword": "Senha", "LabelPath": "Caminho", + "LabelPermanent": "Permanente", "LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas", "LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas", "LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos", @@ -565,8 +578,11 @@ "LabelSettingsStoreMetadataWithItem": "Armazenar metadados com o item", "LabelSettingsStoreMetadataWithItemHelp": "Por padrão os arquivos de metadados são armazenados em /metadata/items. Ao ativar essa configuração os arquivos de metadados serão armazenadas nas pastas dos itens na sua biblioteca", "LabelSettingsTimeFormat": "Formato da Tempo", + "LabelShare": "Compartilhar", + "LabelShareURL": "Compartilhar URL", "LabelShowAll": "Exibir Todos", "LabelShowSeconds": "Exibir segundos", + "LabelShowSubtitles": "Mostrar Legendas", "LabelSize": "Tamanho", "LabelSleepTimer": "Timer", "LabelSlug": "Slug", @@ -603,7 +619,11 @@ "LabelTheme": "Tema", "LabelThemeDark": "Escuro", "LabelThemeLight": "Claro", + "LabelThemeSepia": "Sépia", "LabelTimeBase": "Base de tempo", + "LabelTimeDurationXHours": "{0} horas", + "LabelTimeDurationXMinutes": "{0} minutos", + "LabelTimeDurationXSeconds": "{0} segundos", "LabelTimeListened": "Tempo de escuta", "LabelTimeListenedToday": "Tempo de escuta hoje", "LabelTimeRemaining": "{0} restantes", @@ -623,6 +643,7 @@ "LabelTracksMultiTrack": "Várias trilhas", "LabelTracksNone": "Sem trilha", "LabelTracksSingleTrack": "Trilha única", + "LabelTrailer": "Trailer", "LabelType": "Tipo", "LabelUnabridged": "Não Abreviada", "LabelUndo": "Desfazer", @@ -646,6 +667,8 @@ "LabelViewQueue": "Ver fila do reprodutor", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Dias da semana para executar", + "LabelXBooks": "{0} livros", + "LabelXItems": "{0} itens", "LabelYearReviewHide": "Ocultar Retrospectiva", "LabelYearReviewShow": "Exibir Retrospectiva", "LabelYourAudiobookDuration": "Duração do seu audiobook", @@ -772,6 +795,7 @@ "MessageServerCouldNotBeReached": "Não foi possível estabelecer conexão com o servidor", "MessageSetChaptersFromTracksDescription": "Definir os capítulos usando cada arquivo de áudio como um capítulo e o nome do arquivo como o título do capítulo", "MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?", + "MessageTaskFailed": "Falhou", "MessageThinking": "Pensando...", "MessageUploaderItemFailed": "Falha no upload", "MessageUploaderItemSuccess": "Upload realizado!", @@ -795,6 +819,11 @@ "PlaceholderNewPlaylist": "Novo nome da lista de reprodução", "PlaceholderSearch": "Buscar..", "PlaceholderSearchEpisode": "Buscar Episódio..", + "StatsAuthorsAdded": "autores adicionados", + "StatsBooksAdded": "livros adicionados", + "StatsBooksFinished": "livros concluídos", + "StatsTopAuthor": "TOP AUTOR", + "StatsTopAuthors": "TOP AUTORES", "ToastAccountUpdateSuccess": "Conta atualizada", "ToastAppriseUrlRequired": "É preciso digitar uma URL Apprise", "ToastAuthorImageRemoveSuccess": "Imagem do autor removida", From 96ef0129edf11cc08048084d0a7bff3a8f02e500 Mon Sep 17 00:00:00 2001 From: Blubberland Date: Tue, 14 Oct 2025 09:25:56 +0200 Subject: [PATCH 035/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/strings/de.json b/client/strings/de.json index 2eaebbcc6..5acae6d9b 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen", "ToastCollectionRemoveSuccess": "Sammlung entfernt", "ToastCollectionUpdateSuccess": "Sammlung aktualisiert", + "ToastConnectionNotAvailable": "Verbindung nicht möglich. Bitte später erneut versuchen", + "ToastCoverSearchFailed": "Cover-Suche fehlgeschlagen", "ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen", "ToastDateTimeInvalidOrIncomplete": "Datum und Zeit sind ungültig oder unvollständig", "ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden", From d3b5612fc01310741ab01f0250964093e0223d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Wed, 15 Oct 2025 16:08:22 +0200 Subject: [PATCH 036/285] Translated using Weblate (Finnish) Currently translated at 95.3% (1109 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index d29ce7f95..8d224d81c 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -355,7 +355,7 @@ "LabelExample": "Esimerkki", "LabelExpandSeries": "Laajenna sarja", "LabelExpandSubSeries": "Laajenna alisarja", - "LabelExplicit": "Yksiselitteinen", + "LabelExplicit": "Sopimaton", "LabelExplicitChecked": "Yksiselitteinen (valittu)", "LabelExplicitUnchecked": "Ei yksiselitteinen (ei valittu)", "LabelExportOPML": "Vie OPML", From c3c9e7731d04079b0b90a11123e1b3f9282c3ff9 Mon Sep 17 00:00:00 2001 From: burghy86 Date: Thu, 16 Oct 2025 23:24:19 +0200 Subject: [PATCH 037/285] Translated using Weblate (Italian) 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/it/ --- client/strings/it.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/strings/it.json b/client/strings/it.json index a032a3c95..2354bc0ae 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -309,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)", "LabelDescription": "Descrizione", "LabelDeselectAll": "Deseleziona Tutto", + "LabelDetectedPattern": "Trovato pattern:", "LabelDevice": "Dispositivo", "LabelDeviceInfo": "Info dispositivo", "LabelDeviceIsAvailableTo": "Il dispositivo e disponibile su…", @@ -377,6 +378,7 @@ "LabelFilterByUser": "Filtro per Utente", "LabelFindEpisodes": "Trova Episodi", "LabelFinished": "Finita", + "LabelFinishedDate": "Finito {0}", "LabelFolder": "Cartella", "LabelFolders": "Cartelle", "LabelFontBold": "Grassetto", @@ -434,8 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Nessuno {0}", "LabelLibraryItem": "Elementi della biblioteca", "LabelLibraryName": "Nome della biblioteca", - "LabelLibrarySortByProgress": "Aggiornamento dei progressi", - "LabelLibrarySortByProgressStarted": "Data di inizio", + "LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti", + "LabelLibrarySortByProgressFinished": "Progressi: Completati", + "LabelLibrarySortByProgressStarted": "Progressi: Iniziati", "LabelLimit": "Limiti", "LabelLineSpacing": "Interlinea", "LabelListenAgain": "Ascolta ancora", @@ -635,6 +638,7 @@ "LabelStartTime": "Tempo di inizio", "LabelStarted": "Iniziato", "LabelStartedAt": "Iniziato al", + "LabelStartedDate": "Iniziati {0}", "LabelStatsAudioTracks": "Tracce Audio", "LabelStatsAuthors": "Autori", "LabelStatsBestDay": "Giorno migliore", @@ -1022,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "l'aggiunta dell'elemento(i) alla raccolta non è riuscito", "ToastCollectionRemoveSuccess": "Collezione rimossa", "ToastCollectionUpdateSuccess": "Raccolta aggiornata", + "ToastConnectionNotAvailable": "Connessione non disponibile. Provare più tardi", + "ToastCoverSearchFailed": "Ricerca Cover fallita", "ToastCoverUpdateFailed": "Aggiornamento cover fallito", "ToastDateTimeInvalidOrIncomplete": "Data e ora non sono valide o incomplete", "ToastDeleteFileFailed": "Impossibile eliminare il file", From a5750deaaf824e99fec54848640c21d5395dd626 Mon Sep 17 00:00:00 2001 From: "Tomasz N." Date: Wed, 22 Oct 2025 23:02:49 +0200 Subject: [PATCH 038/285] The key change: Move the Database.bookAuthorModel.create() block outside the if (!author) check, so it runs whether the author was just created OR already existed in the database. This bug was visible when using "Match Books" for a library and the outcome was books had no author(s) assigned despite the custom providers correctly providing those values. --- server/scanner/Scanner.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 206068cc4..cf5c87b78 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -259,7 +259,7 @@ class Scanner { SocketAuthority.emitter('author_added', author.toOldJSON()) // Update filter data Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id) - + } await Database.bookAuthorModel .create({ authorId: author.id, @@ -270,7 +270,6 @@ class Scanner { libraryItem.media.authors.push(author) hasAuthorUpdates = true }) - } } const authorsRemoved = libraryItem.media.authors.filter((a) => !matchData.author.find((ma) => ma.toLowerCase() === a.name.toLowerCase())) if (authorsRemoved.length) { From f77de1743e2f764ba2d9b5b7fa8c3472d04624c4 Mon Sep 17 00:00:00 2001 From: James Tattersall <10601770+jamerst@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:31:37 +0100 Subject: [PATCH 039/285] Don't require listening today for 'Days in a row' stat --- client/components/stats/DailyListeningChart.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/components/stats/DailyListeningChart.vue b/client/components/stats/DailyListeningChart.vue index c0f3acb32..1c92e675d 100644 --- a/client/components/stats/DailyListeningChart.vue +++ b/client/components/stats/DailyListeningChart.vue @@ -186,10 +186,16 @@ export default { daysInARow() { var count = 0 while (true) { - var _date = this.$addDaysToToday(count * -1) + var _date = this.$addDaysToToday((count * -1) - 1) var datestr = this.$formatJsDate(_date, 'yyyy-MM-dd') if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) { + // don't require listening today to count towards days in a row, but do count it if already listened today + var today = this.$formatJsDate(new Date(), 'yyyy-MM-dd'); + if (this.listeningStatsDays[today]) { + count++; + } + return count } count++ From 372c9a5322bca988f66c4d714067bfa5a9922ac8 Mon Sep 17 00:00:00 2001 From: "Tomasz N." Date: Fri, 31 Oct 2025 15:36:47 +0100 Subject: [PATCH 040/285] Increasing the timeout for bookfinder - some metadata providers heavily throttle the requests, original 10s is not enough. --- server/finders/BookFinder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index fe1a61027..c25afe075 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -11,7 +11,7 @@ const { levenshteinDistance, levenshteinSimilarity, escapeRegExp, isValidASIN } const htmlSanitizer = require('../utils/htmlSanitizer') class BookFinder { - #providerResponseTimeout = 10000 + #providerResponseTimeout = 120000 constructor() { this.openLibrary = new OpenLibrary() From 961d066bdd5d18efa2d8774362b2b236a8ca2d82 Mon Sep 17 00:00:00 2001 From: "Tomasz N." Date: Fri, 31 Oct 2025 15:39:12 +0100 Subject: [PATCH 041/285] Wrong branch. --- server/finders/BookFinder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index c25afe075..fe1a61027 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -11,7 +11,7 @@ const { levenshteinDistance, levenshteinSimilarity, escapeRegExp, isValidASIN } const htmlSanitizer = require('../utils/htmlSanitizer') class BookFinder { - #providerResponseTimeout = 120000 + #providerResponseTimeout = 10000 constructor() { this.openLibrary = new OpenLibrary() From 2cf6e8a5fe9040b89e280146505c1241885f4788 Mon Sep 17 00:00:00 2001 From: Finn Dittmar Date: Fri, 7 Nov 2025 19:02:32 +0100 Subject: [PATCH 042/285] Support eac3 --- server/objects/Stream.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index d9bdd583c..8150c43c3 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -73,7 +73,7 @@ class Stream extends EventEmitter { return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF] } get codecsToForceAAC() { - return ['alac'] + return ['alac', 'ac3', 'eac3'] } get userToken() { return this.user.token @@ -273,7 +273,16 @@ class Stream extends EventEmitter { audioCodec = 'aac' } - this.ffmpeg.addOption([`-loglevel ${logLevel}`, '-map 0:a', `-c:a ${audioCodec}`]) + const codecOptions = [`-loglevel ${logLevel}`, '-map 0:a'] + + if (this.codecsToForceAAC.slice(1, 3).includes(this.tracksCodec)) { + // In case for ac3/eac3 it needs to be passed the bitrate and channels to avoid ffmpeg errors + codecOptions.push(`-c:a ${audioCodec}`, `-b:a ${this.tracks[0].bitRate}`, `-ac ${this.tracks[0].channels}`) + } else { + codecOptions.push(`-c:a ${audioCodec}`) + } + + this.ffmpeg.addOption(codecOptions) const hlsOptions = ['-f hls', '-copyts', '-avoid_negative_ts make_non_negative', '-max_delay 5000000', '-max_muxing_queue_size 2048', `-hls_time 6`, `-hls_segment_type ${this.hlsSegmentType}`, `-start_number ${this.segmentStartNumber}`, '-hls_playlist_type vod', '-hls_list_size 0', '-hls_allow_cache 0'] this.ffmpeg.addOption(hlsOptions) if (this.hlsSegmentType === 'fmp4') { From 3316505d1c9b8f9af2606ef628aeb765d309cf11 Mon Sep 17 00:00:00 2001 From: Finn Dittmar Date: Fri, 7 Nov 2025 19:12:38 +0100 Subject: [PATCH 043/285] Really makes sure nothing can break --- server/objects/Stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 8150c43c3..b3582329d 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -275,7 +275,7 @@ class Stream extends EventEmitter { const codecOptions = [`-loglevel ${logLevel}`, '-map 0:a'] - if (this.codecsToForceAAC.slice(1, 3).includes(this.tracksCodec)) { + if (this.codecsToForceAAC.slice(1, 3).includes(this.tracksCodec) && this.tracks.length > 0 && this.tracks[0].bitRate && this.tracks[0].channels) { // In case for ac3/eac3 it needs to be passed the bitrate and channels to avoid ffmpeg errors codecOptions.push(`-c:a ${audioCodec}`, `-b:a ${this.tracks[0].bitRate}`, `-ac ${this.tracks[0].channels}`) } else { From 763d8810e3fcff9b8e16ff4a6769654a9e0541d3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 8 Nov 2025 17:08:43 -0600 Subject: [PATCH 044/285] Update Stream ac3/eac3 check --- server/objects/Stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index b3582329d..5aa013e8e 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -275,7 +275,7 @@ class Stream extends EventEmitter { const codecOptions = [`-loglevel ${logLevel}`, '-map 0:a'] - if (this.codecsToForceAAC.slice(1, 3).includes(this.tracksCodec) && this.tracks.length > 0 && this.tracks[0].bitRate && this.tracks[0].channels) { + if (['ac3', 'eac3'].includes(this.tracksCodec) && this.tracks.length > 0 && this.tracks[0].bitRate && this.tracks[0].channels) { // In case for ac3/eac3 it needs to be passed the bitrate and channels to avoid ffmpeg errors codecOptions.push(`-c:a ${audioCodec}`, `-b:a ${this.tracks[0].bitRate}`, `-ac ${this.tracks[0].channels}`) } else { From 5e68936c20ba0fc3a21ccab7fb7c9bbc870500c7 Mon Sep 17 00:00:00 2001 From: mikiher Date: Mon, 10 Nov 2025 09:14:46 +0200 Subject: [PATCH 045/285] Update English strings for library watcher settings and scan notes #4095 --- client/strings/en-us.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 83acb5a69..fb2bcb281 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves", "LabelSettingsChromecastSupport": "Chromecast support", "LabelSettingsDateFormat": "Date Format", - "LabelSettingsEnableWatcher": "Automatically scan libraries for changes", - "LabelSettingsEnableWatcherForLibrary": "Automatically scan library for changes", + "LabelSettingsEnableWatcher": "Automatically watch libraries for changes", + "LabelSettingsEnableWatcherForLibrary": "Automatically watch library for changes", "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart", "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs", "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?", "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.

Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.

All clients using your server will be automatically refreshed.", - "MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.", + "MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the \"Automatically watch library for changes\" setting enabled - it will automatically detect changes in your library folders. Enable this feature if \"Automatically watch library for changes\" does not work for your file system (like NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}", "MessageSearchResultsFor": "Search results for", "MessageSelected": "{0} selected", From 01a833ea59e2bdc8174c02fac5ed8f6a9b01d4f1 Mon Sep 17 00:00:00 2001 From: Aleksandr Zakirov Date: Wed, 22 Oct 2025 00:36:30 +0200 Subject: [PATCH 046/285] Translated using Weblate (Estonian) Currently translated at 64.2% (747 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/et/ --- client/strings/et.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/strings/et.json b/client/strings/et.json index d34e0b7ba..d3ab4e760 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -490,13 +490,13 @@ "LabelStatsDaysListened": "Kuulatud päevad", "LabelStatsHours": "Tunnid", "LabelStatsInARow": "järjest", - "LabelStatsItemsFinished": "Lõpetatud üksused", + "LabelStatsItemsFinished": "Lõpetatud raamatud", "LabelStatsItemsInLibrary": "Üksused raamatukogus", "LabelStatsMinutes": "minuteid", "LabelStatsMinutesListening": "Kuulamise minutid", "LabelStatsOverallDays": "Kokku päevad", "LabelStatsOverallHours": "Kokku tunnid", - "LabelStatsWeekListening": "Nädala kuulamine", + "LabelStatsWeekListening": "Nädala keskmine", "LabelSubtitle": "Alapealkiri", "LabelSupportedFileTypes": "Toetatud failitüübid", "LabelTag": "Silt", @@ -561,7 +561,8 @@ "MessageAppriseDescription": "Selle funktsiooni kasutamiseks peate käivitama Apprise API eksemplari või API, mis töötleb samu päringuid.
Apprise API URL peaks olema täielik URL-rada teatise saatmiseks, näiteks kui teie API eksemplar töötab aadressil http://192.168.1.1:8337, siis peaksite sisestama http://192.168.1.1:8337/notify.", "MessageBackupsDescription": "Varukoopiad hõlmavad kasutajaid, kasutajate edenemist, raamatukogu üksikasju, serveri seadeid ja kaustades /metadata/items ja /metadata/authors salvestatud pilte. Varukoopiad ei hõlma ühtegi teie raamatukogu kaustades olevat faili.", "MessageBatchQuickMatchDescription": "Kiire sobitamine üritab lisada valitud üksustele puuduvad kaaned ja metaandmed. Luba allpool olevad valikud, et lubada Kiire sobitamine'il üle kirjutada olemasolevaid kaasi ja/või metaandmeid.", - "MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud", + "MessageBookshelfNoCollections": "Te pole veel ühtegi kollektsiooni teinud", + "MessageBookshelfNoCollectionsHelp": "Kollektsioonid on avalikud. Kõik kasutajad kellel on olemas ligipääs raamatukogule saavad neid näha.", "MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud", "MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi", "MessageBookshelfNoSeries": "Teil pole ühtegi seeriat", @@ -600,6 +601,7 @@ "MessageConfirmRenameTagMergeNote": "Märkus: See silt on juba olemas, nii et need ühendatakse.", "MessageConfirmRenameTagWarning": "Hoiatus! Sarnane silt erineva puhvriga on juba olemas \"{0}\".", "MessageConfirmSendEbookToDevice": "Olete kindel, et soovite saata {0} e-raamatu \"{1}\" seadmesse \"{2}\"?", + "MessageDaysListenedInTheLastYear": "{0} päeva kuuldud viimase aasta jooksul", "MessageDownloadingEpisode": "Episoodi allalaadimine", "MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda", "MessageEmbedFinished": "Manustamine lõpetatud!", From 82f512d4057fc45e4e3b0e6e01dfaab671f9c4de Mon Sep 17 00:00:00 2001 From: pryszczoskor Date: Mon, 27 Oct 2025 19:16:34 +0100 Subject: [PATCH 047/285] Translated using Weblate (Polish) Currently translated at 88.3% (1027 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 0b92dc89c..6f83720de 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -233,8 +233,8 @@ "LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji", "LabelAddToPlaylist": "Dodaj do playlisty", "LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty", - "LabelAddedAt": "Dodano", - "LabelAddedDate": "Dodano {0}", + "LabelAddedAt": "Dodano w", + "LabelAddedDate": "Dodano", "LabelAdminUsersOnly": "Tylko użytkownicy administracyjni", "LabelAll": "Wszystkie", "LabelAllEpisodesDownloaded": "Wszystkie odcinki pobrane", @@ -295,6 +295,7 @@ "LabelContinueListening": "Kontynuuj słuchanie", "LabelContinueReading": "Kontynuuj czytanie", "LabelContinueSeries": "Kontynuuj serię", + "LabelCorsAllowed": "Dozwolone źródła CORS", "LabelCover": "Okładka", "LabelCoverImageURL": "URL okładki", "LabelCoverProvider": "Dostawca okładki", From b42c7421b05af0c8e2e79aa358e10e7e486a7352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Sun, 26 Oct 2025 13:13:25 +0100 Subject: [PATCH 048/285] Translated using Weblate (Finnish) Currently translated at 95.5% (1111 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index 8d224d81c..6e2999bd5 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -182,6 +182,7 @@ "HeaderPlaylist": "Soittolista", "HeaderPlaylistItems": "Soittolistan kohteet", "HeaderPodcastsToAdd": "Lisättävät podcastit", + "HeaderPresets": "Esivalinnat", "HeaderPreviewCover": "Esikatsele kansikuvaa", "HeaderRSSFeedGeneral": "RSS yksityiskohdat", "HeaderRSSFeedIsOpen": "RSS syöte on avoinna", @@ -221,7 +222,7 @@ "LabelAbridged": "Lyhennetty", "LabelAbridgedChecked": "Lyhennetty (tarkistettu)", "LabelAbridgedUnchecked": "Lyhentämätön (tarkistamaton)", - "LabelAccessibleBy": "Saavutettavissa:", + "LabelAccessibleBy": "Saavutettavissa", "LabelAccountType": "Tilin tyyppi", "LabelAccountTypeAdmin": "Järjestelmänvalvoja", "LabelAccountTypeGuest": "Vieras", @@ -243,6 +244,7 @@ "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.", + "LabelApiKeyUser": "Toimi käyttäjän puolesta", "LabelApiToken": "Sovellusliittymätunnus", "LabelAppend": "Lisää loppuun", "LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)", @@ -523,7 +525,7 @@ "LabelPublishers": "Julkaisijat", "LabelRSSFeedCustomOwnerEmail": "Mukautettu omistajan sähköposti", "LabelRSSFeedCustomOwnerName": "Mukautettu omistajan nimi", - "LabelRSSFeedOpen": "RSS-syöte avoin", + "LabelRSSFeedOpen": "RSS Syöte Avoin", "LabelRSSFeedPreventIndexing": "Estä indeksointi", "LabelRSSFeedSlug": "RSS-syöte Slug", "LabelRSSFeedURL": "RSS-syötteen URL-osoite", From c1f4e4120ee95003ff76664f5aed3763742fa997 Mon Sep 17 00:00:00 2001 From: dapitch666 Date: Thu, 30 Oct 2025 14:22:17 +0100 Subject: [PATCH 049/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index e744b15ef..e90cd73a2 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -172,7 +172,7 @@ "HeaderNotificationUpdate": "Mise à jour de la notification", "HeaderNotifications": "Notifications", "HeaderOpenIDConnectAuthentication": "Authentification via OpenID Connect", - "HeaderOpenListeningSessions": "Ouvrir les sessions d'écoutes", + "HeaderOpenListeningSessions": "Sessions d'écoute ouvertes", "HeaderOpenRSSFeed": "Ouvrir le flux RSS", "HeaderOtherFiles": "Autres fichiers", "HeaderPasswordAuthentication": "Authentification par mot de passe", @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Échec de l’ajout de(s) élément(s) à la collection", "ToastCollectionRemoveSuccess": "Collection supprimée", "ToastCollectionUpdateSuccess": "Collection mise à jour", + "ToastConnectionNotAvailable": "Connexion indisponible. Essayez plus tard", + "ToastCoverSearchFailed": "La recherche de la couverture a échoué", "ToastCoverUpdateFailed": "Échec de la mise à jour de la couverture", "ToastDateTimeInvalidOrIncomplete": "La date et l'heure sont invalides ou incomplètes", "ToastDeleteFileFailed": "Échec de la suppression du fichier", From f91be185272b2a00bc8a8c7cc772f7f15894d555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Gon=C3=A7alves=20Jaksys?= Date: Fri, 31 Oct 2025 22:21:01 +0100 Subject: [PATCH 050/285] Translated using Weblate (Portuguese (Brazil)) Currently translated at 93.1% (1083 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/ --- client/strings/pt-br.json | 195 +++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 2 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 53b9199e2..5f207154e 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -143,8 +143,8 @@ "HeaderEmail": "Email", "HeaderEmailSettings": "Configurações de Email", "HeaderEpisodes": "Episódios", - "HeaderEreaderDevices": "Dispositivos Ereader", - "HeaderEreaderSettings": "Configurações Ereader", + "HeaderEreaderDevices": "Dispositivos leitores de ebook", + "HeaderEreaderSettings": "Configurações do leitor de ebook", "HeaderFiles": "Arquivos", "HeaderFindChapters": "Localizar Capítulos", "HeaderIgnoredFiles": "Arquivos Ignorados", @@ -349,24 +349,36 @@ "LabelEnd": "Fim", "LabelEndOfChapter": "Fim do Capítulo", "LabelEpisode": "Episódio", + "LabelEpisodeNotLinkedToRssFeed": "Episódio não vinculado ao feed RSS", + "LabelEpisodeNumber": "Episódio #{0}", "LabelEpisodeTitle": "Título do Episódio", "LabelEpisodeType": "Tipo do Episódio", + "LabelEpisodeUrlFromRssFeed": "URL do episódio a partir do feed RSS", "LabelEpisodes": "Episódios", + "LabelEpisodic": "Episódico", "LabelExample": "Exemplo", + "LabelExpandSeries": "Expandir Série", + "LabelExpandSubSeries": "Expandir Subséries", "LabelExpired": "Expirado", + "LabelExpiresAt": "Expira às", + "LabelExpiresInSeconds": "Expira em (segundos)", "LabelExpiresNever": "Nunca", "LabelExplicit": "Explícito", "LabelExplicitChecked": "Explícito (verificado)", "LabelExplicitUnchecked": "Não explícito (não verificado)", + "LabelExportOPML": "Exportar OPML", "LabelFeedURL": "URL do Feed", "LabelFetchingMetadata": "Buscando Metadados", "LabelFile": "Arquivo", "LabelFileBirthtime": "Criação do Arquivo", + "LabelFileBornDate": "Criado {0}", "LabelFileModified": "Modificação do Arquivo", + "LabelFileModifiedDate": "Modificado {0}", "LabelFilename": "Nome do Arquivo", "LabelFilterByUser": "Filtrar por Usuário", "LabelFindEpisodes": "Localizar Episódios", "LabelFinished": "Concluído", + "LabelFinishedDate": "Concluído {0}", "LabelFolder": "Pasta", "LabelFolders": "Pastas", "LabelFontBold": "Negrito", @@ -376,6 +388,7 @@ "LabelFontScale": "Escala de fonte", "LabelFontStrikethrough": "Tachado", "LabelFormat": "Formato", + "LabelFull": "Cheio", "LabelGenre": "Gênero", "LabelGenres": "Gêneros", "LabelHardDeleteFile": "Apagar definitivamente", @@ -403,11 +416,14 @@ "LabelIntervalEveryMinute": "A cada minuto", "LabelInvert": "Inverter", "LabelItem": "Item", + "LabelJumpBackwardAmount": "Tempo de retrocesso", + "LabelJumpForwardAmount": "Tempo de avanço", "LabelLanguage": "Idioma", "LabelLanguageDefaultServer": "Idioma Padrão do Servidor", "LabelLanguages": "Idiomas", "LabelLastBookAdded": "Último Livro Acrescentado", "LabelLastBookUpdated": "Último Livro Atualizado", + "LabelLastProgressDate": "Último progresso: {0}", "LabelLastSeen": "Visto pela Última Vez", "LabelLastTime": "Progresso", "LabelLastUpdate": "Última Atualização", @@ -422,6 +438,7 @@ "LabelLibraryName": "Nome da Biblioteca", "LabelLibrarySortByProgress": "Última Atualização", "LabelLibrarySortByProgressFinished": "Concluído", + "LabelLibrarySortByProgressStarted": "Progresso: Iniciado", "LabelLimit": "Limite", "LabelLineSpacing": "Espaçamento entre linhas", "LabelListenAgain": "Escutar novamente", @@ -430,8 +447,13 @@ "LabelLogLevelWarn": "Atenção", "LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data", "LabelLowestPriority": "Prioridade mais baixa", + "LabelMatchConfidence": "Confiabilidade", "LabelMatchExistingUsersBy": "Consultar usuários existentes usando", "LabelMatchExistingUsersByDescription": "Utilizado para conectar usuários já existentes. Uma vez conectados, usuários serão consultados utilizando uma identificação única do seu provedor de SSO", + "LabelMaxEpisodesToDownload": "Número máximo de episódios para baixar. Use 0 para download ilimitado.", + "LabelMaxEpisodesToDownloadPerCheck": "Número máximo de novos episódios para baixar por verificação", + "LabelMaxEpisodesToKeep": "Número máximo de episódios para manter", + "LabelMaxEpisodesToKeepHelp": "O valor 0 define que não há limite máximo. Após um novo episódio ser baixado automaticamente, o episódio mais antigo será excluído caso você tenha mais de X episódios. Apenas um episódio será excluído a cada novo download.", "LabelMediaPlayer": "Reprodutor de mídia", "LabelMediaType": "Tipo de Mídia", "LabelMetaTag": "Etiqueta Meta", @@ -457,6 +479,7 @@ "LabelNextBackupDate": "Data do próximo backup", "LabelNextChapters": "Próximo capítulo será:", "LabelNextScheduledRun": "Próxima execução programada", + "LabelNoApiKeys": "Sem chaves de API", "LabelNoCustomMetadataProviders": "Não existem fontes de metadados customizados", "LabelNoEpisodesSelected": "Nenhum episódio selecionado", "LabelNotFinished": "Não concluído", @@ -472,6 +495,7 @@ "LabelNotificationsMaxQueueSize": "Tamanho máximo da fila de eventos de notificação", "LabelNotificationsMaxQueueSizeHelp": "Eventos estão limitados a um disparo por segundo. Eventos serão ignorados se a fila estiver no tamanho máximo. Isso evita o excesso de notificações.", "LabelNumberOfBooks": "Número de Livros", + "LabelNumberOfChapters": "Número de capítulos:", "LabelNumberOfEpisodes": "# de Episódios", "LabelOpenIDAdvancedPermsClaimDescription": "Nome do claim OpenID contendo as permissões avançadas para ações do usuário na aplicação para serem aplicadas aos perfis não-administradores (se configurados). Se o claim não estiver presente na resposta, acesso ao ABS será negado. Se apenas uma opção estiver ausente, ela será tratada como false. Garanta que o claim do provedor de identidade segue a estrutura esperada:", "LabelOpenIDClaims": "Deixe as opções a seguir em branco para desativar a atribuição de grupos e permissões avançadas; nesse caso, o grupo 'Usuário' será atribuído automaticamente.", @@ -485,6 +509,7 @@ "LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas", "LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas", "LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos", + "LabelPermissionsCreateEreader": "Pode criar leitor de ebooks", "LabelPermissionsDelete": "Pode Apagar", "LabelPermissionsDownload": "Pode Fazer Download", "LabelPermissionsUpdate": "Pode Atualizar", @@ -492,6 +517,7 @@ "LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})", "LabelPhotoPathURL": "Caminho/URL para Foto", "LabelPlayMethod": "Método de Reprodução", + "LabelPlaybackRateIncrementDecrement": "Valor de incremento/decremento da taxa de reprodução", "LabelPlayerChapterNumberMarker": "{0} de {1}", "LabelPlaylists": "Listas de Reprodução", "LabelPodcast": "Podcast", @@ -504,9 +530,12 @@ "LabelPrimaryEbook": "Ebook principal", "LabelProgress": "Progresso", "LabelProvider": "Fonte", + "LabelProviderAuthorizationValue": "Valor do Cabeçalho de Autorização", "LabelPubDate": "Data de Publicação", "LabelPublishYear": "Ano de Publicação", "LabelPublishedDate": "Publicado {0}", + "LabelPublishedDecade": "Década de Publicação", + "LabelPublishedDecades": "Décadas de Publicação", "LabelPublisher": "Editora", "LabelPublishers": "Editoras", "LabelRSSFeedCustomOwnerEmail": "E-mail do dono personalizado", @@ -516,6 +545,7 @@ "LabelRSSFeedSlug": "Slug do Feed RSS", "LabelRSSFeedURL": "URL do Feed RSS", "LabelRandomly": "Aleatoriamente", + "LabelReAddSeriesToContinueListening": "Adicionar novamente a série à \"Continuar ouvindo\"", "LabelRead": "Lido", "LabelReadAgain": "Ler novamente", "LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso", @@ -525,24 +555,34 @@ "LabelRedo": "Refazer", "LabelRegion": "Região", "LabelReleaseDate": "Data de Lançamento", + "LabelRemoveAllMetadataAbs": "Remover todos os arquivos metadata.abs", + "LabelRemoveAllMetadataJson": "Remover todos os arquivos metadata.json", + "LabelRemoveAudibleBranding": "Remover introdução e encerramento da Audible dos capítulos", "LabelRemoveCover": "Remover capa", + "LabelRemoveMetadataFile": "Remover arquivos de metadados nas pastas da biblioteca", + "LabelRemoveMetadataFileHelp": "Remover todos os arquivos metadata.json e metadata.abs em suas {0} pastas.", "LabelRowsPerPage": "Linhas por Página", "LabelSearchTerm": "Busca por Termo", "LabelSearchTitle": "Busca por Título", "LabelSearchTitleOrASIN": "Busca por Título ou ASIN", "LabelSeason": "Temporada", + "LabelSeasonNumber": "Temporada #{0}", "LabelSelectAll": "Selecionar todos", "LabelSelectAllEpisodes": "Selecionar todos os Episódios", "LabelSelectEpisodesShowing": "Selecionar os {0} Episódios Visíveis", + "LabelSelectUser": "Selecionar usuário", "LabelSelectUsers": "Selecionar usuários", "LabelSendEbookToDevice": "Enviar Ebook para...", "LabelSequence": "Sequência", + "LabelSerial": "Serial", "LabelSeries": "Série", "LabelSeriesName": "Nome da Série", "LabelSeriesProgress": "Progresso da Série", + "LabelServerLogLevel": "Nível de Logs do Servidor", "LabelServerYearReview": "Retrospectiva Anual do Servidor ({0})", "LabelSetEbookAsPrimary": "Definir como principal", "LabelSetEbookAsSupplementary": "Definir como complementar", + "LabelSettingsAllowIframe": "Permitir incorporação em iframe", "LabelSettingsAudiobooksOnly": "Apenas Audiobooks", "LabelSettingsAudiobooksOnlyHelp": "Ao ativar essa configuração os arquivos de ebooks serão ignorados a não ser que estejam dentro de uma pasta com um audiobook. Nesse caso eles serão definidos como ebooks complementares", "LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira", @@ -561,6 +601,9 @@ "LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.", "LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal", "LabelSettingsLibraryBookshelfView": "Usar visão estante na página da biblioteca", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "O percentual de conclusão é maior que", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "O tempo restante é inferior a (segundos)", + "LabelSettingsLibraryMarkAsFinishedWhen": "Marcar o item de mídia como concluído quando", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Pular livros anteriores em Continuar Série", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "A prateleira Continuar Série na página principal de exibe o primeiro livro não iniciado em uma série que tem pelo menos um livro concluído e nenhum livro em andamento. Ativar essa configuração irá continuar a série a partir do livro mais recentemente concluído ao invés do primeiro livro não iniciado.", "LabelSettingsParseSubtitles": "Analisar subtítulos", @@ -579,6 +622,8 @@ "LabelSettingsStoreMetadataWithItemHelp": "Por padrão os arquivos de metadados são armazenados em /metadata/items. Ao ativar essa configuração os arquivos de metadados serão armazenadas nas pastas dos itens na sua biblioteca", "LabelSettingsTimeFormat": "Formato da Tempo", "LabelShare": "Compartilhar", + "LabelShareDownloadableHelp": "Permitir que os usuários com o link de compartilhamento baixem um arquivo zip do item da biblioteca.", + "LabelShareOpen": "Abrir compartilhamento", "LabelShareURL": "Compartilhar URL", "LabelShowAll": "Exibir Todos", "LabelShowSeconds": "Exibir segundos", @@ -586,10 +631,14 @@ "LabelSize": "Tamanho", "LabelSleepTimer": "Timer", "LabelSlug": "Slug", + "LabelSortAscending": "Crescente", + "LabelSortDescending": "Decrescente", + "LabelSortPubDate": "Ordenar por data de publicação", "LabelStart": "Iniciar", "LabelStartTime": "Horário do Início", "LabelStarted": "Iniciado", "LabelStartedAt": "Iniciado Em", + "LabelStartedDate": "Iniciado {0}", "LabelStatsAudioTracks": "Trilhas de Áudio", "LabelStatsAuthors": "Autores", "LabelStatsBestDay": "Melhor Dia", @@ -624,6 +673,8 @@ "LabelTimeDurationXHours": "{0} horas", "LabelTimeDurationXMinutes": "{0} minutos", "LabelTimeDurationXSeconds": "{0} segundos", + "LabelTimeInMinutes": "Tempo em minutos", + "LabelTimeLeft": "{0} restante", "LabelTimeListened": "Tempo de escuta", "LabelTimeListenedToday": "Tempo de escuta hoje", "LabelTimeRemaining": "{0} restantes", @@ -631,6 +682,7 @@ "LabelTitle": "Título", "LabelToolsEmbedMetadata": "Incluir Metadados", "LabelToolsEmbedMetadataDescription": "Incluir metadados no arquivo de áudio, com imagem da capa e capítulos.", + "LabelToolsM4bEncoder": "Codificador M4B", "LabelToolsMakeM4b": "Gerar audiobook no formato M4B", "LabelToolsMakeM4bDescription": "Gerar um arquivo de audiobook no formato .M4B com metadados, imagem da capa e capítulos.", "LabelToolsSplitM4b": "Dividir um M4B em MP3s", @@ -648,24 +700,31 @@ "LabelUnabridged": "Não Abreviada", "LabelUndo": "Desfazer", "LabelUnknown": "Desconhecido", + "LabelUnknownPublishDate": "Data de publicação desconhecida", "LabelUpdateCover": "Atualizar Capa", "LabelUpdateCoverHelp": "Permite sobrescrever capas existentes para os livros selecionados quando uma consulta for localizada", "LabelUpdateDetails": "Atualizar Detalhes", "LabelUpdateDetailsHelp": "Permite sobrescrever detalhes existentes para os livros selecionados quando uma consulta for localizada", "LabelUpdatedAt": "Atualizado em", "LabelUploaderDragAndDrop": "Arraste e solte arquivos ou pastas", + "LabelUploaderDragAndDropFilesOnly": "Arraste e solte arquivos", "LabelUploaderDropFiles": "Solte os arquivos", "LabelUploaderItemFetchMetadataHelp": "Busca título, autor e série automaticamente", + "LabelUseAdvancedOptions": "Usar Opções Avançadas", "LabelUseChapterTrack": "Usar a trilha do capítulo", "LabelUseFullTrack": "Usar a trilha toda", + "LabelUseZeroForUnlimited": "Use 0 para ilimitado", "LabelUser": "Usuário", "LabelUsername": "Nome do usuário", "LabelValue": "Valor", "LabelVersion": "Versão", "LabelViewBookmarks": "Ver marcadores", "LabelViewChapters": "Ver capítulos", + "LabelViewPlayerSettings": "Ver configurações do reprodutor", "LabelViewQueue": "Ver fila do reprodutor", "LabelVolume": "Volume", + "LabelWebRedirectURLsDescription": "Autorize esses URLs em seu provedor OAuth para permitir o redirecionamento de volta ao aplicativo web após o login:", + "LabelWebRedirectURLsSubfolder": "Subpasta de URLs de redirecionamento", "LabelWeekdaysToRun": "Dias da semana para executar", "LabelXBooks": "{0} livros", "LabelXItems": "{0} itens", @@ -677,41 +736,64 @@ "LabelYourProgress": "Seu Progresso", "MessageAddToPlayerQueue": "Adicionar à lista do reprodutor", "MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da API do Apprise em execução ou uma API que possa tratar esses mesmos chamados.
A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em http://192.168.1.1:8337 você deve utilizar http://192.168.1.1:8337/notify.", + "MessageAsinCheck": "Certifique-se de estar usando o ASIN da região correta da Audible, e não da Amazon.", + "MessageAuthenticationLegacyTokenWarning": "Os tokens de API legados serão removidos no futuro. Use Chaves de API em vez disso.", + "MessageAuthenticationOIDCChangesRestart": "Reinicie o servidor após salvar para aplicar as alterações do OIDC.", + "MessageAuthenticationSecurityMessage": "A autenticação foi aprimorada para maior segurança. Todos os usuários precisam fazer login novamente.", "MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em /metadata/items & /metadata/authors. Backups não incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.", + "MessageBackupsLocationEditNote": "Observação: Atualizar o local de backup não moverá nem modificará os backups existentes.", + "MessageBackupsLocationNoEditNote": "Observação: o local de backup é definido por meio de uma variável de ambiente e não pode ser alterado aqui.", + "MessageBackupsLocationPathEmpty": "O caminho do local de backup não pode ser vazio.", + "MessageBatchEditPopulateMapDetailsAllHelp": "Preencha os campos habilitados com dados de todos os itens. Os campos com múltiplos valores serão mesclados.", + "MessageBatchEditPopulateMapDetailsItemHelp": "Preencha os campos de detalhes do mapa habilitados com dados deste item.", "MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.", "MessageBookshelfNoCollections": "Você ainda não criou coleções", + "MessageBookshelfNoCollectionsHelp": "As coleções são públicas. Todos os usuários com acesso à biblioteca podem vê-las.", "MessageBookshelfNoRSSFeeds": "Não existem feeds RSS abertos", "MessageBookshelfNoResultsForFilter": "Sem Resultados para o filtro \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Sem resultados para a consulta", "MessageBookshelfNoSeries": "Você não tem séries", + "MessageBulkChapterPattern": "Quantos capítulos você gostaria de adicionar seguindo esse padrão de numeração?", "MessageChapterEndIsAfter": "O final do capítulo está além do final do seu audiobook", "MessageChapterErrorFirstNotZero": "O primeiro capítulo precisa começar no 0", "MessageChapterErrorStartGteDuration": "Tempo de início não é válido pois precisa ser menor do que a duração do audioboook", "MessageChapterErrorStartLtPrev": "Tempo de início não é válido pois precisa ser igual ou maior que o tempo de início do capítulo anterior", "MessageChapterStartIsAfter": "Início do capítulo está além do final do seu audiobook", + "MessageChaptersNotFound": "Capítulos não encontrados", "MessageCheckingCron": "Verificando o cron...", "MessageConfirmCloseFeed": "Tem certeza de que deseja fechar esse feed?", + "MessageConfirmDeleteApiKey": "Tem certeza de que deseja excluir a chave de API \"{0}\"?", "MessageConfirmDeleteBackup": "Tem certeza de que deseja apagar o backup {0}?", + "MessageConfirmDeleteDevice": "Tem certeza de que deseja excluir o leitor de ebook \"{0}\"?", "MessageConfirmDeleteFile": "Essa ação apagará o arquivo do seu sistema de arquivos. Tem certeza?", "MessageConfirmDeleteLibrary": "Tem certeza de que deseja apagar a biblioteca \"{0}\" definitivamente?", "MessageConfirmDeleteLibraryItem": "Essa ação apagará o item da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?", "MessageConfirmDeleteLibraryItems": "Essa ação apagará {0} itens da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?", + "MessageConfirmDeleteMetadataProvider": "Tem certeza de que deseja excluir o provedor de metadados personalizado \"{0}\"?", + "MessageConfirmDeleteNotification": "Tem certeza de que deseja excluir esta notificação?", "MessageConfirmDeleteSession": "Tem certeza de que deseja apagar essa sessão?", + "MessageConfirmEmbedMetadataInAudioFiles": "Tem certeza de que deseja incorporar metadados em arquivos de áudio {0}?", "MessageConfirmForceReScan": "Tem certeza de que deseja forçar a nova verificação?", "MessageConfirmMarkAllEpisodesFinished": "Tem certeza de que deseja marcar todos os episódios como concluídos?", "MessageConfirmMarkAllEpisodesNotFinished": "Tem certeza de que deseja marcar todos os episódios como não concluídos?", + "MessageConfirmMarkItemFinished": "Tem certeza de que deseja marcar \"{0}\" como concluído?", + "MessageConfirmMarkItemNotFinished": "Tem certeza de que deseja marcar \"{0}\" como não concluído?", "MessageConfirmMarkSeriesFinished": "Tem certeza de que deseja marcar todos os livros nesta série como concluídos?", "MessageConfirmMarkSeriesNotFinished": "Tem certeza de que deseja marcar todos os livros nesta série como não concluídos?", + "MessageConfirmNotificationTestTrigger": "Acionar esta notificação com dados de teste?", "MessageConfirmPurgeCache": "Apagar o cache irá apagar o diretório todo localizado em /metadata/cache.

Tem certeza que deseja apagar o diretório de cache?", "MessageConfirmPurgeItemsCache": "Apagar o cache de itens irá apagar todo conteúdo da pasta /metadata/cache/items.
Tem certeza?", "MessageConfirmQuickEmbed": "Aviso! Inclusão rápida não fará backup dos seus arquivos de áudio. Verifique se tem um backup dos seus arquivos de áudio.

Quer continuar?", + "MessageConfirmQuickMatchEpisodes": "A correspondência rápida de episódios sobrescreverá os detalhes se uma correspondência for encontrada. Somente os episódios sem correspondência serão atualizados. Tem certeza?", "MessageConfirmReScanLibraryItems": "Tem certeza de que deseja uma nova verificação de {0} itens?", "MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos os capítulos?", "MessageConfirmRemoveAuthor": "Tem certeza de que deseja remover o autor \"{0}\"?", "MessageConfirmRemoveCollection": "Tem certeza de que deseja remover a coleção \"{0}\"?", "MessageConfirmRemoveEpisode": "Tem certeza de que deseja remover o episódio \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Observação: Isso não exclui o arquivo de áudio, a menos que você ative a opção \"Excluir arquivo permanentemente\"", "MessageConfirmRemoveEpisodes": "Tem certeza de que deseja remover os {0} episódios?", "MessageConfirmRemoveListeningSessions": "Tem certeza de que deseja remover as {0} sessões de escuta?", + "MessageConfirmRemoveMetadataFiles": "Tem certeza de que deseja remover todos os arquivos metadata.{0} nas pastas dos itens da sua biblioteca?", "MessageConfirmRemoveNarrator": "Tem certeza de que deseja remover o narrador \"{0}\"?", "MessageConfirmRemovePlaylist": "Tem certeza de que deseja remover a sua lista de reprodução \"{0}\"?", "MessageConfirmRenameGenre": "Tem certeza de que deseja renomear o gênero \"{0}\" para \"{1}\" em todos os itens?", @@ -720,17 +802,25 @@ "MessageConfirmRenameTag": "Tem certeza de que deseja renomear a etiqueta \"{0}\" para \"{1}\" em todos os itens?", "MessageConfirmRenameTagMergeNote": "Aviso: Esta etiqueta já existe então elas serão combinadas.", "MessageConfirmRenameTagWarning": "Atenção! Uma etiqueta com um nome semelhante já existe \"{0}\".", + "MessageConfirmResetProgress": "Tem certeza de que deseja redefinir seu progresso?", "MessageConfirmSendEbookToDevice": "Tem certeza de que deseja enviar {0} ebook(s) \"{1}\" para o dispositivo \"{2}\"?", + "MessageConfirmUnlinkOpenId": "Tem certeza de que deseja desvincular este usuário do OpenID?", + "MessageDaysListenedInTheLastYear": "{0} dias ouvidos no último ano", "MessageDownloadingEpisode": "Realizando o download do episódio", "MessageDragFilesIntoTrackOrder": "Arraste os arquivos para ordenar as trilhas corretamente", + "MessageEmbedFailed": "Falha ao incorporar!", "MessageEmbedFinished": "Inclusão Concluída!", + "MessageEmbedQueue": "Enfileirado para incorporação de metadados ({0} na fila)", "MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download", "MessageEreaderDevices": "Para garantir a entrega dos ebooks, você pode precisar adicionar o endereço de email acima como um remetente válido para cada um dos dispositivos listados abaixo.", "MessageFeedURLWillBe": "URL do Feed será {0}", "MessageFetching": "Buscando...", "MessageForceReScanDescription": "verificará todos os arquivos, como uma verificação nova. Etiquetas ID3 de arquivos de áudio, arquivos OPF e arquivos de texto serão tratados como novos.", + "MessageHeatmapListeningTimeTooltip": "{0} ouvindo em {1}", + "MessageHeatmapNoListeningSessions": "Nenhuma sessão de reprodução em {0}", "MessageImportantNotice": "Aviso Importante!", "MessageInsertChapterBelow": "Inserir capítulo abaixo", + "MessageInvalidAsin": "ASIN inválido", "MessageItemsSelected": "{0} itens selecionados", "MessageItemsUpdated": "{0} itens atualizados", "MessageJoinUsOn": "Junte-se a nós", @@ -753,6 +843,7 @@ "MessageNoCollections": "Sem Coleções", "MessageNoCoversFound": "Nenhuma Capa Encontrada", "MessageNoDescription": "Sem Descrições", + "MessageNoDevices": "Nenhum dispositivo", "MessageNoDownloadsInProgress": "Não existem downloads em andamento", "MessageNoDownloadsQueued": "Não existem itens na fila de download", "MessageNoEpisodeMatchesFound": "Não existem episódios correspondentes", @@ -766,6 +857,7 @@ "MessageNoLogs": "Sem Logs", "MessageNoMediaProgress": "Sem Progresso de Mídia", "MessageNoNotifications": "Sem Notificações", + "MessageNoPodcastFeed": "Podcast inválido: Sem feed", "MessageNoPodcastsFound": "Nenhum podcast encontrado", "MessageNoResults": "Sem resultados", "MessageNoSearchResultsFor": "Sem resultados para \"{0}\"", @@ -774,13 +866,19 @@ "MessageNoTasksRunning": "Sem Tarefas em Execução", "MessageNoUpdatesWereNecessary": "Nenhuma atualização é necessária", "MessageNoUserPlaylists": "Você não tem listas de reprodução", + "MessageNoUserPlaylistsHelp": "As listas de reprodução são privadas. Somente o usuário que as criou pode vê-las.", "MessageNotYetImplemented": "Ainda não implementado", + "MessageOpmlPreviewNote": "Nota: esta é uma visualização do arquivo OPML processado. O título verdadeiro do podcast será recuperado do feed RSS.", "MessageOr": "ou", "MessagePauseChapter": "Pausar reprodução do capítulo", "MessagePlayChapter": "Escutar o início do capítulo", "MessagePlaylistCreateFromCollection": "Criar uma lista de reprodução a partir da coleção", + "MessagePleaseWait": "Por favor, aguarde...", "MessagePodcastHasNoRSSFeedForMatching": "Podcast não tem uma URL do feed RSS para ser usada na consulta", "MessagePodcastSearchField": "Digite um termo para a busca ou a URL de um feed RSS", + "MessageQuickEmbedInProgress": "Incorporação rápida em andamento", + "MessageQuickEmbedQueue": "Enfileirado para incorporação rápida ({0} na fila)", + "MessageQuickMatchAllEpisodes": "Correspondência Rápida de Todos os Episódios", "MessageQuickMatchDescription": "Preenche detalhes vazios do item & capa com o primeiro resultado de '{0}'. Não sobrescreve detalhes a não ser que a configuração 'Preferir metadados consultados' do servidor esteja ativa.", "MessageRemoveChapter": "Remover capítulo", "MessageRemoveEpisodes": "Remover {0} episódio(s)", @@ -790,12 +888,52 @@ "MessageResetChaptersConfirm": "Tem certeza de que deseja resetar os capítulos e desfazer as alterações realizadas?", "MessageRestoreBackupConfirm": "Tem certeza de que deseja restaurar o backup criado em", "MessageRestoreBackupWarning": "Restaurar um backup sobrescreverá totalmente o banco de dados localizado em /config e as imagens de capa em /metadata/items & /metadata/authors.

Backups não alteram quaisquer arquivos nas pastas da sua biblioteca. Se a configuração do servidor de armazenar a arte da capa e os metadados nas pastas da sua biblioteca estiver ativa, esses itens não estão no backup e não serão sobrescritos.

Todos os clientes usando o seu servidor serão atualizados automaticamente.", + "MessageScheduleLibraryScanNote": "Para a maioria dos usuários, é recomendável deixar este recurso desativado e manter a configuração de monitoramento de pastas ativada. O monitor de pastas detectará automaticamente as alterações nas pastas da sua biblioteca. O monitor de pastas não funciona em todos os sistemas de arquivos (como o NFS), portanto, podem ser usadas verificações agendadas da biblioteca como alternativa.", + "MessageScheduleRunEveryWeekdayAtTime": "Executar a cada {0} às {1}", "MessageSearchResultsFor": "Resultado da busca por", "MessageSelected": "{0} selecionado(s)", + "MessageSeriesSequenceCannotContainSpaces": "O identificador da série não pode conter espaços", "MessageServerCouldNotBeReached": "Não foi possível estabelecer conexão com o servidor", "MessageSetChaptersFromTracksDescription": "Definir os capítulos usando cada arquivo de áudio como um capítulo e o nome do arquivo como o título do capítulo", + "MessageShareExpirationWillBe": "Válido até {0}", + "MessageShareExpiresIn": "Expira em {0}", + "MessageShareURLWillBe": "A URL de compartilhamento será {0}", "MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?", + "MessageTaskAudioFileNotWritable": "O arquivo de áudio \"{0}\" não pode ser gravado", + "MessageTaskCanceledByUser": "Tarefa cancelada pelo usuário", + "MessageTaskDownloadingEpisodeDescription": "Baixando episódio \"{0}\"", + "MessageTaskEmbeddingMetadata": "Incorporação de metadados", + "MessageTaskEmbeddingMetadataDescription": "Incorporando metadados no audiolivro \"{0}\"", + "MessageTaskEncodingM4b": "Codificação M4B", + "MessageTaskEncodingM4bDescription": "Codificação do audiolivro \"{0}\" em um único arquivo m4b", "MessageTaskFailed": "Falhou", + "MessageTaskFailedToBackupAudioFile": "Falha ao fazer backup do arquivo de áudio \"{0}\"", + "MessageTaskFailedToCreateCacheDirectory": "Falha ao criar o diretório de cache", + "MessageTaskFailedToEmbedMetadataInFile": "Falha ao incorporar metadados no arquivo \"{0}\"", + "MessageTaskFailedToMergeAudioFiles": "Falha ao mesclar arquivos de áudio", + "MessageTaskFailedToMoveM4bFile": "Falha ao mover o arquivo m4b", + "MessageTaskFailedToWriteMetadataFile": "Falha ao gravar o arquivo de metadados", + "MessageTaskMatchingBooksInLibrary": "Livros correspondentes na biblioteca \"{0}\"", + "MessageTaskNoFilesToScan": "Não há arquivos para verificar", + "MessageTaskOpmlImport": "Importação OPML", + "MessageTaskOpmlImportDescription": "Criando podcasts a partir de feeds RSS {0}", + "MessageTaskOpmlImportFeed": "Feed de importação OPML", + "MessageTaskOpmlImportFeedDescription": "Importando o feed RSS \"{0}\"", + "MessageTaskOpmlImportFeedFailed": "Não foi possível obter o feed do podcast", + "MessageTaskOpmlImportFeedPodcastDescription": "Criando podcast \"{0}\"", + "MessageTaskOpmlImportFeedPodcastExists": "O podcast já existe no caminho", + "MessageTaskOpmlImportFeedPodcastFailed": "Falha ao criar o podcast", + "MessageTaskOpmlImportFinished": "Adicionados {0} podcasts", + "MessageTaskOpmlParseFailed": "Falha ao analisar o arquivo OPML", + "MessageTaskOpmlParseFastFail": "Arquivo OPML inválido. Tag não encontrada OU tag não encontrada.", + "MessageTaskOpmlParseNoneFound": "Nenhum feed encontrado no arquivo OPML", + "MessageTaskScanItemsAdded": "{0} adicionado", + "MessageTaskScanItemsMissing": "{0} não encontrado", + "MessageTaskScanItemsUpdated": "{0} atualizado", + "MessageTaskScanNoChangesNeeded": "Nenhuma alteração necessária", + "MessageTaskScanningFileChanges": "Verificando alterações de arquivo em \"{0}\"", + "MessageTaskScanningLibrary": "Analisando a biblioteca \"{0}\"", + "MessageTaskTargetDirectoryNotWritable": "Diretório de destino sem permissão de escrita", "MessageThinking": "Pensando...", "MessageUploaderItemFailed": "Falha no upload", "MessageUploaderItemSuccess": "Upload realizado!", @@ -813,6 +951,12 @@ "NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.", "NoteUploaderOnlyAudioFiles": "Ao subir apenas arquivos de áudio, cada arquivo será tratado como um audiobook distinto.", "NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar uma pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.", + "NotificationOnBackupCompletedDescription": "Acionado quando um backup é concluído", + "NotificationOnBackupFailedDescription": "Acionado quando um backup falha", + "NotificationOnEpisodeDownloadedDescription": "Acionado quando um episódio de podcast é baixado automaticamente", + "NotificationOnRSSFeedDisabledDescription": "Acionado quando os downloads automáticos de episódios são desativados devido a muitas tentativas falhas", + "NotificationOnRSSFeedFailedDescription": "Acionado quando a solicitação do feed RSS falha durante o download automático de um episódio", + "NotificationOnTestDescription": "Evento para testar o sistema de notificação", "PlaceholderBulkChapterInput": "Digite o título de um capítulo ou use uma numeração (por exemplo, 'Episódio 1', 'Capítulo 10', '1.')", "PlaceholderNewCollection": "Novo nome da coleção", "PlaceholderNewFolderPath": "Novo caminho para a pasta", @@ -821,22 +965,47 @@ "PlaceholderSearchEpisode": "Buscar Episódio..", "StatsAuthorsAdded": "autores adicionados", "StatsBooksAdded": "livros adicionados", + "StatsBooksAdditional": "Algumas adições incluem…", "StatsBooksFinished": "livros concluídos", + "StatsBooksFinishedThisYear": "Alguns livros foram finalizados este ano…", + "StatsBooksListenedTo": "livros ouvidos", + "StatsCollectionGrewTo": "Sua coleção de livros cresceu para…", + "StatsSessions": "sessões", + "StatsSpentListening": "tempo gasto ouvindo", "StatsTopAuthor": "TOP AUTOR", "StatsTopAuthors": "TOP AUTORES", + "StatsTopGenre": "PRINCIPAL GÊNERO", + "StatsTopGenres": "PRINCIPAL GÊNEROS", + "StatsTopMonth": "PRINCIPAL MÊS", + "StatsTopNarrator": "PRINCIPAL NARRADOR", + "StatsTopNarrators": "PRINCIPAIS NARRADORES", + "StatsTotalDuration": "Com duração total de…", + "StatsYearInReview": "RESUMO DO ANO", "ToastAccountUpdateSuccess": "Conta atualizada", "ToastAppriseUrlRequired": "É preciso digitar uma URL Apprise", + "ToastAsinRequired": "É necessário o ASIN", "ToastAuthorImageRemoveSuccess": "Imagem do autor removida", + "ToastAuthorNotFound": "Autor \"{0}\" não encontrado", + "ToastAuthorRemoveSuccess": "Autor removido", + "ToastAuthorSearchNotFound": "Autor não encontrado", "ToastAuthorUpdateMerged": "Autor combinado", "ToastAuthorUpdateSuccess": "Autor atualizado", "ToastAuthorUpdateSuccessNoImageFound": "Autor atualizado (nenhuma imagem encontrada)", + "ToastBackupAppliedSuccess": "Backup aplicado", "ToastBackupCreateFailed": "Falha ao criar backup", "ToastBackupCreateSuccess": "Backup criado", "ToastBackupDeleteFailed": "Falha ao apagar backup", "ToastBackupDeleteSuccess": "Backup apagado", + "ToastBackupInvalidMaxKeep": "Número inválido de backups a serem mantidos", + "ToastBackupInvalidMaxSize": "Tamanho máximo de backup inválido", "ToastBackupRestoreFailed": "Falha ao restaurar backup", "ToastBackupUploadFailed": "Falha no upload do backup", "ToastBackupUploadSuccess": "Upload do backup realizado", + "ToastBatchApplyDetailsToItemsSuccess": "Detalhes aplicados aos itens", + "ToastBatchDeleteFailed": "A exclusão em lote falhou", + "ToastBatchDeleteSuccess": "Exclusão em lote concluída com sucesso", + "ToastBatchQuickMatchFailed": "A correspondência rápida em lote falhou!", + "ToastBatchQuickMatchStarted": "Correspondência rápida em lote de {0} livros iniciada!", "ToastBatchUpdateFailed": "Falha na atualização em lote", "ToastBatchUpdateSuccess": "Atualização em lote realizada", "ToastBookmarkCreateFailed": "Falha ao criar marcador", @@ -845,12 +1014,34 @@ "ToastBulkChapterInvalidCount": "Digite um número entre 1 e 150", "ToastCachePurgeFailed": "Falha ao apagar o cache", "ToastCachePurgeSuccess": "Cache apagado com sucesso", + "ToastChapterLocked": "Capítulo bloqueado.", + "ToastChapterStartTimeAdjusted": "O horário de início do capítulo foi ajustado em {0} segundos", + "ToastChaptersAllLocked": "Todos os capítulos estão bloqueados. Desbloqueie alguns capítulos para alterar seus horários.", "ToastChaptersHaveErrors": "Capítulos com erro", + "ToastChaptersInvalidShiftAmountLast": "Quantidade de deslocamento inválida. O horário de início do último capítulo ultrapassaria a duração deste audiolivro.", + "ToastChaptersInvalidShiftAmountStart": "Valor de deslocamento inválido. O primeiro capítulo teria duração zero ou negativa e seria sobrescrito pelo segundo capítulo. Aumente a duração inicial do segundo capítulo.", "ToastChaptersMustHaveTitles": "Capítulos precisam ter títulos", + "ToastChaptersRemoved": "Capítulos removidos", + "ToastChaptersUpdated": "Capítulos atualizados", + "ToastCollectionItemsAddFailed": "Falha ao adicionar itens à coleção", "ToastCollectionRemoveSuccess": "Coleção removida", "ToastCollectionUpdateSuccess": "Coleção atualizada", + "ToastConnectionNotAvailable": "Conexão indisponível. Tente novamente mais tarde", + "ToastCoverSearchFailed": "A busca pela capa falhou", + "ToastCoverUpdateFailed": "Falha na atualização da capa", + "ToastDateTimeInvalidOrIncomplete": "A data e a hora são inválidas ou estão incompletas", "ToastDeleteFileFailed": "Falha ao apagar arquivo", "ToastDeleteFileSuccess": "Arquivo apagado", + "ToastDeviceAddFailed": "Falha ao adicionar dispositivo", + "ToastDeviceNameAlreadyExists": "Já existe um leitor de ebook com esse nome.", + "ToastDeviceTestEmailFailed": "Falha ao enviar o e-mail de teste", + "ToastDeviceTestEmailSuccess": "E-mail de teste enviado", + "ToastEmailSettingsUpdateSuccess": "Configurações de e-mail atualizadas", + "ToastEncodeCancelFailed": "Falha ao cancelar a codificação", + "ToastEncodeCancelSucces": "Codificação cancelada", + "ToastEpisodeDownloadQueueClearFailed": "Falha ao limpar a fila", + "ToastEpisodeDownloadQueueClearSuccess": "Fila de downloads de episódios limpa", + "ToastEpisodeUpdateSuccess": "{0} episódios atualizados", "ToastFailedToLoadData": "Falha ao carregar dados", "ToastItemCoverUpdateSuccess": "Capa do item atualizada", "ToastItemDetailsUpdateSuccess": "Detalhes do item atualizados", From b83e2836f605d41712aa1ff4bf3bc969ca9accc5 Mon Sep 17 00:00:00 2001 From: Sami Tauseef Date: Sun, 2 Nov 2025 09:48:23 +0100 Subject: [PATCH 051/285] Translated using Weblate (Bengali) Currently translated at 92.6% (1078 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/ --- client/strings/bn.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/strings/bn.json b/client/strings/bn.json index 5ccab2711..75cf98ee5 100644 --- a/client/strings/bn.json +++ b/client/strings/bn.json @@ -1,5 +1,6 @@ { "ButtonAdd": "যোগ করুন", + "ButtonAddApiKey": "এপিআই কী যোগ করুন", "ButtonAddChapters": "অধ্যায় যোগ করুন", "ButtonAddDevice": "ডিভাইস যোগ করুন", "ButtonAddLibrary": "লাইব্রেরি যোগ করুন", @@ -10,6 +11,8 @@ "ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন", "ButtonAuthors": "লেখকগণ", "ButtonBack": "পেছনে যান", + "ButtonBatchEditPopulateFromExisting": "বিদ্যমান থেকে পূরণ করুন", + "ButtonBatchEditPopulateMapDetails": "ম্যাপ থেকে পূরণ করুন", "ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন", "ButtonCancel": "বাতিল করুন", "ButtonCancelEncode": "এনকোড বাতিল করুন", @@ -18,6 +21,7 @@ "ButtonChooseAFolder": "একটি ফোল্ডার চয়ন করুন", "ButtonChooseFiles": "ফাইল চয়ন করুন", "ButtonClearFilter": "ফিল্টার পরিষ্কার করুন", + "ButtonClose": "বন্ধ করুন", "ButtonCloseFeed": "ফিড বন্ধ করুন", "ButtonCloseSession": "খোলা সেশন বন্ধ করুন", "ButtonCollections": "সংগ্রহ", @@ -117,11 +121,13 @@ "HeaderAccount": "অ্যাকাউন্ট", "HeaderAddCustomMetadataProvider": "কাস্টম মেটাডেটা সরবরাহকারী যোগ করুন", "HeaderAdvanced": "অ্যাডভান্সড", + "HeaderApiKeys": "এপিআই কী সমূহ", "HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন", "HeaderAudioTracks": "অডিও ট্র্যাকসগুলো", "HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস", "HeaderAuthentication": "প্রমাণীকরণ", "HeaderBackups": "ব্যাকআপ", + "HeaderBulkChapterModal": "একাধিক অধ্যায় যোগ করুন", "HeaderChangePassword": "পাসওয়ার্ড পরিবর্তন করুন", "HeaderChapters": "অধ্যায়", "HeaderChooseAFolder": "একটি ফোল্ডার চয়ন করুন", @@ -160,6 +166,7 @@ "HeaderMetadataOrderOfPrecedence": "মেটাডেটা অগ্রাধিকারের ক্রম", "HeaderMetadataToEmbed": "এম্বেড করার জন্য মেটাডেটা", "HeaderNewAccount": "নতুন অ্যাকাউন্ট", + "HeaderNewApiKey": "নতুন API কী", "HeaderNewLibrary": "নতুন লাইব্রেরি", "HeaderNotificationCreate": "বিজ্ঞপ্তি তৈরি করুন", "HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন", @@ -175,6 +182,7 @@ "HeaderPlaylist": "প্লেলিস্ট", "HeaderPlaylistItems": "প্লেলিস্ট আইটেম", "HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট", + "HeaderPresets": "প্রিসেট", "HeaderPreviewCover": "কভার ্দেখুন", "HeaderRSSFeedGeneral": "আরএসএস বিবরণ", "HeaderRSSFeedIsOpen": "আরএসএস ফিড খোলা আছে", @@ -192,6 +200,7 @@ "HeaderSettingsExperimental": "পরীক্ষামূলক ফিচার", "HeaderSettingsGeneral": "সাধারণ", "HeaderSettingsScanner": "স্ক্যানার", + "HeaderSettingsSecurity": "নিরাপত্তা", "HeaderSettingsWebClient": "ওয়েব ক্লায়েন্ট", "HeaderSleepTimer": "স্লিপ টাইমার", "HeaderStatsLargestItems": "সবচেয়ে বড় আইটেম", From b0c073dd7ee0f0516c511785a0eb4b20b1d48851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Gon=C3=A7alves=20Jaksys?= Date: Mon, 3 Nov 2025 06:16:47 +0100 Subject: [PATCH 052/285] 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/ --- client/strings/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/hu.json b/client/strings/hu.json index 61e9316d9..e6d45231f 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -286,7 +286,7 @@ "LabelClickToUseCurrentValue": "Kattintson az aktuális érték használatához", "LabelClosePlayer": "Lejátszó bezárása", "LabelCodec": "Kodek", - "LabelCollapseSeries": "Sorozat összecsukása", + "LabelCollapseSeries": "Sorozatok összecsukása", "LabelCollapseSubSeries": "Alszéria összecsukása", "LabelCollection": "Gyűjtemény", "LabelCollections": "Gyűjtemények", From 9fdda6be628b48f996df3c8d8dfbf42b81f2d18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Gon=C3=A7alves=20Jaksys?= Date: Mon, 3 Nov 2025 06:15:22 +0100 Subject: [PATCH 053/285] Translated using Weblate (Portuguese (Brazil)) 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/pt_BR/ --- client/strings/pt-br.json | 94 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 5f207154e..48ce8b9e0 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -741,11 +741,11 @@ "MessageAuthenticationOIDCChangesRestart": "Reinicie o servidor após salvar para aplicar as alterações do OIDC.", "MessageAuthenticationSecurityMessage": "A autenticação foi aprimorada para maior segurança. Todos os usuários precisam fazer login novamente.", "MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em /metadata/items & /metadata/authors. Backups não incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.", - "MessageBackupsLocationEditNote": "Observação: Atualizar o local de backup não moverá nem modificará os backups existentes.", + "MessageBackupsLocationEditNote": "Observação: Atualizar o local de backup não moverá nem modificará os backups existentes", "MessageBackupsLocationNoEditNote": "Observação: o local de backup é definido por meio de uma variável de ambiente e não pode ser alterado aqui.", - "MessageBackupsLocationPathEmpty": "O caminho do local de backup não pode ser vazio.", - "MessageBatchEditPopulateMapDetailsAllHelp": "Preencha os campos habilitados com dados de todos os itens. Os campos com múltiplos valores serão mesclados.", - "MessageBatchEditPopulateMapDetailsItemHelp": "Preencha os campos de detalhes do mapa habilitados com dados deste item.", + "MessageBackupsLocationPathEmpty": "O caminho do local de backup não pode ser vazio", + "MessageBatchEditPopulateMapDetailsAllHelp": "Preencha os campos habilitados com dados de todos os itens. Os campos com múltiplos valores serão mesclados", + "MessageBatchEditPopulateMapDetailsItemHelp": "Preencha os campos de detalhes do mapa habilitados com dados deste item", "MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.", "MessageBookshelfNoCollections": "Você ainda não criou coleções", "MessageBookshelfNoCollectionsHelp": "As coleções são públicas. Todos os usuários com acesso à biblioteca podem vê-las.", @@ -925,7 +925,7 @@ "MessageTaskOpmlImportFeedPodcastFailed": "Falha ao criar o podcast", "MessageTaskOpmlImportFinished": "Adicionados {0} podcasts", "MessageTaskOpmlParseFailed": "Falha ao analisar o arquivo OPML", - "MessageTaskOpmlParseFastFail": "Arquivo OPML inválido. Tag não encontrada OU tag não encontrada.", + "MessageTaskOpmlParseFastFail": "Arquivo OPML inválido. Tag não encontrada OU tag não encontrada", "MessageTaskOpmlParseNoneFound": "Nenhum feed encontrado no arquivo OPML", "MessageTaskScanItemsAdded": "{0} adicionado", "MessageTaskScanItemsMissing": "{0} não encontrado", @@ -1033,7 +1033,7 @@ "ToastDeleteFileFailed": "Falha ao apagar arquivo", "ToastDeleteFileSuccess": "Arquivo apagado", "ToastDeviceAddFailed": "Falha ao adicionar dispositivo", - "ToastDeviceNameAlreadyExists": "Já existe um leitor de ebook com esse nome.", + "ToastDeviceNameAlreadyExists": "Já existe um leitor de ebook com esse nome", "ToastDeviceTestEmailFailed": "Falha ao enviar o e-mail de teste", "ToastDeviceTestEmailSuccess": "E-mail de teste enviado", "ToastEmailSettingsUpdateSuccess": "Configurações de e-mail atualizadas", @@ -1042,13 +1042,26 @@ "ToastEpisodeDownloadQueueClearFailed": "Falha ao limpar a fila", "ToastEpisodeDownloadQueueClearSuccess": "Fila de downloads de episódios limpa", "ToastEpisodeUpdateSuccess": "{0} episódios atualizados", + "ToastErrorCannotShare": "Não é possível compartilhar nativamente neste dispositivo", + "ToastFailedToCreate": "Falha ao criar", + "ToastFailedToDelete": "Falha ao excluir", "ToastFailedToLoadData": "Falha ao carregar dados", + "ToastFailedToMatch": "Não foi possível encontrar correspondência", + "ToastFailedToShare": "Não foi possível compartilhar", + "ToastFailedToUpdate": "Falha ao atualizar", + "ToastInvalidImageUrl": "URL de imagem inválida", + "ToastInvalidMaxEpisodesToDownload": "Número máximo de episódios para baixar inválido", + "ToastInvalidUrl": "URL inválida", + "ToastInvalidUrls": "Um ou mais URLs são inválidos", "ToastItemCoverUpdateSuccess": "Capa do item atualizada", + "ToastItemDeletedFailed": "Falha ao excluir o item", + "ToastItemDeletedSuccess": "Item excluído", "ToastItemDetailsUpdateSuccess": "Detalhes do item atualizados", "ToastItemMarkedAsFinishedFailed": "Falha ao marcar como Concluído", "ToastItemMarkedAsFinishedSuccess": "Item marcado como Concluído", "ToastItemMarkedAsNotFinishedFailed": "Falha ao marcar como Não Concluído", "ToastItemMarkedAsNotFinishedSuccess": "Item marcado como Não Concluído", + "ToastItemUpdateSuccess": "Item atualizado", "ToastLibraryCreateFailed": "Falha ao criar biblioteca", "ToastLibraryCreateSuccess": "Biblioteca \"{0}\" criada", "ToastLibraryDeleteFailed": "Falha ao apagar biblioteca", @@ -1056,30 +1069,97 @@ "ToastLibraryScanFailedToStart": "Falha ao iniciar verificação", "ToastLibraryScanStarted": "Verificação da biblioteca iniciada", "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada", + "ToastMatchAllAuthorsFailed": "Não foi possível encontrar correspondência para todos os autores", + "ToastMetadataFilesRemovedError": "Erro ao remover arquivos de metadados.{0}", + "ToastMetadataFilesRemovedNoneFound": "Nenhum arquivo metadados.{0} encontrado na biblioteca", + "ToastMetadataFilesRemovedNoneRemoved": "Nenhum arquivo metadados.{0} removido", + "ToastMetadataFilesRemovedSuccess": "{0} arquivos metadados.{1} removidos", + "ToastMustHaveAtLeastOnePath": "Deve ter pelo menos um caminho", + "ToastNameEmailRequired": "Nome e e-mail são obrigatórios", + "ToastNameRequired": "O nome é obrigatório", + "ToastNewApiKeyUserError": "É necessário selecionar um usuário", + "ToastNewEpisodesFound": "{0} novos episódios encontrados", + "ToastNewUserCreatedFailed": "Falha ao criar a conta: \"{0}\"", + "ToastNewUserCreatedSuccess": "Nova conta criada", + "ToastNewUserLibraryError": "É necessário selecionar pelo menos uma biblioteca", + "ToastNewUserPasswordError": "É obrigatória a criação de uma senha; somente o usuário root pode ter uma senha vazia", + "ToastNewUserTagError": "É necessário selecionar pelo menos uma etiqueta", "ToastNewUserUsernameError": "Digite um nome de usuário", + "ToastNoNewEpisodesFound": "Nenhum episódio novo encontrado", + "ToastNoRSSFeed": "O Podcast não possui um feed RSS", + "ToastNoUpdatesNecessary": "Nenhuma atualização necessária", + "ToastNotificationCreateFailed": "Falha ao criar notificação", + "ToastNotificationDeleteFailed": "Falha ao excluir a notificação", + "ToastNotificationFailedMaximum": "O número máximo de tentativas falhas deve ser >= 0", + "ToastNotificationQueueMaximum": "A fila máxima de notificações deve ser >= 0", + "ToastNotificationSettingsUpdateSuccess": "Configurações de notificação atualizadas", + "ToastNotificationTestTriggerFailed": "Falha ao acionar a notificação de teste", + "ToastNotificationTestTriggerSuccess": "Notificação de teste acionada", + "ToastNotificationUpdateSuccess": "Notificação atualizada", "ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução", "ToastPlaylistCreateSuccess": "Lista de reprodução criada", "ToastPlaylistRemoveSuccess": "Lista de reprodução removida", "ToastPlaylistUpdateSuccess": "Lista de reprodução atualizada", "ToastPodcastCreateFailed": "Falha ao criar podcast", "ToastPodcastCreateSuccess": "Podcast criado", + "ToastPodcastEpisodeUpdated": "Episódio atualizado", + "ToastPodcastGetFeedFailed": "Não foi possível obter o feed do podcast", + "ToastPodcastNoEpisodesInFeed": "Nenhum episódio encontrado no feed RSS", + "ToastPodcastNoRssFeed": "O podcast não possui um feed RSS", + "ToastProgressIsNotBeingSynced": "O progresso não está sendo sincronizado, reinicie a reprodução", + "ToastProviderCreatedFailed": "Falha ao adicionar o provedor", + "ToastProviderCreatedSuccess": "Novo provedor adicionado", + "ToastProviderNameAndUrlRequired": "Nome e URL obrigatórios", + "ToastProviderRemoveSuccess": "Fornecedor removido", "ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS", "ToastRSSFeedCloseSuccess": "Feed RSS fechado", + "ToastRemoveFailed": "Falha ao remover", "ToastRemoveItemFromCollectionFailed": "Falha ao remover item da coleção", "ToastRemoveItemFromCollectionSuccess": "Item removido da coleção", + "ToastRemoveItemsWithIssuesFailed": "Falha ao remover itens da biblioteca com problemas", + "ToastRemoveItemsWithIssuesSuccess": "Itens da biblioteca com problemas foram removidos", + "ToastRenameFailed": "Falha ao renomear", + "ToastRescanFailed": "Falha na nova verificação para {0}", + "ToastRescanRemoved": "O item foi removido após a digitalização ser concluída", + "ToastRescanUpToDate": "A nova verificação completou o item, que estava atualizado", + "ToastRescanUpdated": "O item foi atualizado após a digitalização", + "ToastScanFailed": "Falha ao digitalizar o item da biblioteca", + "ToastSelectAtLeastOneUser": "Selecione pelo menos um usuário", "ToastSendEbookToDeviceFailed": "Falha ao enviar ebook para dispositivo", "ToastSendEbookToDeviceSuccess": "Ebook enviado para o dispositivo \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Não é possível adicionar duas séries com o mesmo nome", "ToastSeriesUpdateFailed": "Falha ao atualizar série", "ToastSeriesUpdateSuccess": "Série atualizada", "ToastServerSettingsUpdateSuccess": "Configurações do servidor atualizadas", + "ToastSessionCloseFailed": "Falha ao encerrar a sessão", "ToastSessionDeleteFailed": "Falha ao apagar sessão", "ToastSessionDeleteSuccess": "Sessão apagada", + "ToastSleepTimerDone": "Temporizador de sono concluído... zZzzZz", + "ToastSlugMustChange": "O slug possui caracteres inválidos", + "ToastSlugRequired": "Slug é necessário", "ToastSocketConnected": "Socket conectado", "ToastSocketDisconnected": "Socket desconectado", "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastSortingPrefixesEmptyError": "É preciso ter pelo menos um prefixo de ordenação", "ToastSortingPrefixesUpdateSuccess": "Prefixos de ordenação atualizados ({0} item(ns))", + "ToastTitleRequired": "É necessário ter um título", + "ToastUnknownError": "Erro desconhecido", + "ToastUnlinkOpenIdFailed": "Falha ao desvincular o usuário do OpenID", + "ToastUnlinkOpenIdSuccess": "Usuário desvinculado do OpenID", + "ToastUploaderFilepathExistsError": "O caminho de arquivo \"{0}\" já existe no servidor", + "ToastUploaderItemExistsInSubdirectoryError": "O item \"{0}\" está usando um subdiretório do caminho de upload.", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado", - "ToastUserRootRequireName": "É preciso entrar com um nome de usuário root" + "ToastUserPasswordChangeSuccess": "Senha alterada com sucesso", + "ToastUserPasswordMismatch": "As senhas não coincidem", + "ToastUserPasswordMustChange": "A nova senha não pode ser igual à senha antiga", + "ToastUserRootRequireName": "É preciso entrar com um nome de usuário root", + "TooltipAddChapters": "Adicionar capítulo(s)", + "TooltipAddOneSecond": "Adicionar 1 segundo", + "TooltipAdjustChapterStart": "Clique para ajustar a hora de início", + "TooltipLockAllChapters": "Bloquear todos os capítulos", + "TooltipLockChapter": "Bloquear capítulo (Shift+clique para selecionar o intervalo)", + "TooltipSubtractOneSecond": "Subtrair 1 segundo", + "TooltipUnlockAllChapters": "Desbloqueie todos os capítulos", + "TooltipUnlockChapter": "Desbloquear capítulo (Shift + clique para selecionar o intervalo)" } From a0137fcb428b285fd1ea1c3b81a24fdca2f5d386 Mon Sep 17 00:00:00 2001 From: phewi Date: Mon, 3 Nov 2025 20:43:04 +0100 Subject: [PATCH 054/285] Translated using Weblate (Finnish) Currently translated at 95.5% (1111 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index 6e2999bd5..4b43c9ec4 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -535,7 +535,7 @@ "LabelReadAgain": "Lue uudelleen", "LabelReadEbookWithoutProgress": "Lue s-kirja tallentamatta edistymistietoja", "LabelRecentSeries": "Viimeisimmät sarjat", - "LabelRecentlyAdded": "Viimeeksi lisätyt", + "LabelRecentlyAdded": "Viimeksi lisätyt", "LabelRecommended": "Suositeltu", "LabelRedo": "Tee uudelleen", "LabelRegion": "Alue", From 40783c8644e3a60c91c8de86e04946d962d4e34f Mon Sep 17 00:00:00 2001 From: Charlie Date: Thu, 6 Nov 2025 03:47:22 +0100 Subject: [PATCH 055/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index e90cd73a2..b560bfc06 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -744,7 +744,7 @@ "MessageBackupsLocationEditNote": "Remarque : Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes", "MessageBackupsLocationNoEditNote": "Remarque : l’emplacement de sauvegarde est défini via une variable d’environnement et ne peut pas être modifié ici.", "MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide", - "MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. Les champs avec des valeurs multiples seront fusionnés.", + "MessageBatchEditPopulateMapDetailsAllHelp": "Renseignez les champs activés avec les données de tous les éléments. Les champs comportant plusieurs valeurs seront fusionnés.", "MessageBatchEditPopulateMapDetailsItemHelp": "Renseigner les champs de la carte active avec les informations de cet élément", "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.", "MessageBookshelfNoCollections": "Vous n’avez pas encore de collections", @@ -1026,7 +1026,7 @@ "ToastCollectionItemsAddFailed": "Échec de l’ajout de(s) élément(s) à la collection", "ToastCollectionRemoveSuccess": "Collection supprimée", "ToastCollectionUpdateSuccess": "Collection mise à jour", - "ToastConnectionNotAvailable": "Connexion indisponible. Essayez plus tard", + "ToastConnectionNotAvailable": "Connexion indisponible. Veuillez réessayer plus tard.", "ToastCoverSearchFailed": "La recherche de la couverture a échoué", "ToastCoverUpdateFailed": "Échec de la mise à jour de la couverture", "ToastDateTimeInvalidOrIncomplete": "La date et l'heure sont invalides ou incomplètes", From ab9b798bfa7191a4457d09d50b7f36b1b8aa935d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Sat, 8 Nov 2025 08:22:15 +0100 Subject: [PATCH 056/285] Translated using Weblate (Finnish) 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/fi/ --- client/strings/fi.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/strings/fi.json b/client/strings/fi.json index 4b43c9ec4..421d5df43 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -245,6 +245,7 @@ "LabelApiKeyCreated": "API avain \"{0}\" luotu onnistuneesti.", "LabelApiKeyCreatedDescription": "Varmista, että kopioit API avaimen. Sitä ei näytetä enää tämän jälkeen.", "LabelApiKeyUser": "Toimi käyttäjän puolesta", + "LabelApiKeyUserDescription": "Tällä API-avaimella on samat käyttöoikeudet kuin käyttäjällä, jonka puolesta se toimii. Tämä näkyy lokeissa samalla tavalla kuin jos käyttäjä itse tekisi pyynnön.", "LabelApiToken": "Sovellusliittymätunnus", "LabelAppend": "Lisää loppuun", "LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)", @@ -308,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "Poista tiedostojärjestelmästä (poista merkintä, jos haluat poistaa vain tietokannasta)", "LabelDescription": "Kuvaus", "LabelDeselectAll": "Poista valinta kaikista", + "LabelDetectedPattern": "Tunnista malli:", "LabelDevice": "Laite", "LabelDeviceInfo": "Laitteen tiedot", "LabelDeviceIsAvailableTo": "Laite on saatavilla...", @@ -357,6 +359,10 @@ "LabelExample": "Esimerkki", "LabelExpandSeries": "Laajenna sarja", "LabelExpandSubSeries": "Laajenna alisarja", + "LabelExpired": "Vanhentunut", + "LabelExpiresAt": "Vanhentuu", + "LabelExpiresInSeconds": "Vanhentuu (sekunnissa)", + "LabelExpiresNever": "Ei koskaan", "LabelExplicit": "Sopimaton", "LabelExplicitChecked": "Yksiselitteinen (valittu)", "LabelExplicitUnchecked": "Ei yksiselitteinen (ei valittu)", @@ -372,6 +378,7 @@ "LabelFilterByUser": "Suodata käyttäjien perusteella", "LabelFindEpisodes": "Etsi jaksoja", "LabelFinished": "Valmis", + "LabelFinishedDate": "Valmis {0}", "LabelFolder": "Kansio", "LabelFolders": "Kansiot", "LabelFontBold": "Lihavoitu", @@ -416,6 +423,7 @@ "LabelLanguages": "Kielet", "LabelLastBookAdded": "Viimeisin lisätty kirja", "LabelLastBookUpdated": "Viimeisin päivitetty kirja", + "LabelLastProgressDate": "Viimeisin edistyminen {0}", "LabelLastSeen": "Nähty viimeksi", "LabelLastTime": "Viimeinen kerta", "LabelLastUpdate": "Viimeisin päivitys", @@ -428,6 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ei {0}", "LabelLibraryItem": "Kirjaston kohde", "LabelLibraryName": "Kirjaston nimi", + "LabelLibrarySortByProgress": "Edistyminen: Viimeksi päivitetty", + "LabelLibrarySortByProgressFinished": "Edistyminen: Valmis", + "LabelLibrarySortByProgressStarted": "Edistyminen: Aloitettu", "LabelLimit": "Raja", "LabelLineSpacing": "Riviväli", "LabelListenAgain": "Kuuntele uudelleen", @@ -436,6 +447,7 @@ "LabelLogLevelWarn": "Varoitus", "LabelLookForNewEpisodesAfterDate": "Etsi uusia jaksoja tämän päivämäärän jälkeen", "LabelLowestPriority": "Vähiten tärkeä", + "LabelMatchConfidence": "Varmuus", "LabelMatchExistingUsersBy": "Vastaa olemassa olevia käyttäjiä mukaan", "LabelMatchExistingUsersByDescription": "Käytetään olemassa olevien käyttäjien yhdistämiseen. Kun yhteys on muodostettu, käyttäjät saavat yksilöllisen tunnuksen SSO-palveluntarjoajaltasi", "LabelMaxEpisodesToDownload": "Jaksojen maksimilatausmäärä. 0 poistaa rajoituksen.", @@ -465,7 +477,9 @@ "LabelNewestAuthors": "Uusimmat tekijät", "LabelNewestEpisodes": "Uusimmat jaksot", "LabelNextBackupDate": "Seuraava varmuuskopiointipäivämäärä", + "LabelNextChapters": "Seuraavat luvut:", "LabelNextScheduledRun": "Seuraava ajastettu suorittaminen", + "LabelNoApiKeys": "Ei API-avaimia", "LabelNoCustomMetadataProviders": "Ei mukautettuja kuvailutietojen toimittajia", "LabelNoEpisodesSelected": "Jaksoja ei ole valittu", "LabelNotFinished": "Ei valmis", @@ -481,6 +495,7 @@ "LabelNotificationsMaxQueueSize": "Ilmoitustapahtumajonon enimmäispituus", "LabelNotificationsMaxQueueSizeHelp": "Tapahtumat on rajoitettu ampumaan yksi sekunnissa. Tapahtumat ohitetaan, jos jono on enimmäiskoko. Tämä estää ilmoitusten roskapostin.", "LabelNumberOfBooks": "Kirjojen määrä", + "LabelNumberOfChapters": "Lukujen lukumäärä:", "LabelNumberOfEpisodes": "# jaksoja", "LabelOpenIDAdvancedPermsClaimDescription": "OpenID-vaatimuksen nimi, joka sisältää lisäoikeudet sovelluksen käyttäjän toimiin, joita sovelletaan muihin kuin järjestelmänvalvojan rooleihin (jos määritetty). Jos vaatimus puuttuu vastauksesta, pääsy ABS:iin evätään. Jos yksittäinen vaihtoehto puuttuu, sitä käsitellään false-arvona. Varmista, että identiteetin tarjoajan vaatimus vastaa odotettua rakennetta:", "LabelOpenIDClaims": "Jätä seuraavat vaihtoehdot tyhjiksi, jos haluat poistaa edistyneen ryhmän ja lupien määrityksen käytöstä ja määrittää sitten automaattisesti käyttäjäryhmän.", @@ -555,6 +570,7 @@ "LabelSelectAll": "Valitse kaikki", "LabelSelectAllEpisodes": "Valitse kaikki jaksot", "LabelSelectEpisodesShowing": "Valitse {0} näytettävää jaksoa", + "LabelSelectUser": "Valitse käyttäjä", "LabelSelectUsers": "Valitse käyttäjät", "LabelSendEbookToDevice": "Lähetä s-kirja kohteeseen...", "LabelSequence": "Sekvenssi", @@ -622,6 +638,7 @@ "LabelStartTime": "Aloitusaika", "LabelStarted": "Aloitettu", "LabelStartedAt": "Aloitettu", + "LabelStartedDate": "Aloitettu {0}", "LabelStatsAudioTracks": "Ääniraidat", "LabelStatsAuthors": "Tekijät", "LabelStatsBestDay": "Paras päivä", @@ -651,6 +668,7 @@ "LabelTheme": "Teema", "LabelThemeDark": "Tumma", "LabelThemeLight": "Kirkas", + "LabelThemeSepia": "Seepia", "LabelTimeBase": "Aika-alusta", "LabelTimeDurationXHours": "{0} tuntia", "LabelTimeDurationXMinutes": "{0} minuuttia", @@ -718,6 +736,8 @@ "LabelYourProgress": "Edistymisesi", "MessageAddToPlayerQueue": "Lisää soittimen jonoon", "MessageAppriseDescription": "Käyttääksesi tätä toimintoa tarvitset Apprise API -instanssin tai rajapinnan joka käsittelee samoja pyyntöjä.
Apprise rajapinnan osoite tulee olla täysi URL polku ilmoituksen lähetykseen, esim. jos rajapinta on osoitteessa http://192.168.1.1:8337,niin arvoksi tulee antaa http://192.168.1.1:8337/notify.", + "MessageAsinCheck": "Varmista, että käytät ASIN-tunnusta oikealta Audible-alueelta, ei Amazonista.", + "MessageAuthenticationLegacyTokenWarning": "Vanhat API-tunnukset poistetaan tulevaisuudessa. Käytä sen sijaan API-avaimia.", "MessageBackupsDescription": "Varmuuskopiot sisältävät käyttäjät, käyttäjien edistymisen, kirjastokohteiden tiedot, palvelinasetukset ja /metadata/items- ja /metadata/authors -kansioihin tallennetut kuvat. Varmuuskopiot eivät sisällä kirjastosi kansioihin tallennettuja tiedostoja.", "MessageBackupsLocationEditNote": "Huomautus: Varmuuskopion sijainnin päivittäminen ei siirrä tai muokkaa olemassa olevia varmuuskopioita", "MessageBackupsLocationNoEditNote": "Huomautus: Varmuuskopion sijainti asetetaan ympäristömuuttujan kautta, eikä sitä voi muuttaa tässä.", From c75d976320df532129ef19efa7ce0a581dee822a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dinu=20B=C4=83lan?= Date: Fri, 7 Nov 2025 17:33:11 +0100 Subject: [PATCH 057/285] Translated using Weblate (Romanian) Currently translated at 17.3% (202 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/ --- client/strings/ro.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/strings/ro.json b/client/strings/ro.json index c6628bd7d..f6e8977ef 100644 --- a/client/strings/ro.json +++ b/client/strings/ro.json @@ -4,7 +4,11 @@ "ButtonAddChapters": "Adaugă Capitole", "ButtonAddDevice": "Adaugă Dispozitiv", "ButtonAddLibrary": "Adaugă Librărie", + "ButtonAddPodcasts": "Adaugă Podcasturi", "ButtonAddUser": "Adaugă Utilizator", + "ButtonAddYourFirstLibrary": "Adaugă prima librărie", + "ButtonApply": "Aplică", + "ButtonApplyChapters": "Aplică Capitole", "ButtonAuthors": "Autori", "ButtonBack": "Înapoi", "ButtonCancel": "Anulează", From a05cb170a2bc13fa65d8916dbf88f4b9c7d4abcc Mon Sep 17 00:00:00 2001 From: biuklija Date: Tue, 11 Nov 2025 06:46:16 +0100 Subject: [PATCH 058/285] Translated using Weblate (Croatian) Currently translated at 99.9% (1162 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/ --- client/strings/hr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/hr.json b/client/strings/hr.json index c5cbf66e1..42987527c 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeumorfni dizajn sa drvenim policama", "LabelSettingsChromecastSupport": "Podrška za Chromecast", "LabelSettingsDateFormat": "Format datuma", - "LabelSettingsEnableWatcher": "Automatski pretražuj ima li promjena u knjižnicama", - "LabelSettingsEnableWatcherForLibrary": "Automatski traži promjene u knjižnicama", + "LabelSettingsEnableWatcher": "Automatski prati ima li promjena u knjižnicama", + "LabelSettingsEnableWatcherForLibrary": "Automatski prati promjene u knjižnicama", "LabelSettingsEnableWatcherHelp": "Omogućuje automatsko dodavanje/ažuriranje stavki kada se uoče izmjene datoteka. *Potrebno je ponovno pokretanje poslužitelja", "LabelSettingsEpubsAllowScriptedContent": "Omogući skripte u epub datotekama", "LabelSettingsEpubsAllowScriptedContentHelp": "Omogućuje epub datotekama izvođenje skripti. Preporučamo isključiti ovu mogućnost ukoliko nemate povjerenja u izvore epub datoteka.", From 9eda4e36fa3b699e5ae594674729ff23b752b2c1 Mon Sep 17 00:00:00 2001 From: Lucas Jaksys Date: Tue, 11 Nov 2025 23:51:52 +0100 Subject: [PATCH 059/285] Translated using Weblate (Portuguese (Brazil)) 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/pt_BR/ --- client/strings/pt-br.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 48ce8b9e0..9c8754c44 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Tem certeza de que deseja resetar os capítulos e desfazer as alterações realizadas?", "MessageRestoreBackupConfirm": "Tem certeza de que deseja restaurar o backup criado em", "MessageRestoreBackupWarning": "Restaurar um backup sobrescreverá totalmente o banco de dados localizado em /config e as imagens de capa em /metadata/items & /metadata/authors.

Backups não alteram quaisquer arquivos nas pastas da sua biblioteca. Se a configuração do servidor de armazenar a arte da capa e os metadados nas pastas da sua biblioteca estiver ativa, esses itens não estão no backup e não serão sobrescritos.

Todos os clientes usando o seu servidor serão atualizados automaticamente.", - "MessageScheduleLibraryScanNote": "Para a maioria dos usuários, é recomendável deixar este recurso desativado e manter a configuração de monitoramento de pastas ativada. O monitor de pastas detectará automaticamente as alterações nas pastas da sua biblioteca. O monitor de pastas não funciona em todos os sistemas de arquivos (como o NFS), portanto, podem ser usadas verificações agendadas da biblioteca como alternativa.", + "MessageScheduleLibraryScanNote": "Para a maioria dos usuários, é recomendável deixar este recurso desativado e manter a configuração \"Monitorar automaticamente alterações na biblioteca\" ativada. Dessa forma alterações nas pastas da sua biblioteca serão automáticamente detectadas. Habilite essa opção se \"Monitorar automaticamente alterações na biblioteca\" não fucnionara para o seu sistema de arquivos (Como NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Executar a cada {0} às {1}", "MessageSearchResultsFor": "Resultado da busca por", "MessageSelected": "{0} selecionado(s)", From 61b72aff9d73d9c8a22228cbb05177e4060b1a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=93=D0=BE=D1=80?= =?UTF-8?q?=D0=BF=D0=B8=D0=BD=D1=96=D1=87?= Date: Tue, 11 Nov 2025 20:23:03 +0100 Subject: [PATCH 060/285] 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/ --- client/strings/uk.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/uk.json b/client/strings/uk.json index fbb736f6d..32546fe03 100644 --- a/client/strings/uk.json +++ b/client/strings/uk.json @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Імітує вигляд дерев'яних полиць", "LabelSettingsChromecastSupport": "Підтримка Chromecast", "LabelSettingsDateFormat": "Формат дати", - "LabelSettingsEnableWatcher": "Автоматично сканувати бібліотеки на наявність змін", - "LabelSettingsEnableWatcherForLibrary": "Автоматично сканувати бібліотеку на наявність змін", + "LabelSettingsEnableWatcher": "Автоматично відстежувати зміни в бібліотеках", + "LabelSettingsEnableWatcherForLibrary": "Автоматично відстежувати зміни в бібліотеці", "LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера", "LabelSettingsEpubsAllowScriptedContent": "Дозволити JavaScript-вміст у epub", "LabelSettingsEpubsAllowScriptedContentHelp": "Дозволяти epub-файлам виконувати код. Вмикайте цей параметр лише якщо ви довіряєте джерелу epub-файлів.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Ви впевнені, що хочете скинути глави та скасувати внесені зміни?", "MessageRestoreBackupConfirm": "Ви впевнені, що хочете відновити резервну копію, створену", "MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних у /config і зображення обкладинок у /metadata/items та /metadata/authors.

Резервні копії не змінюють файли у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються.

Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.", - "MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.", + "MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується відключити цю функцію та підтримувати налаштування \"Автоматично переглядати бібліотеку для змін\" - вона автоматично виявить зміни в ваших папках бібліотеки. Звісніть цю функцію, якщо для вашої файлової системи (наприклад, NFS) не працює \"Автоматично переглядати бібліотеку.", "MessageScheduleRunEveryWeekdayAtTime": "Запуск кожні {0} о {1}", "MessageSearchResultsFor": "Результати пошуку для", "MessageSelected": "Вибрано: {0}", From 38dd1beff7298b7933101db945279a972f7025f4 Mon Sep 17 00:00:00 2001 From: FiendFEARing Date: Tue, 11 Nov 2025 02:44:46 +0100 Subject: [PATCH 061/285] 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/ --- client/strings/zh-cn.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 361a74613..b7e4eac30 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计", "LabelSettingsChromecastSupport": "Chromecast 支持", "LabelSettingsDateFormat": "日期格式", - "LabelSettingsEnableWatcher": "自动扫描库以查找更改", - "LabelSettingsEnableWatcherForLibrary": "自动扫描库以查找更改", + "LabelSettingsEnableWatcher": "自动检测媒体库变化", + "LabelSettingsEnableWatcherForLibrary": "自动检测媒体库变化", "LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器", "LabelSettingsEpubsAllowScriptedContent": "允许 epubs 中包含脚本内容", "LabelSettingsEpubsAllowScriptedContentHelp": "允许 epub 文件执行脚本. 建议将此设置保持禁用, 除非你信任 epub 文件的来源.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "你确定要重置章节并撤消你所做的更改吗?", "MessageRestoreBackupConfirm": "你确定要恢复创建的这个备份", "MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.

备份不会修改媒体库文件夹中的任何文件. 如果你已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.

将自动刷新使用服务器的所有客户端.", - "MessageScheduleLibraryScanNote": "对于大多数用户, 建议禁用此功能并保持文件夹监视程序设置启用. 文件夹监视程序将自动检测库文件夹中的更改. 文件夹监视程序不适用于每个文件系统 (如 NFS), 因此可以使用计划库扫描.", + "MessageScheduleLibraryScanNote": "对于大多数用户, 建议保持此功能关闭并启用 \"自动检测媒体库变化\" 选项, 它会自动检测媒体库文件夹中的更改. 如果 \"自动检测媒体库变化\" 在你的文件系统 (例如 NFS) 上无法正常工作, 请启用此功能.", "MessageScheduleRunEveryWeekdayAtTime": "每 {0} 的 {1} 执行", "MessageSearchResultsFor": "搜索结果", "MessageSelected": "{0} 已选择", From cd295c03cafe02b5f959a0d09512d63fe2ecd9c4 Mon Sep 17 00:00:00 2001 From: thehijacker Date: Tue, 11 Nov 2025 06:08:49 +0100 Subject: [PATCH 062/285] Translated using Weblate (Slovenian) 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/sl/ --- client/strings/sl.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/strings/sl.json b/client/strings/sl.json index adcccb80c..962ccaa80 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami", "LabelSettingsChromecastSupport": "Podpora za Chromecast", "LabelSettingsDateFormat": "Oblika datuma", - "LabelSettingsEnableWatcher": "Samodejno preglej knjižnice za spremembe", - "LabelSettingsEnableWatcherForLibrary": "Samodejno preglej knjižnico za spremembe", + "LabelSettingsEnableWatcher": "Samodejno preišči knjižnice za spremembe", + "LabelSettingsEnableWatcherForLibrary": "Samodejno preišči knjižnico za spremembe", "LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika", "LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih", "LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Ali ste prepričani, da želite ponastaviti poglavja in razveljaviti spremembe, ki ste jih naredili?", "MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob", "MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.

Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.

Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.", - "MessageScheduleLibraryScanNote": "Za večino uporabnikov je priporočljivo, da to funkcijo pustite onemogočeno in ohranite nastavitev pregledovalnika map omogočeno. Pregledovalnik map bo samodejno zaznal spremembe v mapah vaše knjižnice. Pregledovalnik map ne deluje za vse datotečne sisteme (na primer NFS), zato lahko namesto tega uporabite načrtovane preglede knjižnic.", + "MessageScheduleLibraryScanNote": "Za večino uporabnikov je priporočljivo, da to funkcijo pustijo onemogočeno in ohranijo nastavitev »Samodejno spremljaj knjižnico za spremembe« omogočeno – samodejno bo zaznala spremembe v mapah vaše knjižnice. Omogočite to funkcijo, če »Samodejno spremljaj knjižnico za spremembe« ne deluje za vaš datotečni sistem (kot je NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Zaženi vsakih {0} ob {1}", "MessageSearchResultsFor": "Rezultati iskanja za", "MessageSelected": "{0} izbrano", @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Dodajanje elementov v zbirko ni uspelo", "ToastCollectionRemoveSuccess": "Zbirka je bila odstranjena", "ToastCollectionUpdateSuccess": "Zbirka je bila posodobljena", + "ToastConnectionNotAvailable": "Povezava ni na voljo. Poskusite znova pozneje", + "ToastCoverSearchFailed": "Iskanje naslovnice ni uspelo", "ToastCoverUpdateFailed": "Posodobitev naslovnice ni uspela", "ToastDateTimeInvalidOrIncomplete": "Datum in čas sta neveljavna ali nepopolna", "ToastDeleteFileFailed": "Brisanje datoteke ni uspelo", From d460757df42b006d4b608848ec854ea09c63bd4a Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Thu, 13 Nov 2025 16:45:10 +0100 Subject: [PATCH 063/285] 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/ --- client/strings/hr.json | 230 ++++++++++++++++++++--------------------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/client/strings/hr.json b/client/strings/hr.json index 42987527c..f22feb656 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -11,8 +11,8 @@ "ButtonApplyChapters": "Primijeni poglavlja", "ButtonAuthors": "Autori", "ButtonBack": "Natrag", - "ButtonBatchEditPopulateFromExisting": "Popuni iz postojećeg", - "ButtonBatchEditPopulateMapDetails": "Popuni mapirane pojedinosti", + "ButtonBatchEditPopulateFromExisting": "Popuni iz postojećih", + "ButtonBatchEditPopulateMapDetails": "Popuni detalje karte", "ButtonBrowseForFolder": "Pronađi mapu", "ButtonCancel": "Odustani", "ButtonCancelEncode": "Otkaži kodiranje", @@ -83,7 +83,7 @@ "ButtonRemoveAllLibraryItems": "Ukloni sve stavke iz knjižnice", "ButtonRemoveFromContinueListening": "Ukloni iz Nastavi slušati", "ButtonRemoveFromContinueReading": "Ukloni iz Nastavi čitati", - "ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju", + "ButtonRemoveSeriesFromContinueSeries": "Ukloni serijal iz popisa „Nastavi serijal”", "ButtonReset": "Poništi", "ButtonResetToDefault": "Vrati na početne postavke", "ButtonRestore": "Vraćanje", @@ -107,17 +107,17 @@ "ButtonSubmit": "Pošalji", "ButtonTest": "Test", "ButtonUnlinkOpenId": "Prekini vezu s OpenID-jem", - "ButtonUpload": "Učitaj", - "ButtonUploadBackup": "Učitaj sigurnosnu kopiju", - "ButtonUploadCover": "Učitaj naslovnicu", - "ButtonUploadOPMLFile": "Učitaj OPML datoteku", + "ButtonUpload": "Prenesi", + "ButtonUploadBackup": "Prenesi sigurnosnu kopiju", + "ButtonUploadCover": "Prenesi naslovnicu", + "ButtonUploadOPMLFile": "Prenesi OPML datoteku", "ButtonUserDelete": "Izbriši korisnika {0}", "ButtonUserEdit": "Uredi korisnika {0}", "ButtonViewAll": "Prikaži sve", "ButtonYes": "Da", - "ErrorUploadFetchMetadataAPI": "Pogreška pri dohvatu meta-podataka", - "ErrorUploadFetchMetadataNoResults": "Dohvat meta-podataka nije uspio - pokušajte ispraviti naslov i/ili autora", - "ErrorUploadLacksTitle": "Naslov je obavezan", + "ErrorUploadFetchMetadataAPI": "Pogreška pri dohvaćanju meta-podataka", + "ErrorUploadFetchMetadataNoResults": "Nije bilo moguće dohvatiti meta-podatake – pokušajte aktualizirati naslov i/ili autora", + "ErrorUploadLacksTitle": "Mora imati naslov", "HeaderAccount": "Korisnički račun", "HeaderAddCustomMetadataProvider": "Dodaj prilagođenog pružatelja meta-podataka", "HeaderAdvanced": "Napredno", @@ -134,7 +134,7 @@ "HeaderCollection": "Zbirka", "HeaderCollectionItems": "Stavke u zbirci", "HeaderCover": "Naslovnica", - "HeaderCurrentDownloads": "Preuzimanja u tijeku", + "HeaderCurrentDownloads": "Trenutačna preuzimanja", "HeaderCustomMessageOnLogin": "Prilagođena poruka prilikom prijave", "HeaderCustomMetadataProviders": "Prilagođeni pružatelji meta-podataka", "HeaderDetails": "Pojedinosti", @@ -197,7 +197,7 @@ "HeaderSetBackupSchedule": "Zakazivanje sigurnosne pohrane", "HeaderSettings": "Postavke", "HeaderSettingsDisplay": "Prikaz", - "HeaderSettingsExperimental": "Eksperimentalne značajke", + "HeaderSettingsExperimental": "Eksperimentalne funkcije", "HeaderSettingsGeneral": "Općenito", "HeaderSettingsScanner": "Skener", "HeaderSettingsSecurity": "Sigurnost", @@ -257,7 +257,7 @@ "LabelAuthors": "Autori", "LabelAutoDownloadEpisodes": "Automatski preuzmi nastavke", "LabelAutoFetchMetadata": "Automatski dohvati meta-podatke", - "LabelAutoFetchMetadataHelp": "Dohvaća meta-podatke o naslovu, autoru i serijalu kako bi pojednostavnio učitavanje. Dodatni meta-podatci će se možda morati dohvatiti nakon učitavanja.", + "LabelAutoFetchMetadataHelp": "Dohvaća meta-podatke o naslovu, autoru i serijalu kako bi pojednostavnio prijenos. Dodatni meta-podatci će se možda morati usporediti nakon prijenosa.", "LabelAutoLaunch": "Automatsko pokretanje", "LabelAutoLaunchDescription": "Automatski preusmjeri na pružatelja autentifikacijskih usluga prilikom otvaranja stranice za prijavu (putanja za ručno zaobilaženje opcije /login?autoLaunch=0)", "LabelAutoRegister": "Automatska registracija", @@ -286,8 +286,8 @@ "LabelClickToUseCurrentValue": "Kliknite za trenutnu vrijednost", "LabelClosePlayer": "Zatvori reproduktor", "LabelCodec": "Kodek", - "LabelCollapseSeries": "Serijale prikaži sažeto", - "LabelCollapseSubSeries": "Podserijale prikaži sažeto", + "LabelCollapseSeries": "Sažmi serijal", + "LabelCollapseSubSeries": "Sažmi podserijale", "LabelCollection": "Zbirka", "LabelCollections": "Zbirke", "LabelComplete": "Potpuno", @@ -303,7 +303,7 @@ "LabelCronExpression": "Cron izraz", "LabelCurrent": "Trenutan", "LabelCurrently": "Trenutno:", - "LabelCustomCronExpression": "Prilagođeni CRON izraz:", + "LabelCustomCronExpression": "Prilagođeni cron izraz:", "LabelDatetime": "Datum i vrijeme", "LabelDays": "Dani", "LabelDeleteFromFileSystemCheckbox": "Izbriši datoteke (uklonite kvačicu ako stavku želite izbrisati samo iz baze podataka)", @@ -312,7 +312,7 @@ "LabelDetectedPattern": "Prepoznat obrazac:", "LabelDevice": "Uređaj", "LabelDeviceInfo": "O uređaju", - "LabelDeviceIsAvailableTo": "Uređaj je dostupan...", + "LabelDeviceIsAvailableTo": "Uređaj je dostupan za...", "LabelDirectory": "Direktorij", "LabelDiscFromFilename": "Disk iz imena datoteke", "LabelDiscFromMetadata": "Disk iz metapodataka", @@ -340,7 +340,7 @@ "LabelEncodingBackupLocation": "Sigurnosna kopija vaših izvornih zvučnih datoteka čuvat će se u mapi:", "LabelEncodingChaptersNotEmbedded": "Poglavlja se ne ugrađuju u zvučne knjige koje se sastoje od više zvučnih zapisa.", "LabelEncodingClearItemCache": "Svakako redovito praznite predmemoriju stavki.", - "LabelEncodingFinishedM4B": "Stvorene M4B datoteke spremit će se u vašu mapu sa zvučnim knjigama:", + "LabelEncodingFinishedM4B": "Gotove M4B datoteke spremit će se u vašu mapu sa zvučnim knjigama:", "LabelEncodingInfoEmbedded": "Meta-podatci će se ugraditi u zvučne zapise u vašoj mapi sa zvučnim knjigama.", "LabelEncodingStartedNavigation": "Nakon pokretanja zadatka možete napustiti ovu stranicu.", "LabelEncodingTimeWarning": "Kodiranje može potrajati do 30 minuta.", @@ -357,8 +357,8 @@ "LabelEpisodes": "Nastavci", "LabelEpisodic": "U nastavcima", "LabelExample": "Primjer", - "LabelExpandSeries": "Serijal prikaži prošireno", - "LabelExpandSubSeries": "Podserijal prikaži prošireno", + "LabelExpandSeries": "Proširi serijal", + "LabelExpandSubSeries": "Proširi podserijal", "LabelExpired": "Istekao", "LabelExpiresAt": "Istječe", "LabelExpiresInSeconds": "Istječe za (sekundi)", @@ -374,7 +374,7 @@ "LabelFileBornDate": "Stvoreno {0}", "LabelFileModified": "Datoteka izmijenjena", "LabelFileModifiedDate": "Izmijenjeno {0}", - "LabelFilename": "Naziv datoteke", + "LabelFilename": "Ime datoteke", "LabelFilterByUser": "Filtriraj po korisniku", "LabelFindEpisodes": "Pronađi nastavke", "LabelFinished": "Završeno", @@ -394,7 +394,7 @@ "LabelHardDeleteFile": "Izbriši datoteku zauvijek", "LabelHasEbook": "Ima e-knjigu", "LabelHasSupplementaryEbook": "Ima dopunsku e-knjigu", - "LabelHideSubtitles": "Skrij podnaslove", + "LabelHideSubtitles": "Sakrij podnaslove", "LabelHighestPriority": "Najviši prioritet", "LabelHost": "Poslužitelj", "LabelHour": "Sat", @@ -402,7 +402,7 @@ "LabelIcon": "Ikona", "LabelImageURLFromTheWeb": "URL slike s weba", "LabelInProgress": "U tijeku", - "LabelIncludeInTracklist": "Uključi u popisu zvučnih zapisa", + "LabelIncludeInTracklist": "Uključi u popis zvučnih zapisa", "LabelIncomplete": "Nepotpuno", "LabelInterval": "Interval", "LabelIntervalCustomDailyWeekly": "Prilagođeno dnevno/tjedno", @@ -437,7 +437,7 @@ "LabelLibraryItem": "Stavka knjižnice", "LabelLibraryName": "Ime knjižnice", "LabelLibrarySortByProgress": "Napredak: zadnje ažurirano", - "LabelLibrarySortByProgressFinished": "Napredak: dovršeno", + "LabelLibrarySortByProgressFinished": "Napredak: završeno", "LabelLibrarySortByProgressStarted": "Napredak: započeto", "LabelLimit": "Ograničenje", "LabelLineSpacing": "Razmak između redaka", @@ -451,8 +451,8 @@ "LabelMatchExistingUsersBy": "Prepoznaj postojeće korisnike pomoću", "LabelMatchExistingUsersByDescription": "Rabi se za povezivanje postojećih korisnika. Nakon što se spoje, korisnike se prepoznaje temeljem jedinstvene oznake vašeg pružatelja SSO usluga", "LabelMaxEpisodesToDownload": "Najveći broj nastavaka za preuzimanje. 0 za neograničeno.", - "LabelMaxEpisodesToDownloadPerCheck": "Najviše novih nastavaka za preuzimanje po provjeri", - "LabelMaxEpisodesToKeep": "Najviše nastavaka za čuvanje", + "LabelMaxEpisodesToDownloadPerCheck": "Najveći broj novih nastavaka za preuzimanje po provjeri", + "LabelMaxEpisodesToKeep": "Najveći broj nastavaka za čuvanje", "LabelMaxEpisodesToKeepHelp": "Ako je vrijednost 0, nema ograničenja broja. Nakon automatskog preuzimanja novog nastavka ova funkcija briše najstariji nastavak ako ih ima više od zadanog broja. Ovo briše samo jedan nastavak po novom preuzetom nastavku.", "LabelMediaPlayer": "Reproduktor medijskih sadržaja", "LabelMediaType": "Vrsta medija", @@ -482,7 +482,7 @@ "LabelNoApiKeys": "Nema API ključeva", "LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka", "LabelNoEpisodesSelected": "Nema odabranih nastavaka", - "LabelNotFinished": "Nije dovršeno", + "LabelNotFinished": "Nezavršeno", "LabelNotStarted": "Nije započeto", "LabelNotes": "Bilješke", "LabelNotificationAppriseURL": "Apprise URL(ovi)", @@ -497,9 +497,9 @@ "LabelNumberOfBooks": "Broj knjiga", "LabelNumberOfChapters": "Broj poglavljâ:", "LabelNumberOfEpisodes": "broj nastavaka", - "LabelOpenIDAdvancedPermsClaimDescription": "Naziv OpenID zahtjeva koji sadrži napredna dopuštenja za korisničke radnje u aplikaciji koje će se primijeniti na ne-administratorske uloge (ako su konfigurirane). Ako zahtjev nedostaje u odgovoru, pristup ABS-u neće se odobriti. Ako i jedna opcija nedostaje, smatrat će se da je false. Pripazite da zahtjev pružatelja identiteta uvijek odgovara očekivanoj strukturi:", + "LabelOpenIDAdvancedPermsClaimDescription": "Ime OpenID zahtjeva koji sadrži napredna dopuštenja za korisničke radnje u aplikaciji koje će se primijeniti na ne-administratorske uloge (ako su konfigurirane). Ako zahtjev nedostaje u odgovoru, pristup ABS-u će se odbiti. Ako jedna opcija nedostaje, smatrat će se da je false. Pripazite da zahtjev pružatelja identiteta uvijek odgovara očekivanoj strukturi:", "LabelOpenIDClaims": "Sljedeće opcije ostavite praznima ako želite onemogućiti napredno dodjeljivanje grupa i dozvola, odnosno ako želite automatski dodijeliti grupu 'korisnik'.", - "LabelOpenIDGroupClaimDescription": "Naziv OpenID zahtjeva koji sadrži popis korisnikovih grupa. Često se naziva groups. Ako se konfigurira, aplikacija će automatski dodijeliti uloge temeljem korisnikovih članstava u grupama, pod uvjetom da se iste zovu 'admin', 'user' ili 'guest' u zahtjevu (ne razlikuju se velika i mala slova). Zahtjev treba sadržavati popis i ako je korisnik član više grupa, aplikacija će dodijeliti ulogu koja odgovara najvišoj razini pristupa. Ukoliko se niti jedna grupa ne podudara, pristup će biti onemogućen.", + "LabelOpenIDGroupClaimDescription": "Ime OpenID zahtjeva koji sadrži popis grupa korisnika. Često se zove groups. Ako se konfigurira, aplikacija će automatski dodijeliti uloge temeljem korisničkih članstava u grupama, pod uvjetom da se iste zovu 'admin', 'user' ili 'guest' u zahtjevu (ne razlikuju se velika i mala slova). Zahtjev treba sadržavati popis i ako je korisnik član više grupa, aplikacija će dodijeliti ulogu koja odgovara najvišoj razini pristupa. Ukoliko se niti jedna grupa ne podudara, pristup će se odbiti.", "LabelOpenRSSFeed": "Otvori RSS Feed", "LabelOverwrite": "Prepiši", "LabelPaginationPageXOfY": "Stranica {0} od {1}", @@ -513,7 +513,7 @@ "LabelPermissionsDelete": "Smije brisati", "LabelPermissionsDownload": "Smije preuzimati", "LabelPermissionsUpdate": "Smije ažurirati", - "LabelPermissionsUpload": "Smije učitavati", + "LabelPermissionsUpload": "Smije prenositi", "LabelPersonalYearReview": "Vaš godišnji pregled ({0})", "LabelPhotoPathURL": "Putanja ili URL fotografije", "LabelPlayMethod": "Način reprodukcije", @@ -545,11 +545,11 @@ "LabelRSSFeedSlug": "Slug RSS izvora", "LabelRSSFeedURL": "URL RSS izvora", "LabelRandomly": "Nasumično", - "LabelReAddSeriesToContinueListening": "Ponovno dodaj serijal u Nastavi slušati", + "LabelReAddSeriesToContinueListening": "Ponovo dodaj serijal u „Nastavi slušati”", "LabelRead": "Čitaj", "LabelReadAgain": "Ponovno čitaj", "LabelReadEbookWithoutProgress": "Čitaj e-knjige bez praćenja napretka", - "LabelRecentSeries": "Najnoviji serijali", + "LabelRecentSeries": "Nedavni serijali", "LabelRecentlyAdded": "Nedavno dodano", "LabelRecommended": "Preporučeno", "LabelRedo": "Ponovi", @@ -572,7 +572,7 @@ "LabelSelectEpisodesShowing": "Prikazujem {0} odabranih nastavaka", "LabelSelectUser": "Odaberite korisnika", "LabelSelectUsers": "Označi korisnike", - "LabelSendEbookToDevice": "Pošalji e-knjigu …", + "LabelSendEbookToDevice": "Pošalji e-knjigu na…", "LabelSequence": "Slijed", "LabelSerial": "Serijal", "LabelSeries": "Serijal", @@ -588,36 +588,36 @@ "LabelSettingsBookshelfViewHelp": "Skeumorfni dizajn sa drvenim policama", "LabelSettingsChromecastSupport": "Podrška za Chromecast", "LabelSettingsDateFormat": "Format datuma", - "LabelSettingsEnableWatcher": "Automatski prati ima li promjena u knjižnicama", - "LabelSettingsEnableWatcherForLibrary": "Automatski prati promjene u knjižnicama", + "LabelSettingsEnableWatcher": "Automatski prati promjene u knjižnicama", + "LabelSettingsEnableWatcherForLibrary": "Automatski prati promjene u knjižnici", "LabelSettingsEnableWatcherHelp": "Omogućuje automatsko dodavanje/ažuriranje stavki kada se uoče izmjene datoteka. *Potrebno je ponovno pokretanje poslužitelja", "LabelSettingsEpubsAllowScriptedContent": "Omogući skripte u epub datotekama", "LabelSettingsEpubsAllowScriptedContentHelp": "Omogućuje epub datotekama izvođenje skripti. Preporučamo isključiti ovu mogućnost ukoliko nemate povjerenja u izvore epub datoteka.", - "LabelSettingsExperimentalFeatures": "Eksperimentalne značajke", - "LabelSettingsExperimentalFeaturesHelp": "Značajke u razvoju za koje trebamo vaše povratne informacije i pomoć u testiranju. Kliknite za otvaranje rasprave na githubu.", + "LabelSettingsExperimentalFeatures": "Eksperimentalne funkcije", + "LabelSettingsExperimentalFeaturesHelp": "Funkcije u razvoju za koje trebamo vaše povratne informacije i pomoć u testiranju. Kliknite za otvaranje rasprave na githubu.", "LabelSettingsFindCovers": "Pronađi naslovnice", "LabelSettingsFindCoversHelp": "Ako vaša zvučna knjiga nema ugrađenu naslovnicu ili sliku naslovnice u mapi, skener će pokušati pronaći naslovnicu.
Napomena: ovo će produžiti trajanje skeniranja", - "LabelSettingsHideSingleBookSeries": "Skrij serijale sa samo jednom knjigom", + "LabelSettingsHideSingleBookSeries": "Sakrij serijale sa samo jednom knjigom", "LabelSettingsHideSingleBookSeriesHelp": "Serijali koji se sastoje od samo jedne knjige neće se prikazivati na stranici serijala i na policama početne stranice.", "LabelSettingsHomePageBookshelfView": "Prikaži početnu stranicu kao policu s knjigama", "LabelSettingsLibraryBookshelfView": "Prikaži knjižnicu kao policu s knjigama", - "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Postotak dovršenosti veći od", - "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Preostalo vrijeme je manje od (sekundi)", - "LabelSettingsLibraryMarkAsFinishedWhen": "Označi medij dovršenim kada", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Postotak završenosti je veći od", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Preostalo vrijeme je manje od (sekunde)", + "LabelSettingsLibraryMarkAsFinishedWhen": "Označi medij kao završen kada", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Preskoči ranije knjige u funkciji Nastavi serijal", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Na polici početne stranice Nastavi serijal prikazuje se prva nezapočeta knjiga serijala koji imaju barem jednu dovršenu knjigu i nijednu započetu knjigu. Ako se ova opcija uključi serijal će nastaviti od zadnje dovršene knjige umjesto od prve nezapočete knjige.", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Na polici početne stranice Nastavi serijal prikazuje se prva nezapočeta knjiga serijala koji imaju barem jednu završenu knjigu i nijednu započetu knjigu. Ako se ova opcija uključi serijal će nastaviti od zadnje završene knjige umjesto od prve nezapočete knjige.", "LabelSettingsParseSubtitles": "Raščlani podnaslove", - "LabelSettingsParseSubtitlesHelp": "Iz naziva mape zvučne knjige raščlanjuje podnaslov.
Podnaslov mora biti odvojen s \" - \"
npr. \"Naslov knjige - Ovo je podnaslov\" imat će podnaslov \"Ovo je podnaslov\"", + "LabelSettingsParseSubtitlesHelp": "Raščlani podnaslove iz imena mape zvučne knjige.
Podnaslov mora biti odvojen sa \" - \"
npr. \"Naslov knjige - Ovo je podnaslov\" imat će podnaslov \"Ovo je podnaslov\"", "LabelSettingsPreferMatchedMetadata": "Daj prednost meta-podatcima prepoznatih stavki", - "LabelSettingsPreferMatchedMetadataHelp": "Podatci prepoznatog naslova nadjačat će postojeće informacije kod korištenja funkcije Brzog prepoznavanja. Zadana funkcionalnost je da Brzo prepoznavanje samo dopuni podatke koji nedostaju.", + "LabelSettingsPreferMatchedMetadataHelp": "Podatci prepoznatog naslova će nadjačati postojeće informacije pri korištenju funkcije „Brzo prepoznavanje”. Zadana funkcionalnost je da „Brzo prepoznavanje” samo doda nedostajuće podatke.", "LabelSettingsSkipMatchingBooksWithASIN": "Preskoči prepoznavanje knjiga koje već imaju ASIN", "LabelSettingsSkipMatchingBooksWithISBN": "Preskoči prepoznavanje knjiga koje već imaju ISBN", "LabelSettingsSortingIgnorePrefixes": "Zanemari prefikse kod sortiranja", "LabelSettingsSortingIgnorePrefixesHelp": "npr. za prefiks \"the\" naslov knjige \"The Book Title\" sortirat će se \"Book Title, The\"", "LabelSettingsSquareBookCovers": "Koristi pravokutne naslovnice knjiga", "LabelSettingsSquareBookCoversHelp": "Koristi pravokutne naslovnice umjesto uobičajenih naslovnica omjera 1,6:1", - "LabelSettingsStoreCoversWithItem": "Spremi naslovnice uz stavke", - "LabelSettingsStoreCoversWithItemHelp": "Naslovnice se obično spremaju u /metadata/items, ako uključite ovu opciju naslovnice će se spremati u mapu knjižničke stavke. Čuva se samo jedna datoteka naziva \"cover\"", + "LabelSettingsStoreCoversWithItem": "Spremi naslovnice sa stavkom", + "LabelSettingsStoreCoversWithItemHelp": "Naslovnice se obično spremaju u /metadata/items. Uključivanjem ove opcije naslovnice će se spremati u mapu stavki vaše knjižnice. Čuva se samo jedna datoteka s imenom \"cover\"", "LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku", "LabelSettingsStoreMetadataWithItemHelp": "Meta-podatci se obično spremaju u /metadata/items; ako uključite ovu postavku meta-podatci će se čuvati u mapama knjižničkih stavki", "LabelSettingsTimeFormat": "Format vremena", @@ -647,7 +647,7 @@ "LabelStatsDaysListened": "Dana slušano", "LabelStatsHours": "Sati", "LabelStatsInARow": "uzastopno", - "LabelStatsItemsFinished": "Dovršenih stavki", + "LabelStatsItemsFinished": "Završene stavke", "LabelStatsItemsInLibrary": "Stavke u knjižnici", "LabelStatsMinutes": "minute", "LabelStatsMinutesListening": "Minuta odslušano", @@ -706,12 +706,12 @@ "LabelUpdateDetails": "Ažuriraj pojedinosti", "LabelUpdateDetailsHelp": "Dopusti prepisivanje postojećih podataka za odabrane knjige kada se prepoznaju", "LabelUpdatedAt": "Ažurirano", - "LabelUploaderDragAndDrop": "Pritisni i prevuci datoteke ili mape", - "LabelUploaderDragAndDropFilesOnly": "Pritisni i prevuci datoteke", + "LabelUploaderDragAndDrop": "Povuci i ispusti datoteke ili mape", + "LabelUploaderDragAndDropFilesOnly": "Povuci i ispusti datoteke", "LabelUploaderDropFiles": "Ispusti datoteke", "LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal", - "LabelUseAdvancedOptions": "Koristi se naprednim opcijama", - "LabelUseChapterTrack": "Upravljaj trakom poglavlja", + "LabelUseAdvancedOptions": "Koristi napredne opcije", + "LabelUseChapterTrack": "Koristi zvučni zapis poglavlja", "LabelUseFullTrack": "Koristi cijeli zvučni zapis", "LabelUseZeroForUnlimited": "0 za neograničeno", "LabelUser": "Korisnik", @@ -735,12 +735,12 @@ "LabelYourPlaylists": "Vaši popisi za izvođenje", "LabelYourProgress": "Vaš napredak", "MessageAddToPlayerQueue": "Dodaj u redoslijed izvođenja", - "MessageAppriseDescription": "Da biste se koristili ovom značajkom, treba vam instanca Apprise API-ja ili API koji može rukovati istom vrstom zahtjeva.
The Adresa Apprise API-ja treba biti puna URL putanja za slanje obavijesti, npr. ako vam se API instanca poslužuje na adresi http://192.168.1.1:8337 trebate upisati http://192.168.1.1:8337/notify.", + "MessageAppriseDescription": "Za korištnje ove funkcije, treba vam instanca Apprise API-ja ili API koji može rukovati istom vrstom zahtjeva.
URL Apprise API-ja mora biti potpuna URL putanja za slanje obavijesti, npr. ako vam se API instanca poslužuje na adresi http://192.168.1.1:8337 morate upisati http://192.168.1.1:8337/notify.", "MessageAsinCheck": "Upišite ASIN iz odgovarajuće Audibleove regije, ne s Amazonov.", "MessageAuthenticationLegacyTokenWarning": "Starije API tokene ćemo ukloniti. Umjesto njih, koristite se API ključevima .", "MessageAuthenticationOIDCChangesRestart": "Ponovno pokrenite poslužitelj da biste primijenili OIDC promjene.", "MessageAuthenticationSecurityMessage": "Provjera autentičnosti poboljšana je radi sigurnosti. Svi se korisnici moraju ponovno prijaviti.", - "MessageBackupsDescription": "Sigurnosne kopije sadrže korisnike, korisnikov napredak medija, pojedinosti knjižničke građe, postavke poslužitelja i slike koje se spremaju u /metadata/items & /metadata/authors. Sigurnosne kopije ne sadrže niti jednu datoteku iz mapa knjižnice.", + "MessageBackupsDescription": "Sigurnosne kopije sadrže korisnike, napredak korisnika, pojedinosti knjižničke građe, postavke poslužitelja i slike koje se spremaju u /metadata/items i /metadata/authors. Sigurnosne kopije ne sadrže niti jednu datoteku iz mapa knjižnice.", "MessageBackupsLocationEditNote": "Napomena: Uređivanje lokacije za sigurnosne kopije ne premješta ili mijenja postojeće sigurnosne kopije", "MessageBackupsLocationNoEditNote": "Napomena: Lokacija za sigurnosne kopije zadana je kroz varijablu okoline i ovdje se ne može izmijeniti.", "MessageBackupsLocationPathEmpty": "Putanja do lokacije za sigurnosne kopije ne može ostati prazna", @@ -760,7 +760,7 @@ "MessageChapterErrorStartLtPrev": "Netočno vrijeme početka, mora biti veće ili jednako vremenu početka prethodnog poglavlja", "MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja zvučne knjige", "MessageChaptersNotFound": "Poglavlja nisu pronađena", - "MessageCheckingCron": "Provjeravam cron...", + "MessageCheckingCron": "Provjeravanje cron izraza...", "MessageConfirmCloseFeed": "Sigurno želite zatvoriti ovaj izvor?", "MessageConfirmDeleteApiKey": "Sigurno želite izbrisati API ključ \"{0}\"?", "MessageConfirmDeleteBackup": "Sigurno želite izbrisati sigurnosnu kopiju za {0}?", @@ -774,12 +774,12 @@ "MessageConfirmDeleteSession": "Sigurno želite izbrisati ovu sesiju?", "MessageConfirmEmbedMetadataInAudioFiles": "Sigurno želite ugraditi meta-podatke u {0} zvučnih datoteka?", "MessageConfirmForceReScan": "Sigurno želite ponovno pokrenuti skeniranje?", - "MessageConfirmMarkAllEpisodesFinished": "Sigurno želite označiti sve nastavke dovršenima?", - "MessageConfirmMarkAllEpisodesNotFinished": "Sigurno želite označiti sve nastavke nedovršenima?", - "MessageConfirmMarkItemFinished": "Sigurno želite označiti \"{0}\" dovršenim?", - "MessageConfirmMarkItemNotFinished": "Sigurno želite označiti \"{0}\" nedovršenim?", - "MessageConfirmMarkSeriesFinished": "Sigurno želite označiti sve knjige u ovom serijalu dovršenima?", - "MessageConfirmMarkSeriesNotFinished": "Sigurno želite označiti sve knjige u ovom serijalu nedovršenima?", + "MessageConfirmMarkAllEpisodesFinished": "Sigurno želite označiti sve nastavke kao završene?", + "MessageConfirmMarkAllEpisodesNotFinished": "Sigurno želite označiti sve nastavke kao nezavršene?", + "MessageConfirmMarkItemFinished": "Sigurno želite označiti stavku \"{0}\" kao završenu?", + "MessageConfirmMarkItemNotFinished": "Sigurno želite označiti stavku \"{0}\" kao nezavršenu?", + "MessageConfirmMarkSeriesFinished": "Sigurno želite označiti sve knjige u ovom serijalu kao završene?", + "MessageConfirmMarkSeriesNotFinished": "Sigurno želite označiti sve knjige u ovom serijalu kao nezavršene?", "MessageConfirmNotificationTestTrigger": "Želite li okinuti ovu obavijest s probnim podatcima?", "MessageConfirmPurgeCache": "Brisanje predmemorije izbrisat će cijelu mapu /metadata/cache.

Sigurno želite izbrisati mapu predmemorije?", "MessageConfirmPurgeItemsCache": "Brisanje predmemorije stavki izbrisat će cijelu mapu /metadata/cache/items.
Jeste li sigurni?", @@ -807,14 +807,14 @@ "MessageConfirmUnlinkOpenId": "Sigurno želite odspojiti ovog korisnika s OpenID-ja?", "MessageDaysListenedInTheLastYear": "{0} dana slušanja u posljednjih godinu dana", "MessageDownloadingEpisode": "Preuzimam nastavak", - "MessageDragFilesIntoTrackOrder": "Prevlačenjem datoteka složite pravilan redoslijed", + "MessageDragFilesIntoTrackOrder": "Povlači datoteke u ispravan redoslijed", "MessageEmbedFailed": "Ugrađivanje nije uspjelo!", - "MessageEmbedFinished": "Ugrađivanje je dovršeno!", + "MessageEmbedFinished": "Ugrađivanje je završeno!", "MessageEmbedQueue": "Ugrađivanje meta-podataka dodano u red obrade ({0} u redu)", "MessageEpisodesQueuedForDownload": "{0} nastavak(a) u redu za preuzimanje", "MessageEreaderDevices": "Da biste osigurali isporuku e-knjiga, možda ćete morati gornju adresu e-pošte dodati kao dopuštenog pošiljatelja za svaki od donjih uređaja.", "MessageFeedURLWillBe": "URL izvora bit će {0}", - "MessageFetching": "Dohvaćam...", + "MessageFetching": "Dohvaćanje...", "MessageForceReScanDescription": "će ponovno skenirati sve datoteke kao nove datoteke. ID3 tagovi zvučnih datoteka, OPF datoteke i tekstualne datoteke skenirat će se kao da su nove.", "MessageHeatmapListeningTimeTooltip": "{0} sluša na {1}", "MessageHeatmapNoListeningSessions": "Nema sesija slušanja na {0}", @@ -824,27 +824,27 @@ "MessageItemsSelected": "{0} odabranih stavki", "MessageItemsUpdated": "{0} stavki ažurirano", "MessageJoinUsOn": "Pridruži nam se na", - "MessageLoading": "Učitavam...", - "MessageLoadingFolders": "Učitavam mape...", + "MessageLoading": "Učitavanje...", + "MessageLoadingFolders": "Učitavanje mape...", "MessageLogsDescription": "Zapisnici se čuvaju u /metadata/logs u obliku JSON datoteka. Zapisnici pada sustava čuvaju se u datoteci /metadata/logs/crash_logs.txt.", - "MessageM4BFailed": "M4B neuspješan!", - "MessageM4BFinished": "M4B završio!", - "MessageMapChapterTitles": "Mapiraj nazive poglavlja postojećim poglavljima zvučne knjige bez uređivanja vremenskih identifikatora", - "MessageMarkAllEpisodesFinished": "Označi sve nastavke dovršenima", - "MessageMarkAllEpisodesNotFinished": "Označi sve nastavke nedovršenima", - "MessageMarkAsFinished": "Označi kao dovršeno", - "MessageMarkAsNotFinished": "Označi kao nedovršeno", - "MessageMatchBooksDescription": "će pokušati prepoznati knjige iz knjižnice u katalogu odabranog pružatelja podatka te nadopuniti podatke koji nedostaju i naslovnice. Ne prepisuje preko postojećih podataka.", + "MessageM4BFailed": "M4B datoteka neuspjela!", + "MessageM4BFinished": "M4B datoteka je završena!", + "MessageMapChapterTitles": "Mapiraj imana poglavlja s vašim postojećim poglavljima zvučne knjige bez podešavanja vremenskih identifikatora", + "MessageMarkAllEpisodesFinished": "Označi sve nastavke kao završene", + "MessageMarkAllEpisodesNotFinished": "Označi sve nastavke kao nezavršene", + "MessageMarkAsFinished": "Označi kao završeno", + "MessageMarkAsNotFinished": "Označi kao nezavršeno", + "MessageMatchBooksDescription": "će pokušati prepoznati knjige iz knjižnice u katalogu odabranog pružatelja podatka te nadopuniti podatke koji nedostaju i naslovnice. Ne prepisuje postojeće podatke.", "MessageNoAudioTracks": "Nema zvučnih zapisa", "MessageNoAuthors": "Nema autora", "MessageNoBackups": "Nema sigurnosnih kopija", "MessageNoBookmarks": "Nema knjižnih oznaka", "MessageNoChapters": "Nema poglavlja", "MessageNoCollections": "Nema zbirki", - "MessageNoCoversFound": "Naslovnice nisu pronađene", + "MessageNoCoversFound": "Nije pronađena nijedna naslovnica", "MessageNoDescription": "Nema opisa", "MessageNoDevices": "Nema uređaja", - "MessageNoDownloadsInProgress": "Nema preuzimanja u tijeku", + "MessageNoDownloadsInProgress": "Trenutačno nema preuzimanja u tijeku", "MessageNoDownloadsQueued": "Nema preuzimanja u redu", "MessageNoEpisodeMatchesFound": "Nije pronađen ni jedan odgovarajući nastavak", "MessageNoEpisodes": "Nema nastavaka", @@ -873,28 +873,28 @@ "MessagePauseChapter": "Pauziraj reprodukciju poglavlja", "MessagePlayChapter": "Slušaj početak poglavlja", "MessagePlaylistCreateFromCollection": "Stvori popis za izvođenje od zbirke", - "MessagePleaseWait": "Molimo pričekajte...", + "MessagePleaseWait": "Pričekajte...", "MessagePodcastHasNoRSSFeedForMatching": "Podcast nema adresu RSS izvora za prepoznavanje", "MessagePodcastSearchField": "Upišite izraz za pretraživanje ili URL RSS izvora", "MessageQuickEmbedInProgress": "Brzo ugrađivanje u tijeku", "MessageQuickEmbedQueue": "Dodano u red za brzo ugrađivanje ({0} u redu izvođenja)", "MessageQuickMatchAllEpisodes": "Brzo prepoznavanje svih nastavaka", - "MessageQuickMatchDescription": "Popuni pojedinosti i naslovnice koji nedostaju prvim pronađenim rezultatom za '{0}'. Ne prepisuje podatke osim ako ne uključite mogućnost 'Daj prednost meta-podatcima prepoznatih stavki'.", + "MessageQuickMatchDescription": "Popuni pojedinosti i naslovnice koji nedostaju prvim pronađenim rezultatom za '{0}'. Ne prepisuje podatke osim ako je postavka poslužitelja „Daj prednost meta-podatcima prepoznatih stavki” uključena.", "MessageRemoveChapter": "Ukloni poglavlje", "MessageRemoveEpisodes": "Ukloni {0} nastavaka", "MessageRemoveFromPlayerQueue": "Ukloni iz redoslijeda izvođenja", "MessageRemoveUserWarning": "Sigurno želite trajno izbrisati korisnika \"{0}\"?", - "MessageReportBugsAndContribute": "Prijavite pogreške, zatražite značajke i doprinesite na", + "MessageReportBugsAndContribute": "Prijavite pogreške, zatražite funkcije 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.

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.

Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.", - "MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom i ostaviti postavku promatrača mape aktiviranom. Promatrač mapa će automatski otkriti promjene u mapama vaše knjižnice. Promatrač mapa ne radi na svakom datotečnom sustavu (kao što je NFS) pa se umjesto njega mogu koristiti planirana pretraživanja knjižnice.", + "MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom kao i fukciju „Automatski prati promjene u knjižnici” koja automatski otkriva promjene u mapama vaše knjižnice. Fukcija „Automatski prati promjene u knjižnici” ne radi na svakom datotečnom sustavu (kao što je NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Pokreni svaki {0} u {1}", "MessageSearchResultsFor": "Rezultati pretrage za", "MessageSelected": "{0} odabrano", "MessageSeriesSequenceCannotContainSpaces": "Slijed serijala ne može sadržavati praznine", - "MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju", - "MessageSetChaptersFromTracksDescription": "Postavi poglavlja koristeći se zvučnom datotekom kao poglavljem i nazivom datoteke kao naslovom poglavlja", + "MessageServerCouldNotBeReached": "Nije bilo moguće pristupiti poslužitelju", + "MessageSetChaptersFromTracksDescription": "Postavi poglavlja koristeći svaku zvučnu datoteku kao poglavlje, a naslov poglavlja kao ime datoteke", "MessageShareExpirationWillBe": "Vrijeme isteka će biti {0}", "MessageShareExpiresIn": "Istječe za {0}", "MessageShareURLWillBe": "URL za dijeljenje bit će {0}", @@ -923,7 +923,7 @@ "MessageTaskOpmlImportFeedPodcastDescription": "Stvaranje podcasta \"{0}\"", "MessageTaskOpmlImportFeedPodcastExists": "Podcast već postoji u putanji", "MessageTaskOpmlImportFeedPodcastFailed": "Stvaranje podcasta nije uspjelo", - "MessageTaskOpmlImportFinished": "Dodano {0} podcasta", + "MessageTaskOpmlImportFinished": "Broj dodanih podcasta: {0}", "MessageTaskOpmlParseFailed": "Raščlanjivanje OPML datoteke nije uspjelo", "MessageTaskOpmlParseFastFail": "Neispravna OPML datoteka, oznaka nije pronađena ILI oznaka nije pronađena", "MessageTaskOpmlParseNoneFound": "U OPML datoteci nisu pronađeni izvori", @@ -934,10 +934,10 @@ "MessageTaskScanningFileChanges": "Skeniranje izmijenjenih datoteka u \"{0}\"", "MessageTaskScanningLibrary": "Skeniranje knjižnice \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "U odredišnu mapu nije moguće pisati", - "MessageThinking": "Razmišljam...", - "MessageUploaderItemFailed": "Učitavanje nije uspjelo", - "MessageUploaderItemSuccess": "Uspješno učitano!", - "MessageUploading": "Učitavam...", + "MessageThinking": "Razmišljanje...", + "MessageUploaderItemFailed": "Prijenos nije uspio", + "MessageUploaderItemSuccess": "Uspješno preneseno!", + "MessageUploading": "Prijenos...", "MessageValidCronExpression": "Ispravan cron izraz", "MessageWatcherIsDisabledGlobally": "Praćenje datotečnog sustava globalno je isključen u postavkama poslužitelja", "MessageXLibraryIsEmpty": "{0} Knjižnica je prazna!", @@ -949,8 +949,8 @@ "NoteRSSFeedPodcastAppsHttps": "Pripazite: Većina aplikacija za podcaste iziskuje URL RSS izvora koji se koristi HTTPS protokolom", "NoteRSSFeedPodcastAppsPubDate": "Upozorenje: jedan ili više vaših nastavaka nemaju datum objavljivanja. To je obavezno kod nekih aplikacija za podcaste.", "NoteUploaderFoldersWithMediaFiles": "Mape s medijskim datotekama smatrat će se zasebnim stavkama knjižnice.", - "NoteUploaderOnlyAudioFiles": "Ako učitavate samo zvučne datoteke svaka će se zvučna datoteka uvesti kao zasebna zvučna knjiga.", - "NoteUploaderUnsupportedFiles": "Nepodržane vrste datoteka zanemaruju se. Kada odabirete datoteke ili ispuštate mapu, sve datoteke koje nisu u mapi stavke zanemarit će se.", + "NoteUploaderOnlyAudioFiles": "Ako prenosite samo zvučne datoteke svaka će se zvučna datoteka uvesti kao zasebna zvučna knjiga.", + "NoteUploaderUnsupportedFiles": "Nepodržane vrste datoteka se zanemaruju. Kada birate datoteke ili ispuštate mapu, sve datoteke koje nisu u mapi stavke će se zanemariti.", "NotificationOnBackupCompletedDescription": "Pokreće se po završetku sigurnosnog kopiranja", "NotificationOnBackupFailedDescription": "Pokreće se kada sigurnosno kopiranje ne uspije", "NotificationOnEpisodeDownloadedDescription": "Pokreće se kada se nastavak podcasta automatski preuzme", @@ -960,14 +960,14 @@ "PlaceholderBulkChapterInput": "Upišite naslov poglavlja ili ga numerirajte (npr. '1. nastavak', 'Poglavlje 10', '1.')", "PlaceholderNewCollection": "Ime nove zbirke", "PlaceholderNewFolderPath": "Nova putanja mape", - "PlaceholderNewPlaylist": "Naziv novog popisa za izvođenje", + "PlaceholderNewPlaylist": "Ime novog popisa za izvođenje", "PlaceholderSearch": "Traži...", "PlaceholderSearchEpisode": "Traži nastavak...", "StatsAuthorsAdded": "autora dodano", "StatsBooksAdded": "knjiga dodano", "StatsBooksAdditional": "Novi naslovi uključuju…", - "StatsBooksFinished": "knjiga dovršeno", - "StatsBooksFinishedThisYear": "Neke knjige dovršene ove godine…", + "StatsBooksFinished": "završene knjige", + "StatsBooksFinishedThisYear": "Neke završene knjige ove godine…", "StatsBooksListenedTo": "knjiga slušano", "StatsCollectionGrewTo": "Vaša je zbirka knjiga narasla na…", "StatsSessions": "sesija", @@ -999,8 +999,8 @@ "ToastBackupInvalidMaxKeep": "Neispravan broj sigurnosnih kopija za čuvanje", "ToastBackupInvalidMaxSize": "Neispravna najveća veličina sigurnosne kopije", "ToastBackupRestoreFailed": "Vraćanje sigurnosne kopije nije uspjelo", - "ToastBackupUploadFailed": "Učitavanje sigurnosne kopije nije uspjelo", - "ToastBackupUploadSuccess": "Sigurnosna kopija učitana", + "ToastBackupUploadFailed": "Prijenos sigurnosne kopije nije uspio", + "ToastBackupUploadSuccess": "Sigurnosna kopija je prenesea", "ToastBatchApplyDetailsToItemsSuccess": "Pojedinosti primijenjene stavkama", "ToastBatchDeleteFailed": "Grupno brisanje nije uspjelo", "ToastBatchDeleteSuccess": "Grupno brisanje je uspjelo", @@ -1023,7 +1023,7 @@ "ToastChaptersMustHaveTitles": "Poglavlja moraju imati naslove", "ToastChaptersRemoved": "Poglavlja uklonjena", "ToastChaptersUpdated": "Poglavlja su ažurirana", - "ToastCollectionItemsAddFailed": "Neuspješno dodavanje stavki u zbirku", + "ToastCollectionItemsAddFailed": "Neuspjelo dodavanje stavki u zbirku", "ToastCollectionRemoveSuccess": "Zbirka izbrisana", "ToastCollectionUpdateSuccess": "Zbirka ažurirana", "ToastConnectionNotAvailable": "Veza nije dostupna. Pokušaj ponovo kasnije", @@ -1033,13 +1033,13 @@ "ToastDeleteFileFailed": "Brisanje datoteke nije uspjelo", "ToastDeleteFileSuccess": "Datoteka izbrisana", "ToastDeviceAddFailed": "Dodavanje uređaja nije uspjelo", - "ToastDeviceNameAlreadyExists": "E-čitač s tim nazivom već postoji", + "ToastDeviceNameAlreadyExists": "E-čitač s tim imenom već postoji", "ToastDeviceTestEmailFailed": "Slanje probne poruke e-pošte nije uspjelo", "ToastDeviceTestEmailSuccess": "Probna poruka e-pošte poslana", "ToastEmailSettingsUpdateSuccess": "Postavke e-pošte ažurirane", - "ToastEncodeCancelFailed": "Kodiranje nije uspješno otkazano", + "ToastEncodeCancelFailed": "Prekidanje kodiranja nije uspjelo", "ToastEncodeCancelSucces": "Kodiranje otkazano", - "ToastEpisodeDownloadQueueClearFailed": "Redoslijed izvođenja nije uspješno očišćen", + "ToastEpisodeDownloadQueueClearFailed": "Brisanje redoslijeda izvođenja nije uspjelo", "ToastEpisodeDownloadQueueClearSuccess": "Redoslijed preuzimanja nastavaka očišćen", "ToastEpisodeUpdateSuccess": "{0} nastavak/a ažurirano", "ToastErrorCannotShare": "Dijeljenje na ovaj uređaj nije moguće", @@ -1057,10 +1057,10 @@ "ToastItemDeletedFailed": "Brisanje stavke nije uspjelo", "ToastItemDeletedSuccess": "Stavka je izbrisana", "ToastItemDetailsUpdateSuccess": "Pojedinosti stavke su ažurirane", - "ToastItemMarkedAsFinishedFailed": "Označavanje kao Dovršeno nije uspjelo", - "ToastItemMarkedAsFinishedSuccess": "Stavka označena kao dovršena", - "ToastItemMarkedAsNotFinishedFailed": "Označavanje kao Nije dovršeno nije uspjelo", - "ToastItemMarkedAsNotFinishedSuccess": "Stavka označena kao nedovršena", + "ToastItemMarkedAsFinishedFailed": "Označavanje kao „Završeno” nije uspjelo", + "ToastItemMarkedAsFinishedSuccess": "Stavka je označena kao završena", + "ToastItemMarkedAsNotFinishedFailed": "Označavanje kao „Nezavršeno” nije uspjelo", + "ToastItemMarkedAsNotFinishedSuccess": "Stavka je označena kao nezavršena", "ToastItemUpdateSuccess": "Stavka ažurirana", "ToastLibraryCreateFailed": "Stvaranje knjižnice nije uspjelo", "ToastLibraryCreateSuccess": "Knjižnica \"{0}\" stvorena", @@ -1069,17 +1069,17 @@ "ToastLibraryScanFailedToStart": "Skeniranje nije uspjelo", "ToastLibraryScanStarted": "Skeniranje knjižnice započelo", "ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" ažurirana", - "ToastMatchAllAuthorsFailed": "Nisu prepoznati svi autori", + "ToastMatchAllAuthorsFailed": "Prepoznavanje svih autora nije uspjelo", "ToastMetadataFilesRemovedError": "Pogreška kod uklanjanja datoteka metadata.{0}", "ToastMetadataFilesRemovedNoneFound": "U knjižnici nisu pronađene datoteke metadata.{0}", - "ToastMetadataFilesRemovedNoneRemoved": "Datoteke metadata.{0} nisu uklonjenje", - "ToastMetadataFilesRemovedSuccess": "uklonjeno {0} datoteka metadata.{1}", - "ToastMustHaveAtLeastOnePath": "Mora postojati barem jedna putanja", + "ToastMetadataFilesRemovedNoneRemoved": "Nijedna datoteka metadata.{0} nije uklonjenja", + "ToastMetadataFilesRemovedSuccess": "Broj uklonjenih datoteka metadata.{1}: {0}", + "ToastMustHaveAtLeastOnePath": "Mora imati barem jednu putanju", "ToastNameEmailRequired": "Ime i adresa e-pošte su obavezni", "ToastNameRequired": "Ime je obavezno", "ToastNewApiKeyUserError": "Morate odabrati korisnika", "ToastNewEpisodesFound": "pronađeno {0} novih nastavaka", - "ToastNewUserCreatedFailed": "Račun \"{0}\" nije uspješno izrađen", + "ToastNewUserCreatedFailed": "Stvaranje računa nije uspjelo: \"{0}\"", "ToastNewUserCreatedSuccess": "Novi račun izrađen", "ToastNewUserLibraryError": "Treba odabrati barem jednu knjižnicu", "ToastNewUserPasswordError": "Mora imati zaporku, samo korisnik root može imati praznu zaporku", @@ -1090,20 +1090,20 @@ "ToastNoUpdatesNecessary": "Ažuriranja nisu potrebna", "ToastNotificationCreateFailed": "Stvaranje obavijesti nije uspjelo", "ToastNotificationDeleteFailed": "Brisanje obavijesti nije uspjelo", - "ToastNotificationFailedMaximum": "Najveći broj neuspješnih pokušaja mora biti >= 0", + "ToastNotificationFailedMaximum": "Najveći broj neuspjelih pokušaja mora biti >= 0", "ToastNotificationQueueMaximum": "Najveći broj obavijesti u redu mora biti >= 0", "ToastNotificationSettingsUpdateSuccess": "Postavke obavijesti ažurirane", "ToastNotificationTestTriggerFailed": "Okidanje probne obavijesti nije uspjelo", "ToastNotificationTestTriggerSuccess": "Okinuta je probna obavijest", "ToastNotificationUpdateSuccess": "Obavijest ažurirana", - "ToastPlaylistCreateFailed": "Popis za izvođenje nije izrađen", + "ToastPlaylistCreateFailed": "Izrada popisa za izvođenje nije uspjela", "ToastPlaylistCreateSuccess": "Popis za izvođenje izrađen", "ToastPlaylistRemoveSuccess": "Popis za izvođenje uklonjen", "ToastPlaylistUpdateSuccess": "Popis za izvođenje ažuriran", - "ToastPodcastCreateFailed": "Podcast nije izrađen", + "ToastPodcastCreateFailed": "Stvaranje podcasta nije uspjelo", "ToastPodcastCreateSuccess": "Podcast uspješno izrađen", "ToastPodcastEpisodeUpdated": "Nastavak ažuriran", - "ToastPodcastGetFeedFailed": "Dohvat izvora podcasta nije uspio", + "ToastPodcastGetFeedFailed": "Dohvaćanje izvora podcasta nije uspjelo", "ToastPodcastNoEpisodesInFeed": "U RSS izvoru nisu pronađeni nastavci", "ToastPodcastNoRssFeed": "Podcast nema RSS izvor", "ToastProgressIsNotBeingSynced": "Napredak se ne sinkronizira, ponovno pokrenite reprodukciju", @@ -1111,7 +1111,7 @@ "ToastProviderCreatedSuccess": "Novi pružatelj dodan", "ToastProviderNameAndUrlRequired": "Ime i URL su obavezni", "ToastProviderRemoveSuccess": "Pružatelj uklonjen", - "ToastRSSFeedCloseFailed": "RSS izvor nije uspješno zatvoren", + "ToastRSSFeedCloseFailed": "Zatvaranje RSS izvora nije uspjelo", "ToastRSSFeedCloseSuccess": "RSS izvor zatvoren", "ToastRemoveFailed": "Uklanjanje nije uspjelo", "ToastRemoveItemFromCollectionFailed": "Uklanjanje stavke iz zbirke nije uspjelo", @@ -1119,7 +1119,7 @@ "ToastRemoveItemsWithIssuesFailed": "Uklanjanje knjižničkih stavki s problemima nije uspjelo", "ToastRemoveItemsWithIssuesSuccess": "Uspješno uklonjene knjižničke stavke s problemima", "ToastRenameFailed": "Preimenovanje nije uspjelo", - "ToastRescanFailed": "Ponovno skeniranje {0} nije uspjelo", + "ToastRescanFailed": "Ponovno skeniranje nije uspjelo za {0}", "ToastRescanRemoved": "Ponovno skeniranje dovršene stavke je uklonjeno", "ToastRescanUpToDate": "Ponovno skeniranje dovršene stavke bilo je ažurno", "ToastRescanUpdated": "Ponovno skeniranje dovršene stavke je ažurirano", @@ -1134,7 +1134,7 @@ "ToastSessionCloseFailed": "Zatvaranje sesije nije uspjelo", "ToastSessionDeleteFailed": "Brisanje sesije nije uspjelo", "ToastSessionDeleteSuccess": "Sesija izbrisana", - "ToastSleepTimerDone": "Timer za spavanje istječe... zZzzZz", + "ToastSleepTimerDone": "Timer za spavanje gotov... zZzzZz", "ToastSlugMustChange": "Slug sadrži nedozvoljene znakove", "ToastSlugRequired": "Slug je obavezan", "ToastSocketConnected": "Socket priključen", @@ -1146,8 +1146,8 @@ "ToastUnknownError": "Nepoznata pogreška", "ToastUnlinkOpenIdFailed": "Uklanjanje OpenID veze korisnika nije uspjelo", "ToastUnlinkOpenIdSuccess": "Korisnik odspojen od OpenID-ja", - "ToastUploaderFilepathExistsError": "Putanja \"{0}\" već postoji na poslužitelju", - "ToastUploaderItemExistsInSubdirectoryError": "Stavka \"{0}\" koristi se podmapom u putanje za učitavanje.", + "ToastUploaderFilepathExistsError": "Datotečna putanja \"{0}\" već postoji na poslužitelju", + "ToastUploaderItemExistsInSubdirectoryError": "Stavka \"{0}\" koristi podmapu putanje za prijenos.", "ToastUserDeleteFailed": "Brisanje korisnika nije uspjelo", "ToastUserDeleteSuccess": "Korisnik izbrisan", "ToastUserPasswordChangeSuccess": "Zaporka je uspješno promijenjena", From e51c7b2be1edf27589585b91a566e674a3ae130a Mon Sep 17 00:00:00 2001 From: Jan Schoenfeld Date: Mon, 17 Nov 2025 09:26:28 +0100 Subject: [PATCH 064/285] Translated using Weblate (German) Currently translated at 99.9% (1162 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/ --- client/strings/de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index 5acae6d9b..403272dcc 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -589,7 +589,7 @@ "LabelSettingsChromecastSupport": "Chromecastunterstützung", "LabelSettingsDateFormat": "Datumsformat", "LabelSettingsEnableWatcher": "Bibliotheken automatisch nach Änderungen durchsuchen", - "LabelSettingsEnableWatcherForLibrary": "Bibliothek automatisch nach Änderungen durchsuchen", + "LabelSettingsEnableWatcherForLibrary": "Bibliothek automatisch auf Änderungen überwachen", "LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart", "LabelSettingsEpubsAllowScriptedContent": "Skriptinhalte in Epubs zulassen", "LabelSettingsEpubsAllowScriptedContentHelp": "Erlaube Epub-Dateien, Skripte auszuführen. Es wird empfohlen, diese Einstellung deaktiviert zu lassen, es sei denn, du vertraust der Quelle der Epub-Dateien.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?", "MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am", "MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.

Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.

Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.", - "MessageScheduleLibraryScanNote": "Für die meisten Anwender wird empfohlen, diese Funktion deaktiviert und die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung wird Änderungen in den Bibliotheksordnern automatisch erkennen. Die Ordnerüberwachung funktioniert nicht mit allen Dateisystemen (wie NFS), hier kann stattdessen die automatischen Bibliothekssuchen verwendet werden.", + "MessageScheduleLibraryScanNote": "Für die meisten Benutzer wird empfohlen, diese Funktion deaktiviert und die Einstellung „Bibliothek automatisch auf Änderungen überwachen“ aktiviert zu lassen – dadurch werden Änderungen in Ihren Bibliotheksordnern automatisch erkannt. Aktivieren Sie diese Funktion, wenn „Bibliothek automatisch auf Änderungen überwachen“ für Ihr Dateisystem (wie NFS) nicht funktioniert.", "MessageScheduleRunEveryWeekdayAtTime": "Immer {0} um {1} ausführen", "MessageSearchResultsFor": "Suchergebnisse für", "MessageSelected": "{0} ausgewählt", From 8c5678b5730a257527846bada29482048523fa6d Mon Sep 17 00:00:00 2001 From: dapitch666 Date: Mon, 17 Nov 2025 11:15:14 +0100 Subject: [PATCH 065/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index b560bfc06..f2c795980 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Interface skeumorphique avec étagères en bois", "LabelSettingsChromecastSupport": "Support du Chromecast", "LabelSettingsDateFormat": "Format de date", - "LabelSettingsEnableWatcher": "Analyser automatiquement les bibliothèques pour détecter les modifications", - "LabelSettingsEnableWatcherForLibrary": "Analyser automatiquement la bibliothèque pour détecter les modifications", + "LabelSettingsEnableWatcher": "Surveiller automatiquement les bibliothèques pour détecter les modifications", + "LabelSettingsEnableWatcherForLibrary": "Surveiller automatiquement la bibliothèque pour détecter les modifications", "LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique d'éléments lorsque des modifications de fichiers sont détectées. * Nécessite le redémarrage du serveur", "LabelSettingsEpubsAllowScriptedContent": "Autoriser le contenu scénarisé pour les fichiers EPUB", "LabelSettingsEpubsAllowScriptedContentHelp": "Autoriser les fichiers EPUB à exécuter des scripts. Il est recommandé de laisser ce paramètre désactivé, sauf si vous faites confiance à la source des fichiers EPUB.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Êtes-vous sûr·e de vouloir réinitialiser les chapitres et annuler les changements effectués ?", "MessageRestoreBackupConfirm": "Êtes-vous sûr·e de vouloir restaurer la sauvegarde créée le", "MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.

Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.

Tous les clients utilisant votre serveur seront automatiquement mis à jour.", - "MessageScheduleLibraryScanNote": "Pour la plupart des utilisateurs, il est recommandé de laisser cette fonctionnalité désactivée et de maintenir le réglage du moniteur de dossier activé. Le moniteur de dossier détectera automatiquement les changements dans vos dossiers de bibliothèque. Le moniteur de dossier ne fonctionne pas pour chaque système de fichiers (comme NFS) afin que les scans de bibliothèques programmés puissent être utilisés à la place.", + "MessageScheduleLibraryScanNote": "Pour la plupart des utilisateurs, il est recommandé de laisser cette fonctionnalité désactivée et de maintenir le paramètre « Surveiller automatiquement la bibliothèque pour détecter les modifications » activé – il détectera automatiquement les modifications dans les dossiers de votre bibliothèque. Activez cette fonctionnalité si l'option « Surveiller automatiquement la bibliothèque pour détecter les modifications » ne fonctionne pas pour votre système de fichiers (comme NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Exécuté tous les {0} à {1}", "MessageSearchResultsFor": "Résultats de recherche pour", "MessageSelected": "{0} sélectionnés", From 568b154e8a759558bcd8e6a221253e078a4f9659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Engstr=C3=B6m?= Date: Sun, 16 Nov 2025 09:38:09 +0100 Subject: [PATCH 066/285] Translated using Weblate (Swedish) Currently translated at 99.8% (1161 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index e41dd8e2b..b1fa8f229 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ingen {0}", "LabelLibraryItem": "Objekt", "LabelLibraryName": "Biblioteksnamn", - "LabelLibrarySortByProgress": "Framsteg uppdaterat", - "LabelLibrarySortByProgressFinished": "Avslutsdag", - "LabelLibrarySortByProgressStarted": "Startdag", + "LabelLibrarySortByProgress": "Framsteg: senast uppdaterat", + "LabelLibrarySortByProgressFinished": "Framsteg: avslutad", + "LabelLibrarySortByProgressStarted": "Framsteg: påbörjad", "LabelLimit": "Begränsning", "LabelLineSpacing": "Radavstånd", "LabelListenAgain": "Lyssna igen", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Bakgrund med ett utseende liknande en bokhylla i trä", "LabelSettingsChromecastSupport": "Stöd för Chromecast", "LabelSettingsDateFormat": "Datumformat", - "LabelSettingsEnableWatcher": "Upptäck automatiskt förändringar i biblioteket", - "LabelSettingsEnableWatcherForLibrary": "Upptäck automatiskt förändringar i biblioteket", + "LabelSettingsEnableWatcher": "Bevaka biblioteken automatiskt efter ändringar", + "LabelSettingsEnableWatcherForLibrary": "Bevaka biblioteket automatiskt efter ändringar", "LabelSettingsEnableWatcherHelp": "Aktiverar automatik att upptäcka när objekt
adderas, uppdateras eller raderas.
OBS: Kräver en omstart av servern", "LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script", "LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får innehålla script.
Det rekommenderas att denna inställning är
avstängd när du inte litar på källan för epub-filerna.", @@ -724,6 +724,7 @@ "LabelViewQueue": "Visa spellista", "LabelVolume": "Volym", "LabelWebRedirectURLsDescription": "Auktorisera dessa URLer i din OAuth-leverantör för att tillåta omdirigering tillbaka till webbappen efter inloggning:", + "LabelWebRedirectURLsSubfolder": "Undermappar för omdirigeringslänkar", "LabelWeekdaysToRun": "Veckodagar att köra skanning", "LabelXBooks": "{0} böcker", "LabelXItems": "{0} objekt", @@ -752,6 +753,7 @@ "MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Sökningen gav inget resultat", "MessageBookshelfNoSeries": "Du har inga serier", + "MessageBulkChapterPattern": "Hur många kapitel vill du lägga till med detta numreringsmönster?", "MessageChapterEndIsAfter": "Kapitelns slut är efter din ljudboks slut", "MessageChapterErrorFirstNotZero": "Första kapitlet måste börja vid 0", "MessageChapterErrorStartGteDuration": "Ogiltig starttid, måste vara mindre än ljudbokens längd", @@ -778,6 +780,7 @@ "MessageConfirmMarkItemNotFinished": "Är du säker på att du vill markera \"{0}\" som ej avslutad?", "MessageConfirmMarkSeriesFinished": "Är du säker på att du vill markera alla böcker i denna serie som avslutade?", "MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som ej avslutade?", + "MessageConfirmNotificationTestTrigger": "Trigga denna avisering med testdata?", "MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen /metadata/cache att raderas.

Är du säker på att du vill radera katalogen?", "MessageConfirmPurgeItemsCache": "När du rensar cashen för objekten kommer katalogen /metadata/cache/items att raderas.

Är du säker på att du vill radera katalogen?", "MessageConfirmQuickEmbed": "VARNING! När du infogar metadata i dina ljudfiler kommer INGEN SÄKERHETSKOPIA av filerna att göras. Se därför till att först säkerhetskopiera ljudfilerna.

Vill du fortsätta?", @@ -787,6 +790,7 @@ "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", "MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?", "MessageConfirmRemoveEpisode": "Är du säker på att du vill radera avsnittet \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Anmärkning: Detta tar inte bort ljudfilen om inte \"Hard delete file\" är aktivt", "MessageConfirmRemoveEpisodes": "Är du säker på att du vill radera {0} avsnitt?", "MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?", "MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera filerna 'metadata.{0}' i alla mappar i ditt bibliotek?", @@ -804,12 +808,16 @@ "MessageDaysListenedInTheLastYear": "{0} dagars lyssnande det senaste året", "MessageDownloadingEpisode": "Laddar ner avsnitt", "MessageDragFilesIntoTrackOrder": "Ändra ordningen genom att klicka och dra filerna till rätt plats", + "MessageEmbedFailed": "Inbäddning misslyckades!", "MessageEmbedFinished": "Inbäddning genomförd!", + "MessageEmbedQueue": "Köad för inbäddning av metadata plats ({0} i kön)", "MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning", "MessageEreaderDevices": "För att säkerställa överföring av e-böcker kan du bli tvungen
att addera ovanstående e-postadress som godkänd avsändare
för varje enhet angiven nedan.", "MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}", "MessageFetching": "Hämtar...", "MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.", + "MessageHeatmapListeningTimeTooltip": "{0} lyssnar på {1}", + "MessageHeatmapNoListeningSessions": "Inga lyssningssessioner på {0}", "MessageImportantNotice": "Viktig meddelande!", "MessageInsertChapterBelow": "Infoga kapitel nedanför", "MessageInvalidAsin": "Felaktig ASIN-kod", @@ -849,6 +857,7 @@ "MessageNoLogs": "Inga loggningsinformation finns", "MessageNoMediaProgress": "Ingen medieförlopp", "MessageNoNotifications": "Inga aviseringar", + "MessageNoPodcastFeed": "Felaktig podcast: ingen ström", "MessageNoPodcastsFound": "Inga podcasts hittade", "MessageNoResults": "Inga resultat", "MessageNoSearchResultsFor": "Inga sökresultat för \"{0}\"", @@ -859,6 +868,7 @@ "MessageNoUserPlaylists": "Du har inga spellistor", "MessageNoUserPlaylistsHelp": "Spellistor är privata. Endast den användare som skapat listan kan se den.", "MessageNotYetImplemented": "Ännu inte implementerad", + "MessageOpmlPreviewNote": "Obs: Detta är en förhandsvisning av den analyserade OPML-filen. Den faktiska podcasttiteln kommer att hämtas från RSS-flödet.", "MessageOr": "eller", "MessagePauseChapter": "Pausa kapiteluppspelning", "MessagePlayChapter": "Lyssna på kapitlets början", @@ -878,7 +888,7 @@ "MessageResetChaptersConfirm": "Är du säker på att du vill återställa alla kapitel och ångra de ändringarna du gjort?", "MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den", "MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.

Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.

Alla klienter som använder din server kommer att uppdateras automatiskt.", - "MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas att denna funktion ej aktiveras. Istället bör funktionen som automatisk upptäcker ändringar i biblioteket vara aktiverad. För vissa filsystem (som t.ex. NFS) fungerar inte denna funktion. Då kan schemalagda skanningar av biblioteken användas istället.", + "MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas det att låta den här funktionen vara inaktiverad och att inställningen \"Bevaka biblioteket automatiskt efter ändringar\" är aktiverad – den kommer automatiskt att upptäcka ändringar i dina biblioteksmappar. Aktivera den här funktionen om \"Bevaka biblioteket automatiskt efter ändringar\" inte fungerar för ditt filsystem (som NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Startar varje {0} klockan {1}", "MessageSearchResultsFor": "Sökresultat för", "MessageSelected": "{0} valda", @@ -887,12 +897,14 @@ "MessageSetChaptersFromTracksDescription": "Använd varje ljudfil som ett kapitel och ljudfilens namn som kapitlets rubrik", "MessageShareExpirationWillBe": "Giltig till kommer att bli {0}", "MessageShareExpiresIn": "Upphör om {0}", + "MessageShareURLWillBe": "Delningslänken kommer att vara {0}", "MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" från tidpunkt {1}?", "MessageTaskAudioFileNotWritable": "Det går inte att skriva till ljudfilen \"{0}\"", "MessageTaskCanceledByUser": "Uppgiften avslutades av användaren", "MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"", "MessageTaskEmbeddingMetadata": "Infogar metadata", "MessageTaskEmbeddingMetadataDescription": "Infogar metadata i ljudboken \"{0}\"", + "MessageTaskEncodingM4b": "Kodar M4B", "MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil", "MessageTaskFailed": "Misslyckades", "MessageTaskFailedToBackupAudioFile": "Misslyckades med att göra backup på ljudfil \"{0}\"", @@ -903,8 +915,11 @@ "MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata", "MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"", "MessageTaskNoFilesToScan": "Inga filer finns tillgängliga för skanning", + "MessageTaskOpmlImport": "OPML import", "MessageTaskOpmlImportDescription": "Skapar podcasts från {0} RSS-flöden", + "MessageTaskOpmlImportFeed": "OPML importflöde", "MessageTaskOpmlImportFeedDescription": "Importerar RSS-flödet \"{0}\"", + "MessageTaskOpmlImportFeedFailed": "Det gick inte att hämta poddflödet", "MessageTaskOpmlImportFeedPodcastDescription": "Skapar podcast \"{0}\"", "MessageTaskOpmlImportFeedPodcastExists": "En podcast finns redan med den adressen", "MessageTaskOpmlImportFeedPodcastFailed": "Misslyckades med att skapa podcast", @@ -987,6 +1002,9 @@ "ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopian", "ToastBackupUploadSuccess": "Säkerhetskopian har laddats upp", "ToastBatchApplyDetailsToItemsSuccess": "Informationen har adderats till alla objekt", + "ToastBatchDeleteFailed": "Det gick inte att radera batch", + "ToastBatchDeleteSuccess": "Batch borttagning lyckades", + "ToastBatchQuickMatchFailed": "Snabbmatchning av batch misslyckades!", "ToastBatchQuickMatchStarted": "Snabbmatchning av {0} böcker har påbörjats!", "ToastBatchUpdateFailed": "Batchuppdateringen misslyckades", "ToastBatchUpdateSuccess": "Batchuppdateringen lyckades", @@ -1024,6 +1042,7 @@ "ToastEpisodeDownloadQueueClearFailed": "Misslyckades med att tömma kön", "ToastEpisodeDownloadQueueClearSuccess": "Kö för nedladdning av avsnitt har tömts", "ToastEpisodeUpdateSuccess": "{0} avsnitt uppdaterades", + "ToastErrorCannotShare": "Kan inte dela direkt på den här enheten", "ToastFailedToCreate": "Misslyckades med att addera", "ToastFailedToDelete": "Misslyckades med att radera", "ToastFailedToLoadData": "Misslyckades med att ladda data", @@ -1055,6 +1074,7 @@ "ToastMetadataFilesRemovedNoneFound": "Inga 'metadata.{0}' filer hittades i biblioteket", "ToastMetadataFilesRemovedNoneRemoved": "Inga 'metadata.{0}' filer raderades", "ToastMetadataFilesRemovedSuccess": "{0} 'metadata.{1}' raderades", + "ToastMustHaveAtLeastOnePath": "Måste ha minst en sökväg", "ToastNameEmailRequired": "Ett namn och en e-postadress måste anges", "ToastNameRequired": "Ett namn måste anges", "ToastNewApiKeyUserError": "En användare måste väljas", @@ -1071,8 +1091,10 @@ "ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet", "ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet", "ToastNotificationFailedMaximum": "Maximalt antal misslyckade försök måste vara större än eller lika med 0", + "ToastNotificationQueueMaximum": "Maximala antalet aviseringsköer måste vara >= 0", "ToastNotificationSettingsUpdateSuccess": "Inställningarna för meddelanden har ändrats", "ToastNotificationTestTriggerFailed": "Misslyckades med att skicka testmeddelandet", + "ToastNotificationTestTriggerSuccess": "Triggade testavisering", "ToastNotificationUpdateSuccess": "Meddelandet har uppdaterats", "ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan", "ToastPlaylistCreateSuccess": "Spellistan skapad", @@ -1081,8 +1103,10 @@ "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", "ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt", "ToastPodcastEpisodeUpdated": "Avsnittet har uppdaterats", + "ToastPodcastGetFeedFailed": "Det gick inte att hämta poddflödet", "ToastPodcastNoEpisodesInFeed": "Inga avsnitt finns i RSS-flödet", "ToastPodcastNoRssFeed": "Denna podcast har ingen RSS-flöde", + "ToastProgressIsNotBeingSynced": "Förloppet synkroniseras inte, starta om uppspelningen", "ToastProviderCreatedFailed": "Misslyckades med att addera en källa", "ToastProviderCreatedSuccess": "En ny källa har adderats", "ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs", @@ -1107,9 +1131,12 @@ "ToastSeriesUpdateFailed": "Misslyckades med att uppdatera serien", "ToastSeriesUpdateSuccess": "Uppdateringen av serierna lyckades", "ToastServerSettingsUpdateSuccess": "Inställningarna för servern har uppdaterats", + "ToastSessionCloseFailed": "Misslyckades med att avsluta sessionen", "ToastSessionDeleteFailed": "Misslyckades med att ta bort sessionen", "ToastSessionDeleteSuccess": "Sessionen borttagen", "ToastSleepTimerDone": "Timer har stängt av lyssning. Sov gott... zZzzZz", + "ToastSlugMustChange": "Slug innehåller ogiltiga tecken", + "ToastSlugRequired": "Slug krävs", "ToastSocketConnected": "Socket ansluten", "ToastSocketDisconnected": "Socket frånkopplad", "ToastSocketFailedToConnect": "Socket misslyckades med att ansluta", @@ -1120,6 +1147,7 @@ "ToastUnlinkOpenIdFailed": "Misslyckades med att koppla bort användaren från OpenID", "ToastUnlinkOpenIdSuccess": "Användaren har kopplats bort från OpenID", "ToastUploaderFilepathExistsError": "En fil med namnet \"{0}\" finns redan på servern", + "ToastUploaderItemExistsInSubdirectoryError": "Objektet \"{0}\" använder en underkatalog till uppladdningssökvägen.", "ToastUserDeleteFailed": "Misslyckades med att ta bort användaren", "ToastUserDeleteSuccess": "Användaren borttagen", "ToastUserPasswordChangeSuccess": "Lösenordet har ändrats", From f178841e575e3492e87d61f19d4dff12b0cc1943 Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Tue, 18 Nov 2025 08:43:28 +0100 Subject: [PATCH 067/285] Translated using Weblate (Swedish) Currently translated at 99.8% (1161 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index b1fa8f229..2b8a9de3e 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -915,7 +915,7 @@ "MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata", "MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"", "MessageTaskNoFilesToScan": "Inga filer finns tillgängliga för skanning", - "MessageTaskOpmlImport": "OPML import", + "MessageTaskOpmlImport": "OPML-import", "MessageTaskOpmlImportDescription": "Skapar podcasts från {0} RSS-flöden", "MessageTaskOpmlImportFeed": "OPML importflöde", "MessageTaskOpmlImportFeedDescription": "Importerar RSS-flödet \"{0}\"", From ae0ccb1b47b61f49123a5512ae054ecbe9a34761 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 19 Nov 2025 15:03:59 +0100 Subject: [PATCH 068/285] Added translation using Weblate (Korean) --- client/strings/ko.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 client/strings/ko.json diff --git a/client/strings/ko.json b/client/strings/ko.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/client/strings/ko.json @@ -0,0 +1 @@ +{} From 39b8b9df4f977e8b75814ac9e982e990659e2953 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 20 Nov 2025 16:58:43 -0600 Subject: [PATCH 069/285] Auto formatting --- client/components/stats/DailyListeningChart.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/components/stats/DailyListeningChart.vue b/client/components/stats/DailyListeningChart.vue index 1c92e675d..f3a50d11c 100644 --- a/client/components/stats/DailyListeningChart.vue +++ b/client/components/stats/DailyListeningChart.vue @@ -186,14 +186,14 @@ export default { daysInARow() { var count = 0 while (true) { - var _date = this.$addDaysToToday((count * -1) - 1) - var datestr = this.$formatJsDate(_date, 'yyyy-MM-dd') + const _date = this.$addDaysToToday(count * -1 - 1) + const datestr = this.$formatJsDate(_date, 'yyyy-MM-dd') if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) { // don't require listening today to count towards days in a row, but do count it if already listened today - var today = this.$formatJsDate(new Date(), 'yyyy-MM-dd'); + const today = this.$formatJsDate(new Date(), 'yyyy-MM-dd') if (this.listeningStatsDays[today]) { - count++; + count++ } return count From 3e9ca51088918b76d5474960570415a3043e4ee2 Mon Sep 17 00:00:00 2001 From: Finn Dittmar Date: Thu, 27 Nov 2025 21:40:29 +0100 Subject: [PATCH 070/285] Fix region parameter extraction in findChapters --- server/controllers/SearchController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index f6f0ba475..86558846d 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -187,7 +187,7 @@ class SearchController { try { const query = req.query const asin = getQueryParamAsString(query, 'asin', '', true) - const region = getQueryParamAsString(req.query.region, 'us').toLowerCase() + const region = getQueryParamAsString(query, 'region', 'us').toLowerCase() if (!isValidASIN(asin.toUpperCase())) throw new ValidationError('asin', 'is invalid') From a5d2c1bd64df6ed1c0f2a51f6b1605e85065934f Mon Sep 17 00:00:00 2001 From: Finn Dittmar Date: Sat, 29 Nov 2025 11:02:42 +0100 Subject: [PATCH 071/285] Fix Upload Lookup --- server/controllers/SearchController.js | 4 ++-- server/finders/BookFinder.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 86558846d..ecd9a41c0 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -88,10 +88,10 @@ class SearchController { const provider = getQueryParamAsString(query, 'provider', 'google') const title = getQueryParamAsString(query, 'title', '') const author = getQueryParamAsString(query, 'author', '') - const id = getQueryParamAsString(query, 'id', '', true) + const id = getQueryParamAsString(query, 'id', undefined) // Fetch library item - const libraryItem = await SearchController.fetchLibraryItem(id) + const libraryItem = id ? await SearchController.fetchLibraryItem(id) : null const results = await BookFinder.search(libraryItem, provider, title, author) res.json(results) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index fe1a61027..6d31b2ab9 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -428,7 +428,7 @@ class BookFinder { } } - if (books.length) { + if (books.length && libraryItem) { const isAudibleProvider = provider.startsWith('audible') const libraryItemDurationMinutes = libraryItem?.media?.duration ? libraryItem.media.duration / 60 : null From 552ed43243e5d4001142bb52eea0b0f3d0ba24e3 Mon Sep 17 00:00:00 2001 From: ugyes Date: Wed, 19 Nov 2025 20:18:38 +0100 Subject: [PATCH 072/285] 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/ --- client/strings/hu.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/hu.json b/client/strings/hu.json index e6d45231f..6c1f9abc9 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal", "LabelSettingsChromecastSupport": "Chromecast támogatás", "LabelSettingsDateFormat": "Dátumformátum", - "LabelSettingsEnableWatcher": "Változások automatikus vizsgálata a könyvtárakban", - "LabelSettingsEnableWatcherForLibrary": "Változások automatikus vizsgálata a könyvtárban", + "LabelSettingsEnableWatcher": "Változások automatikus figyelése a könyvtárakban", + "LabelSettingsEnableWatcherForLibrary": "Változások automatikus figyelése a könyvtárban", "LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges", "LabelSettingsEpubsAllowScriptedContent": "Szkriptelt tartalmak engedélyezése epub-okban", "LabelSettingsEpubsAllowScriptedContentHelp": "Megengedi, hogy az epub fájlok szkripteket hajtsanak végre. Ezt a beállítást kikapcsolva ajánlott tartani, kivéve, ha megbízik az epub fájlok forrásában.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Biztosan alaphelyzetbe szeretné állítani a fejezeteket és visszavonni a módosításokat?", "MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült:", "MessageRestoreBackupWarning": "A biztonsági mentés visszaállítása felülírja az egész adatbázist, amely a /config mappában található, valamint a borítóképeket a /metadata/items és /metadata/authors mappákban.

A biztonsági mentések nem módosítják a könyvtár mappáiban található fájlokat. Ha engedélyezte a szerverbeállításokat a borítóképek és a metaadatok könyvtármappákban való tárolására, akkor ezek nem kerülnek biztonsági mentésre vagy felülírásra.

A szerver használó összes kliens automatikusan frissül.", - "MessageScheduleLibraryScanNote": "A legtöbb felhasználó számára ajánlott ezt a funkciót kikapcsolva hagyni, és engedélyezni a mappafigyelő beállítást. A mappafigyelő automatikusan észleli a könyvtári mappák változásait. A mappafigyelő nem működik minden fájlrendszernél (mint például az NFS), ezért helyette ütemezett könyvtárellenőrzéseket lehet használni.", + "MessageScheduleLibraryScanNote": "A legtöbb felhasználó számára ajánlott ezt a funkciót kikapcsolva hagyni, és engedélyezni a mappafigyelő beállítást. A mappafigyelő automatikusan észleli a könyvtári mappák változásait. Kapcsolja be ezt a funkciót, ha az „Automatikus könyvtárfigyelés” nem működik a fájlrendszerén (például NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Futás minden {1} óra {0}-kor", "MessageSearchResultsFor": "Keresési eredmények", "MessageSelected": "{0} kiválasztva", From 0ec2ced0116292baa0eb6f6269fc7ccbf9dd0f38 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:50:06 +0100 Subject: [PATCH 073/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 403272dcc..8398a7211 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -588,7 +588,7 @@ "LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden", "LabelSettingsChromecastSupport": "Chromecastunterstützung", "LabelSettingsDateFormat": "Datumsformat", - "LabelSettingsEnableWatcher": "Bibliotheken automatisch nach Änderungen durchsuchen", + "LabelSettingsEnableWatcher": "Bibliotheken automatisch nach Änderungen überwachen", "LabelSettingsEnableWatcherForLibrary": "Bibliothek automatisch auf Änderungen überwachen", "LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart", "LabelSettingsEpubsAllowScriptedContent": "Skriptinhalte in Epubs zulassen", From 27843c3f9b85dfa1bfdf36b044100df6a27951ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=93=D0=BE=D1=80?= =?UTF-8?q?=D0=BF=D0=B8=D0=BD=D1=96=D1=87?= Date: Sat, 22 Nov 2025 16:55:53 +0100 Subject: [PATCH 074/285] 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/ --- client/strings/uk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/uk.json b/client/strings/uk.json index 32546fe03..1db23322f 100644 --- a/client/strings/uk.json +++ b/client/strings/uk.json @@ -383,7 +383,7 @@ "LabelFolders": "Теки", "LabelFontBold": "Жирний", "LabelFontBoldness": "Товщина шрифту", - "LabelFontFamily": "Гарнітура", + "LabelFontFamily": "Сімейство шрифтів", "LabelFontItalic": "Курсив", "LabelFontScale": "Розмір шрифту", "LabelFontStrikethrough": "Закреслений", From c2fd87d55cb0fab3b57ef3bac3534bdba52c6d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A8lia=20Garriga?= Date: Mon, 24 Nov 2025 12:16:23 +0100 Subject: [PATCH 075/285] Translated using Weblate (Catalan) Currently translated at 91.8% (1068 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ca/ --- client/strings/ca.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/strings/ca.json b/client/strings/ca.json index d882385dc..dbda8b8e4 100644 --- a/client/strings/ca.json +++ b/client/strings/ca.json @@ -1,5 +1,6 @@ { "ButtonAdd": "Afegeix", + "ButtonAddApiKey": "Afegeix clau API", "ButtonAddChapters": "Afegeix capítols", "ButtonAddDevice": "Afegeix un aparell", "ButtonAddLibrary": "Afegeix una biblioteca", @@ -20,6 +21,7 @@ "ButtonChooseAFolder": "Trieu una carpeta", "ButtonChooseFiles": "Trieu fitxers", "ButtonClearFilter": "Neteja el filtre", + "ButtonClose": "Tanca", "ButtonCloseFeed": "Tanca el canal", "ButtonCloseSession": "Tanca la sessió oberta", "ButtonCollections": "Col·leccions", @@ -119,11 +121,13 @@ "HeaderAccount": "Compte", "HeaderAddCustomMetadataProvider": "Afegeix un proveïdor de metadades personalitzat", "HeaderAdvanced": "Avançat", + "HeaderApiKeys": "Claus API", "HeaderAppriseNotificationSettings": "Paràmetres de notificacions Apprise", "HeaderAudioTracks": "Pistes d'àudio", "HeaderAudiobookTools": "Eines de gestió de fitxers de l'audiollibre", "HeaderAuthentication": "Autenticació", "HeaderBackups": "Còpies de Seguretat", + "HeaderBulkChapterModal": "Afegeix capítols múltiples", "HeaderChangePassword": "Canvia Contrasenya", "HeaderChapters": "Capítols", "HeaderChooseAFolder": "Tria una Carpeta", From ff9e87c4d57a3871d7850acd125bd9a179638225 Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Mon, 24 Nov 2025 07:38:43 +0100 Subject: [PATCH 076/285] Translated using Weblate (Swedish) Currently translated at 99.9% (1162 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index 2b8a9de3e..7bd9753b8 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -385,13 +385,13 @@ "LabelFontBoldness": "Fetstil", "LabelFontFamily": "Typsnittsfamilj", "LabelFontItalic": "Kursiv", - "LabelFontScale": "Skala på typsnitt", + "LabelFontScale": "Storlek på typsnitt", "LabelFontStrikethrough": "Genomstruken", "LabelFormat": "Format", "LabelFull": "Komplett", "LabelGenre": "Kategori", "LabelGenres": "Kategorier", - "LabelHardDeleteFile": "Hård radering av fil", + "LabelHardDeleteFile": "Permanent radering av fil", "LabelHasEbook": "Har e-bok", "LabelHasSupplementaryEbook": "Har kompletterande e-bok", "LabelHideSubtitles": "Dölj underrubriker", @@ -588,7 +588,7 @@ "LabelSettingsBookshelfViewHelp": "Bakgrund med ett utseende liknande en bokhylla i trä", "LabelSettingsChromecastSupport": "Stöd för Chromecast", "LabelSettingsDateFormat": "Datumformat", - "LabelSettingsEnableWatcher": "Bevaka biblioteken automatiskt efter ändringar", + "LabelSettingsEnableWatcher": "Bevaka automatiskt förändringar i biblioteken", "LabelSettingsEnableWatcherForLibrary": "Bevaka biblioteket automatiskt efter ändringar", "LabelSettingsEnableWatcherHelp": "Aktiverar automatik att upptäcka när objekt
adderas, uppdateras eller raderas.
OBS: Kräver en omstart av servern", "LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script", @@ -790,7 +790,7 @@ "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", "MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?", "MessageConfirmRemoveEpisode": "Är du säker på att du vill radera avsnittet \"{0}\"?", - "MessageConfirmRemoveEpisodeNote": "Anmärkning: Detta tar inte bort ljudfilen om inte \"Hard delete file\" är aktivt", + "MessageConfirmRemoveEpisodeNote": "OBSERERA: Detta tar inte bort ljudfilen om inte \"Permanent radering av fil\" är aktiverad", "MessageConfirmRemoveEpisodes": "Är du säker på att du vill radera {0} avsnitt?", "MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?", "MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera filerna 'metadata.{0}' i alla mappar i ditt bibliotek?", From 44d7deae99c44b90da0d965f2b954d01bdc1f16d Mon Sep 17 00:00:00 2001 From: FiendFEARing Date: Sun, 23 Nov 2025 09:39:59 +0100 Subject: [PATCH 077/285] 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/ --- client/strings/zh-cn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index b7e4eac30..1a95256ab 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -383,7 +383,7 @@ "LabelFolders": "文件夹", "LabelFontBold": "加粗", "LabelFontBoldness": "字体粗细", - "LabelFontFamily": "字体系列", + "LabelFontFamily": "字体", "LabelFontItalic": "斜体", "LabelFontScale": "字体比例", "LabelFontStrikethrough": "删除线", From 594589da3d015f3b54d88955e722e11faa6187de Mon Sep 17 00:00:00 2001 From: ugyes Date: Mon, 24 Nov 2025 12:23:31 +0100 Subject: [PATCH 078/285] 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/ --- client/strings/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/hu.json b/client/strings/hu.json index 6c1f9abc9..574fd1c9b 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -383,7 +383,7 @@ "LabelFolders": "Mappák", "LabelFontBold": "Félkövér", "LabelFontBoldness": "Betű vastagság", - "LabelFontFamily": "Betűtípus család", + "LabelFontFamily": "Betűcsalád", "LabelFontItalic": "Dőlt", "LabelFontScale": "Betűméret skála", "LabelFontStrikethrough": "Áthúzott", From 7393c03218be9595b6fb498c63238a6c450f9eca Mon Sep 17 00:00:00 2001 From: B0rax Date: Mon, 24 Nov 2025 07:47:31 +0100 Subject: [PATCH 079/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 8398a7211..5f7f81cc4 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -383,7 +383,7 @@ "LabelFolders": "Verzeichnisse", "LabelFontBold": "Fett", "LabelFontBoldness": "Schriftstärke", - "LabelFontFamily": "Schriftfamilie", + "LabelFontFamily": "Schrift Familie", "LabelFontItalic": "Kursiv", "LabelFontScale": "Schriftgröße", "LabelFontStrikethrough": "Durchgestrichen", From 53a416fd285f173c1c36bc2987d26cf64dfb99bc Mon Sep 17 00:00:00 2001 From: Artur Date: Mon, 24 Nov 2025 23:06:39 +0100 Subject: [PATCH 080/285] Translated using Weblate (Polish) Currently translated at 88.9% (1035 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 6f83720de..4ad7fe64f 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -76,7 +76,7 @@ "ButtonReScan": "Ponowne skanowanie", "ButtonRead": "Czytaj", "ButtonReadLess": "Pokaż mniej", - "ButtonReadMore": "Pokaż więcej", + "ButtonReadMore": "Czytaj więcej", "ButtonRefresh": "Odśwież", "ButtonRemove": "Usuń", "ButtonRemoveAll": "Usuń wszystko", @@ -96,7 +96,7 @@ "ButtonScrollRight": "Przewiń w prawo", "ButtonSearch": "Szukaj", "ButtonSelectFolderPath": "Wybierz ścieżkę folderu", - "ButtonSeries": "Seria", + "ButtonSeries": "Serial", "ButtonSetChaptersFromTracks": "Ustawiaj rozdziały na podstawie utworów", "ButtonShare": "Udostępnij", "ButtonShiftTimes": "Przesunięcie czasowe", @@ -363,7 +363,7 @@ "LabelExpiresAt": "Wygasa w", "LabelExpiresInSeconds": "Wygasa za (sekund)", "LabelExpiresNever": "Nigdy", - "LabelExplicit": "Nieprzyzwoite", + "LabelExplicit": "18+", "LabelExplicitChecked": "Nieprzyzwoite (sprawdzone)", "LabelExplicitUnchecked": "Przyzwoite (niesprawdzone)", "LabelExportOPML": "Wyeksportuj OPML", @@ -383,7 +383,7 @@ "LabelFolders": "Foldery", "LabelFontBold": "Pogrubiony", "LabelFontBoldness": "Grubość czcionki", - "LabelFontFamily": "Rodzina czcionek", + "LabelFontFamily": "Krój pisma", "LabelFontItalic": "Kursywa", "LabelFontScale": "Rozmiar czcionki", "LabelFontStrikethrough": "Przekreślony", @@ -587,7 +587,7 @@ "LabelSettingsBookshelfViewHelp": "Widok półki z książkami", "LabelSettingsChromecastSupport": "Wsparcie Chromecast", "LabelSettingsDateFormat": "Format daty", - "LabelSettingsEnableWatcher": "Automatyczne skanowanie bibliotek w poszukiwaniu zmian", + "LabelSettingsEnableWatcher": "Automatyczne monitorowanie bibliotek w poszukiwaniu zmian", "LabelSettingsEnableWatcherForLibrary": "Automatyczne skanowanie biblioteki w poszukiwaniu zmian", "LabelSettingsEnableWatcherHelp": "Włącza automatyczne dodawanie/aktualizację pozycji gdy wykryte zostaną zmiany w plikach. Wymaga restartu serwera", "LabelSettingsEpubsAllowScriptedContent": "Zezwalanie na skrypty w plikach epub", @@ -629,6 +629,7 @@ "LabelShowSubtitles": "Pokaż Napisy", "LabelSize": "Rozmiar", "LabelSleepTimer": "Wyłącznik czasowy", + "LabelSlug": "slug (części URL która identyfikuje konkretny zasób)", "LabelSortAscending": "Rosnąco", "LabelSortDescending": "Malejąco", "LabelSortPubDate": "Sortuj według daty publikacji", @@ -720,6 +721,7 @@ "LabelViewPlayerSettings": "Zobacz ustawienia odtwarzacza", "LabelViewQueue": "Wyświetlaj kolejkę odtwarzania", "LabelVolume": "Głośność", + "LabelWebRedirectURLsDescription": "Zezwól na te adresy URL w swoim dostawcy OAuth, aby umożliwić przekierowanie z powrotem do aplikacji internetowej po zalogowaniu:", "LabelWeekdaysToRun": "Dni tygodnia", "LabelXBooks": "{0} książek", "LabelXItems": "{0} elementów", @@ -740,6 +742,7 @@ "MessageBackupsLocationNoEditNote": "Uwaga: Lokalizacja kopii zapasowej jest ustawiona poprzez zmienną środowiskową i nie może być tutaj zmieniona.", "MessageBackupsLocationPathEmpty": "Ścieżka do kopii zapasowej nie może być pusta", "MessageBatchEditPopulateMapDetailsAllHelp": "Wypełnij włączone pola danymi ze wszystkich elementów. Pola z wieloma wartościami zostaną scalone.", + "MessageBatchEditPopulateMapDetailsItemHelp": "Wypełnij pola szczegółów mapy włączonej danymi z tego elementu", "MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.", "MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji", "MessageBookshelfNoCollectionsHelp": "Kolekcje są publiczne. Wszyscy użytkownicy mający dostęp do biblioteki mogą je zobaczyć.", @@ -885,6 +888,7 @@ "MessageScheduleRunEveryWeekdayAtTime": "Uruchom w każdy {0} o {1}", "MessageSearchResultsFor": "Wyniki wyszukiwania dla", "MessageSelected": "{0} wybranych", + "MessageSeriesSequenceCannotContainSpaces": "Sekwencja serii nie może zawierać spacji", "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 {0}", @@ -902,8 +906,11 @@ "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}\"", + "MessageTaskFailedToMergeAudioFiles": "Nie udało się połączyć plików audio", + "MessageTaskFailedToMoveM4bFile": "Nie udało się przenieść pliku m4b", "MessageTaskFailedToWriteMetadataFile": "Niepowodzenie zapisania pliku metadanych", "MessageTaskNoFilesToScan": "Brak plików do skanowania", + "MessageTaskOpmlImport": "Importuj OPML", "MessageTaskScanItemsAdded": "Dodano {0}", "MessageTaskScanItemsMissing": "Brakuje {0}", "MessageTaskScanItemsUpdated": "Zaktualizowano {0}", From 7a39d581a1f0dad2a3f4d38b0caedf0fdcbfcb03 Mon Sep 17 00:00:00 2001 From: Grzegorz Orlowski Date: Mon, 24 Nov 2025 23:02:45 +0100 Subject: [PATCH 081/285] Translated using Weblate (Polish) Currently translated at 88.9% (1035 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/strings/pl.json b/client/strings/pl.json index 4ad7fe64f..944492681 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -668,6 +668,7 @@ "LabelThemeDark": "Ciemny", "LabelThemeLight": "Jasny", "LabelThemeSepia": "Sepia", + "LabelTimeBase": "Podstawa czasu", "LabelTimeDurationXHours": "{0} godzin", "LabelTimeDurationXMinutes": "{0} minuty", "LabelTimeDurationXSeconds": "{0} sekundy", @@ -814,6 +815,7 @@ "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": "{0} słucha na {1}", + "MessageHeatmapNoListeningSessions": "Brak sesji słuchania na {0}", "MessageImportantNotice": "Ważna informacja!", "MessageInsertChapterBelow": "Wstaw rozdział poniżej", "MessageInvalidAsin": "Nieprawidłowy ASIN", From be27908d44d56e470bc89fe28a5e69871b1fa3e5 Mon Sep 17 00:00:00 2001 From: Plazec Date: Mon, 24 Nov 2025 23:28:12 +0100 Subject: [PATCH 082/285] Translated using Weblate (Czech) Currently translated at 99.5% (1158 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/ --- client/strings/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index 2d07ce4ef..4b20c2a59 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -392,7 +392,7 @@ "LabelGenre": "Žánr", "LabelGenres": "Žánry", "LabelHardDeleteFile": "Trvale smazat soubor", - "LabelHasEbook": "Obsahuje elektronickou knihu", + "LabelHasEbook": "Má e-knihu", "LabelHasSupplementaryEbook": "Obsahuje doplňkovou e-knihu", "LabelHideSubtitles": "Skrýt titulky", "LabelHighestPriority": "Nejvyšší priorita", From 623c2fba1248935df7d2b3a62e2816fc1679e0e5 Mon Sep 17 00:00:00 2001 From: zard Kim Date: Wed, 26 Nov 2025 09:28:52 +0100 Subject: [PATCH 083/285] Translated using Weblate (Korean) 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/ko/ --- client/strings/ko.json | 1166 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 1165 insertions(+), 1 deletion(-) diff --git a/client/strings/ko.json b/client/strings/ko.json index 0967ef424..a4dea5de4 100644 --- a/client/strings/ko.json +++ b/client/strings/ko.json @@ -1 +1,1165 @@ -{} +{ + "ButtonAdd": "추가", + "ButtonAddApiKey": "API Key 추가", + "ButtonAddChapters": "챕터 추가", + "ButtonAddDevice": "기기 추가", + "ButtonAddLibrary": "라이브러리 추가", + "ButtonAddPodcasts": "팟케스트 추가", + "ButtonAddUser": "사용자 추가", + "ButtonAddYourFirstLibrary": "첫 라이브러리 추가", + "ButtonApply": "적용", + "ButtonApplyChapters": "챕터 적용", + "ButtonAuthors": "작가", + "ButtonBack": "뒤로", + "ButtonBatchEditPopulateFromExisting": "기존에서 채우기", + "ButtonBatchEditPopulateMapDetails": "지도 세부 정보 채우기", + "ButtonBrowseForFolder": "폴더 찾아보기", + "ButtonCancel": "취소", + "ButtonCancelEncode": "인코딩 취소", + "ButtonChangeRootPassword": "루트 비밀번호 변경", + "ButtonCheckAndDownloadNewEpisodes": "새로운 에피소드 확인 및 다운로드", + "ButtonChooseAFolder": "폴더를 선택", + "ButtonChooseFiles": "파일 선택", + "ButtonClearFilter": "필터 지우기", + "ButtonClose": "닫기", + "ButtonCloseFeed": "피드 닫기", + "ButtonCloseSession": "열린 세션 닫기", + "ButtonCollections": "컬렉션", + "ButtonConfigureScanner": "스캐너 구성", + "ButtonCreate": "만들기", + "ButtonCreateBackup": "백업 만들기", + "ButtonDelete": "삭제", + "ButtonDownloadQueue": "대기줄", + "ButtonEdit": "편집", + "ButtonEditChapters": "챕터 편집", + "ButtonEditPodcast": "팟캐스트 편집", + "ButtonEnable": "활성화", + "ButtonFireAndFail": "Fire and Fail", + "ButtonFireOnTest": "Fire onTest 이벤트", + "ButtonForceReScan": "강제 재스캔", + "ButtonFullPath": "전체 경로", + "ButtonHide": "숨기기", + "ButtonHome": "홈", + "ButtonIssues": "이슈", + "ButtonJumpBackward": "뒤로 점프", + "ButtonJumpForward": "앞으로 점프", + "ButtonLatest": "최신", + "ButtonLibrary": "라이브러리", + "ButtonLogout": "로그아웃", + "ButtonLookup": "Lookup", + "ButtonManageTracks": "트랙 관리", + "ButtonMapChapterTitles": "지도 장 제목", + "ButtonMatchAllAuthors": "모든 저자와 일치", + "ButtonMatchBooks": "일치한 책", + "ButtonNevermind": "괜찮아요", + "ButtonNext": "다음", + "ButtonNextChapter": "다음 장", + "ButtonNextItemInQueue": "대기열의 다음 항목", + "ButtonOk": "확인", + "ButtonOpenFeed": "피드 열기", + "ButtonOpenManager": "오픈 매니저", + "ButtonPause": "일시정지", + "ButtonPlay": "재생", + "ButtonPlayAll": "모두 재생", + "ButtonPlaying": "재생중", + "ButtonPlaylists": "재생목록", + "ButtonPrevious": "이전", + "ButtonPreviousChapter": "이전 챕터", + "ButtonProbeAudioFile": "Probe 오디오 파일", + "ButtonPurgeAllCache": "모든 캐시 삭제", + "ButtonPurgeItemsCache": "항목 캐시 삭제", + "ButtonQueueAddItem": "대기열에 추가", + "ButtonQueueRemoveItem": "대기열에서 제거", + "ButtonQuickEmbed": "빠른 임베드", + "ButtonQuickEmbedMetadata": "빠른 임베드 메타데이터", + "ButtonQuickMatch": "퀵 매치", + "ButtonReScan": "재스캔", + "ButtonRead": "읽기", + "ButtonReadLess": "덜 읽기", + "ButtonReadMore": "더 읽기", + "ButtonRefresh": "새로고침", + "ButtonRemove": "제거", + "ButtonRemoveAll": "모두 제거", + "ButtonRemoveAllLibraryItems": "모든 라이브러리 항목 제거", + "ButtonRemoveFromContinueListening": "계속 듣기에서 제거", + "ButtonRemoveFromContinueReading": "계속 읽기에서 제거", + "ButtonRemoveSeriesFromContinueSeries": "계속 시리즈에서 시리즈 제거", + "ButtonReset": "리셋", + "ButtonResetToDefault": "기본값으로 재설정", + "ButtonRestore": "복구", + "ButtonSave": "저장", + "ButtonSaveAndClose": "저장 후 닫기", + "ButtonSaveTracklist": "트랙리스트 저장", + "ButtonScan": "스캔", + "ButtonScanLibrary": "라이브러리 스캔", + "ButtonScrollLeft": "왼쪽 스크롤", + "ButtonScrollRight": "오른쪽 스크롤", + "ButtonSearch": "검색", + "ButtonSelectFolderPath": "폴더 경로 선택", + "ButtonSeries": "시리즈", + "ButtonSetChaptersFromTracks": "트랙에서 챕터 설정", + "ButtonShare": "공유", + "ButtonShiftTimes": "시간 이동", + "ButtonShow": "보기", + "ButtonStartM4BEncode": "M4B 인코딩 시작", + "ButtonStartMetadataEmbed": "메타데이터 삽입 시작", + "ButtonStats": "통계", + "ButtonSubmit": "제출", + "ButtonTest": "테스트", + "ButtonUnlinkOpenId": "OpenID 연결 해제", + "ButtonUpload": "업로드", + "ButtonUploadBackup": "백업 업로드", + "ButtonUploadCover": "커버 업로드", + "ButtonUploadOPMLFile": "OPML 파일 업로드", + "ButtonUserDelete": "사용자 {0} 삭제", + "ButtonUserEdit": "사용자 {0} 편집", + "ButtonViewAll": "모두 보기", + "ButtonYes": "예", + "ErrorUploadFetchMetadataAPI": "메타데이터를 가져오는 중 오류가 발생했습니다.", + "ErrorUploadFetchMetadataNoResults": "메타데이터를 가져올 수 없습니다. 제목 및/또는 작성자를 업데이트해 보세요.", + "ErrorUploadLacksTitle": "제목이 있어야 합니다", + "HeaderAccount": "계정", + "HeaderAddCustomMetadataProvider": "사용자 정의 메타데이터 공급자 추가", + "HeaderAdvanced": "고급", + "HeaderApiKeys": "API 키", + "HeaderAppriseNotificationSettings": "Apprise 알림 설정", + "HeaderAudioTracks": "오디오 트랙", + "HeaderAudiobookTools": "오디오북 파일 관리 도구", + "HeaderAuthentication": "인증", + "HeaderBackups": "백업", + "HeaderBulkChapterModal": "여러 챕터 추가", + "HeaderChangePassword": "비밀번호 변경", + "HeaderChapters": "챕터", + "HeaderChooseAFolder": "폴더 선택", + "HeaderCollection": "컬렉션", + "HeaderCollectionItems": "컬렉션 항목", + "HeaderCover": "커버", + "HeaderCurrentDownloads": "현재 다운로드", + "HeaderCustomMessageOnLogin": "로그인시 사용자 정의 메시지", + "HeaderCustomMetadataProviders": "사용자 정의 메타데이터 공급자", + "HeaderDetails": "세부사항", + "HeaderDownloadQueue": "다운로드 대기열", + "HeaderEbookFiles": "전자책 파일", + "HeaderEmail": "이메일", + "HeaderEmailSettings": "이메일 설정", + "HeaderEpisodes": "에피소드", + "HeaderEreaderDevices": "전자책 리더기", + "HeaderEreaderSettings": "전자책 리더 설정", + "HeaderFiles": "파일", + "HeaderFindChapters": "챕터 찾기", + "HeaderIgnoredFiles": "무시된 파일", + "HeaderItemFiles": "항목 파일", + "HeaderItemMetadataUtils": "항목 메타데이터 유틸리티", + "HeaderLastListeningSession": "마지막 청취 세션", + "HeaderLatestEpisodes": "최신 에피소드", + "HeaderLibraries": "라이브러리", + "HeaderLibraryFiles": "라이브러리 파일", + "HeaderLibraryStats": "라이브러리 통계", + "HeaderListeningSessions": "청취 세션", + "HeaderListeningStats": "청취 통계", + "HeaderLogin": "로그인", + "HeaderLogs": "로그", + "HeaderManageGenres": "장르 관리", + "HeaderManageTags": "태그 관리", + "HeaderMapDetails": "지도 세부 정보", + "HeaderMatch": "일치", + "HeaderMetadataOrderOfPrecedence": "메타데이터 우선 순위", + "HeaderMetadataToEmbed": "포함할 메타데이터", + "HeaderNewAccount": "새 계정", + "HeaderNewApiKey": "새로운 API 키", + "HeaderNewLibrary": "새 라이브러리", + "HeaderNotificationCreate": "알림 생성", + "HeaderNotificationUpdate": "업데이트 알림", + "HeaderNotifications": "알림", + "HeaderOpenIDConnectAuthentication": "OpenID Connect 인증", + "HeaderOpenListeningSessions": "청취 세션 열기", + "HeaderOpenRSSFeed": "RSS 피드 열기", + "HeaderOtherFiles": "기타 파일", + "HeaderPasswordAuthentication": "비밀번호 인증", + "HeaderPermissions": "권한", + "HeaderPlayerQueue": "플레이어 대기열", + "HeaderPlayerSettings": "플레이어 설정", + "HeaderPlaylist": "재생목록", + "HeaderPlaylistItems": "재생 목록 항목", + "HeaderPodcastsToAdd": "추가할 팟캐스트", + "HeaderPresets": "사전 설정", + "HeaderPreviewCover": "커버 미리보기", + "HeaderRSSFeedGeneral": "RSS 자세히", + "HeaderRSSFeedIsOpen": "RSS 피드가 열려 있습니다", + "HeaderRSSFeeds": "RSS 피드", + "HeaderRemoveEpisode": "에피소드 제거", + "HeaderRemoveEpisodes": "{0}개 에피소드 제거", + "HeaderSavedMediaProgress": "저장된 미디어 진행 상황", + "HeaderSchedule": "일정", + "HeaderScheduleEpisodeDownloads": "에피소드 자동 다운로드 일정 설정", + "HeaderScheduleLibraryScans": "라이브러리 자동 스캔 일정", + "HeaderSession": "세션", + "HeaderSetBackupSchedule": "백업 일정 설정", + "HeaderSettings": "설정", + "HeaderSettingsDisplay": "디스플레이", + "HeaderSettingsExperimental": "실험적 특징", + "HeaderSettingsGeneral": "일반", + "HeaderSettingsScanner": "스캐너", + "HeaderSettingsSecurity": "보안", + "HeaderSettingsWebClient": "웹 클라이언트", + "HeaderSleepTimer": "슬립 타이머", + "HeaderStatsLargestItems": "가장 큰 항목", + "HeaderStatsLongestItems": "가장 긴 항목(시간)", + "HeaderStatsMinutesListeningChart": "청취 시간(지난 7일)", + "HeaderStatsRecentSessions": "최근 세션", + "HeaderStatsTop10Authors": "상위 10명의 작가", + "HeaderStatsTop5Genres": "상위 5개 장르", + "HeaderTableOfContents": "목차", + "HeaderTools": "도구", + "HeaderUpdateAccount": "계정 업데이트", + "HeaderUpdateApiKey": "API 키 업데이트", + "HeaderUpdateAuthor": "작가 업데이트", + "HeaderUpdateDetails": "업데이트 세부 정보", + "HeaderUpdateLibrary": "라이브러리 업데이트y", + "HeaderUsers": "사용자", + "HeaderYearReview": "{0}년안의 리뷰", + "HeaderYourStats": "내 통계", + "LabelAbridged": "요약", + "LabelAbridgedChecked": "요약(확인됨)", + "LabelAbridgedUnchecked": "요약되지 않음(검토되지 않음)", + "LabelAccessibleBy": "접근 가능한", + "LabelAccountType": "계정 유형", + "LabelAccountTypeAdmin": "관리자", + "LabelAccountTypeGuest": "손님", + "LabelAccountTypeUser": "사용자", + "LabelActivities": "활동", + "LabelActivity": "활동적인", + "LabelAddToCollection": "컬렉션에 추가", + "LabelAddToCollectionBatch": "컬렉션에 {0}권의 책 추가", + "LabelAddToPlaylist": "재생목록에 추가", + "LabelAddToPlaylistBatch": "재생 목록에 {0}개 항목 추가", + "LabelAddedAt": "추가됨", + "LabelAddedDate": "{0} 추가됨", + "LabelAdminUsersOnly": "관리자 사용자만", + "LabelAll": "모두", + "LabelAllEpisodesDownloaded": "모든 에피소드 다운로드됨", + "LabelAllUsers": "모든 사용자", + "LabelAllUsersExcludingGuests": "게스트를 제외한 모든 사용자", + "LabelAllUsersIncludingGuests": "게스트를 포함한 모든 사용자", + "LabelAlreadyInYourLibrary": "이미 내 라이브러리에 있습니다", + "LabelApiKeyCreated": "API 키 \"{0}\"이(가) 성공적으로 생성되었습니다.", + "LabelApiKeyCreatedDescription": "API 키를 지금 복사해 두세요. 다시 볼 수 없게 됩니다.", + "LabelApiKeyUser": "사용자를 대신하여 행동", + "LabelApiKeyUserDescription": "이 API 키는 해당 API 키를 대신하는 사용자와 동일한 권한을 갖습니다. 이는 사용자가 요청을 하는 것과 동일하게 로그에 기록됩니다.", + "LabelApiToken": "API 토큰", + "LabelAppend": "첨부", + "LabelAudioBitrate": "오디오 비트레이트(예: 128k)", + "LabelAudioChannels": "오디오 채널(1 또는 2)", + "LabelAudioCodec": "오디오 코덱", + "LabelAuthor": "작가", + "LabelAuthorFirstLast": "작가 (이름 성)", + "LabelAuthorLastFirst": "작가 (성, 이름)", + "LabelAuthors": "작가", + "LabelAutoDownloadEpisodes": "에피소드 자동 다운로드", + "LabelAutoFetchMetadata": "메타데이터 자동 가져오기", + "LabelAutoFetchMetadataHelp": "업로드를 간소화하기 위해 제목, 저자, 시리즈에 대한 메타데이터를 가져옵니다. 업로드 후 추가 메타데이터를 일치시켜야 할 수도 있습니다.", + "LabelAutoLaunch": "자동 실행", + "LabelAutoLaunchDescription": "로그인 페이지로 이동할 때 자동으로 인증 공급자로 리디렉션합니다(수동 재정의 경로 /login?autoLaunch=0)", + "LabelAutoRegister": "자동 등록", + "LabelAutoRegisterDescription": "로그인 후 자동으로 새 사용자를 생성합니다.", + "LabelBackToUser": "사용자에게 돌아가기", + "LabelBackupAudioFiles": "오디오 파일 백업", + "LabelBackupLocation": "백업 위치", + "LabelBackupsEnableAutomaticBackups": "자동 백업", + "LabelBackupsEnableAutomaticBackupsHelp": "/metadata/backups에 저장된 백업", + "LabelBackupsMaxBackupSize": "최대 백업 크기(GB) (무제한의 경우 0)", + "LabelBackupsMaxBackupSizeHelp": "잘못된 구성에 대한 보호 조치로, 구성된 크기를 초과하면 백업이 실패합니다.", + "LabelBackupsNumberToKeep": "보관할 백업 수", + "LabelBackupsNumberToKeepHelp": "한 번에 1개의 백업만 제거되므로 이미 이보다 많은 백업이 있는 경우 수동으로 제거해야 합니다.", + "LabelBitrate": "비트레이트", + "LabelBonus": "보너스", + "LabelBooks": "전자책", + "LabelButtonText": "버튼 텍스트", + "LabelByAuthor": "by {0}", + "LabelChangePassword": "비밀번호 변경", + "LabelChannels": "채널", + "LabelChapterCount": "{0} 챕터", + "LabelChapterTitle": "챕터 제목", + "LabelChapters": "챕터", + "LabelChaptersFound": "챕터 발견됨", + "LabelClickForMoreInfo": "자세한 내용을 보려면 클릭", + "LabelClickToUseCurrentValue": "현재 값을 사용하려면 클릭", + "LabelClosePlayer": "플레이어 닫기", + "LabelCodec": "코덱", + "LabelCollapseSeries": "시리즈 펴기", + "LabelCollapseSubSeries": "하위 시리즈 축소", + "LabelCollection": "컬렉션", + "LabelCollections": "컬렉션", + "LabelComplete": "완료", + "LabelConfirmPassword": "비밀번호 확인", + "LabelContinueListening": "계속 듣기", + "LabelContinueReading": "계속 읽기", + "LabelContinueSeries": "시리즈 계속하기", + "LabelCorsAllowed": "허용된 CORS Origins", + "LabelCover": "커버", + "LabelCoverImageURL": "커버 이미지 URL", + "LabelCoverProvider": "커버 제공자", + "LabelCreatedAt": "생성일", + "LabelCronExpression": "Cron Expression", + "LabelCurrent": "현재재", + "LabelCurrently": "현재:", + "LabelCustomCronExpression": "사용자 정의 Cron 표현식:", + "LabelDatetime": "일시", + "LabelDays": "일", + "LabelDeleteFromFileSystemCheckbox": "파일 시스템에서 삭제(데이터베이스에서만 제거하려면 선택 취소)", + "LabelDescription": "설명", + "LabelDeselectAll": "모두 선택 해제", + "LabelDetectedPattern": "감지된 패턴:", + "LabelDevice": "장치", + "LabelDeviceInfo": "장치 정보", + "LabelDeviceIsAvailableTo": "사용할 수 있는 장치...", + "LabelDirectory": "디렉토리", + "LabelDiscFromFilename": "파일 이름의 디스크", + "LabelDiscFromMetadata": "메타데이터의 디스크", + "LabelDiscover": "발견", + "LabelDownload": "다운로드", + "LabelDownloadNEpisodes": "{0}개 에피소드 다운로드", + "LabelDownloadable": "다운로드 가능", + "LabelDuration": "Duration", + "LabelDurationComparisonExactMatch": "(정확히 일치)", + "LabelDurationComparisonLonger": "({0} 더 길음)", + "LabelDurationComparisonShorter": "({0} 더 짧음)", + "LabelDurationFound": "찾은 기간:", + "LabelEbook": "전자책", + "LabelEbooks": "전자책", + "LabelEdit": "편집", + "LabelEmail": "이메일", + "LabelEmailSettingsFromAddress": "보낸 사람 주소", + "LabelEmailSettingsRejectUnauthorized": "승인되지 않은 인증서 거부", + "LabelEmailSettingsRejectUnauthorizedHelp": "SSL 인증서 유효성 검사를 비활성화하면 중간자 공격과 같은 보안 위험에 노출될 수 있습니다. 이 옵션의 의미를 이해하고 연결하려는 메일 서버를 신뢰할 수 있는 경우에만 비활성화하세요.", + "LabelEmailSettingsSecure": "보안", + "LabelEmailSettingsSecureHelp": "true이면 서버에 연결할 때 TLS를 사용합니다. false이면 서버가 STARTTLS 확장을 지원하는 경우 TLS가 사용됩니다. 대부분의 경우 포트 465에 연결하는 경우 이 값을 true로 설정하고, 포트 587 또는 25를 사용하는 경우 false로 설정합니다. (nodemailer.com/smtp/#authentication에서 발췌)", + "LabelEmailSettingsTestAddress": "테스트 주소", + "LabelEmbeddedCover": "내장형 커버", + "LabelEnable": "활성화", + "LabelEncodingBackupLocation": "원본 오디오 파일의 백업이 저장되는 위치 :", + "LabelEncodingChaptersNotEmbedded": "다중 트랙 오디오북에는 챕터가 포함되지 않습니다..", + "LabelEncodingClearItemCache": "주기적으로 항목 캐시를 삭제하세요.", + "LabelEncodingFinishedM4B": "완성된 M4B는 오디오북 저장 폴더 위치:", + "LabelEncodingInfoEmbedded": "메타데이터는 오디오북 폴더 내의 오디오 트랙에 포함됩니다.", + "LabelEncodingStartedNavigation": "작업이 시작되면 이 페이지에서 벗어날 수 있습니다.", + "LabelEncodingTimeWarning": "인코딩은 최대 30분이 걸릴 수 있습니다.", + "LabelEncodingWarningAdvancedSettings": "경고: ffmpeg 인코딩 옵션에 익숙하지 않은 경우 이러한 설정을 업데이트하지 마세요.", + "LabelEncodingWatcherDisabled": "시청 기능을 비활성화한 경우 나중에 오디오북을 다시 스캔해야 합니다.", + "LabelEnd": "마지막", + "LabelEndOfChapter": "챕터의 마지작", + "LabelEpisode": "에피소드", + "LabelEpisodeNotLinkedToRssFeed": "에피소드가 RSS 피드에 연결되지 않았습니다.", + "LabelEpisodeNumber": "에피소드 #{0}", + "LabelEpisodeTitle": "에피소드 제목", + "LabelEpisodeType": "에피소드 유형", + "LabelEpisodeUrlFromRssFeed": "RSS 피드의 에피소드 URL", + "LabelEpisodes": "에피소드", + "LabelEpisodic": "에피소드식", + "LabelExample": "예제", + "LabelExpandSeries": "시리즈 확장", + "LabelExpandSubSeries": "하위 시리즈 확장", + "LabelExpired": "만료됨", + "LabelExpiresAt": "만료일", + "LabelExpiresInSeconds": "(초) 후에 만료됩니다", + "LabelExpiresNever": "Never", + "LabelExplicit": "명백한", + "LabelExplicitChecked": "명시적(확인됨)", + "LabelExplicitUnchecked": "명시적이지 않음(체크되지 않음)", + "LabelExportOPML": "OPML 내보내기", + "LabelFeedURL": "피드 URL", + "LabelFetchingMetadata": "메타데이터 가져오는중", + "LabelFile": "파일", + "LabelFileBirthtime": "파일 생년월일", + "LabelFileBornDate": "생성된 {0}", + "LabelFileModified": "파일 수정됨", + "LabelFileModifiedDate": "수정된 {0}", + "LabelFilename": "파일 이름", + "LabelFilterByUser": "사용자별 필터링", + "LabelFindEpisodes": "에피소드 찾기", + "LabelFinished": "완료됨", + "LabelFinishedDate": "완료 {0}", + "LabelFolder": "폴더", + "LabelFolders": "폴더", + "LabelFontBold": "진하게", + "LabelFontBoldness": "글꼴 굵기", + "LabelFontFamily": "글꼴 모양", + "LabelFontItalic": "이탤릭체", + "LabelFontScale": "글꼴 크기", + "LabelFontStrikethrough": "취소선", + "LabelFormat": "형식", + "LabelFull": "전체", + "LabelGenre": "장르", + "LabelGenres": "장르", + "LabelHardDeleteFile": "파일 하드 삭제", + "LabelHasEbook": "보유한 전자책", + "LabelHasSupplementaryEbook": "보유한 보충 전자책", + "LabelHideSubtitles": "자막 숨기기", + "LabelHighestPriority": "가장 높은 우선순위", + "LabelHost": "호스트", + "LabelHour": "시간", + "LabelHours": "시간", + "LabelIcon": "아이콘", + "LabelImageURLFromTheWeb": "웹의 이미지 URL", + "LabelInProgress": "진행 중", + "LabelIncludeInTracklist": "트랙리스트에 포함", + "LabelIncomplete": "불완전한", + "LabelInterval": "간격", + "LabelIntervalCustomDailyWeekly": "사용자 정의 일일/주간", + "LabelIntervalEvery12Hours": "12시간마다", + "LabelIntervalEvery15Minutes": "15분마다", + "LabelIntervalEvery2Hours": "2시간마다", + "LabelIntervalEvery30Minutes": "30분마다", + "LabelIntervalEvery6Hours": "6시간마다", + "LabelIntervalEveryDay": "매일", + "LabelIntervalEveryHour": "매 시간마다", + "LabelIntervalEveryMinute": "매 분마다", + "LabelInvert": "반전", + "LabelItem": "항목", + "LabelJumpBackwardAmount": "뒤로 점프", + "LabelJumpForwardAmount": "앞으로 점프", + "LabelLanguage": "언어", + "LabelLanguageDefaultServer": "기본 서버 언어", + "LabelLanguages": "언어", + "LabelLastBookAdded": "마지막으로 추가된 책", + "LabelLastBookUpdated": "마지막 책 업데이트됨", + "LabelLastProgressDate": "마지막 진행 상황: {0}", + "LabelLastSeen": "마지막으로 본", + "LabelLastTime": "지난번", + "LabelLastUpdate": "마지막 업데이트", + "LabelLayout": "레이아웃", + "LabelLayoutSinglePage": "단일 페이지", + "LabelLayoutSplitPage": "분할 페이지", + "LabelLess": "Less", + "LabelLibrariesAccessibleToUser": "사용자가 접근 가능한 라이브러리", + "LabelLibrary": "라이브러리", + "LabelLibraryFilterSublistEmpty": "{0} 없음", + "LabelLibraryItem": "라이브러리 항목", + "LabelLibraryName": "라이브러리 이름", + "LabelLibrarySortByProgress": "Progress: 마지막 업데이트", + "LabelLibrarySortByProgressFinished": "진행 상황: 완료", + "LabelLibrarySortByProgressStarted": "진행 상황: 시작됨", + "LabelLimit": "한계", + "LabelLineSpacing": "줄 간격", + "LabelListenAgain": "다시 듣기", + "LabelLogLevelDebug": "디버그", + "LabelLogLevelInfo": "정보", + "LabelLogLevelWarn": "경고", + "LabelLookForNewEpisodesAfterDate": "이 날짜 이후의 새로운 에피소드를 찾아보세요", + "LabelLowestPriority": "가장 낮은 우선순위", + "LabelMatchConfidence": "신뢰", + "LabelMatchExistingUsersBy": "기존 사용자와 일치시키기", + "LabelMatchExistingUsersByDescription": "기존 사용자 연결에 사용됩니다. 연결되면 사용자는 SSO 제공업체의 고유 ID와 매칭됩니다.", + "LabelMaxEpisodesToDownload": "다운로드 가능한 최대 # 에피소드 수. 무제한을 원하시면 0을 사용하세요.", + "LabelMaxEpisodesToDownloadPerCheck": "체크당 다운로드 가능한 최대 신규 # 에피소드 수", + "LabelMaxEpisodesToKeep": "보관할 최대 # 에피소드 수", + "LabelMaxEpisodesToKeepHelp": "값이 0이면 최대 제한이 없습니다. 새 에피소드가 자동으로 다운로드된 후, 에피소드가 X개 이상인 경우 가장 오래된 에피소드부터 삭제됩니다. 새로 다운로드할 때마다 에피소드 1개만 삭제됩니다.", + "LabelMediaPlayer": "미디어 플레이어", + "LabelMediaType": "미디어 유형", + "LabelMetaTag": "메타 태그", + "LabelMetaTags": "메타 태그", + "LabelMetadataOrderOfPrecedenceDescription": "우선순위가 높은 메타데이터 소스는 우선순위가 낮은 메타데이터 소스를 재정의합니다.", + "LabelMetadataProvider": "메타데이터 공급자", + "LabelMinute": "분", + "LabelMinutes": "분", + "LabelMissing": "누락", + "LabelMissingEbook": "전자책이 없습니다", + "LabelMissingSupplementaryEbook": "보충 전자책이 없습니다", + "LabelMobileRedirectURIs": "허용된 모바일 리디렉션 URI", + "LabelMobileRedirectURIsDescription": "모바일 앱에 유효한 리디렉션 URI 허용 목록입니다. 기본 허용 목록은 audiobookshelf://oauth이며, 타사 앱 통합을 위해 추가 URI로 보완하거나 삭제할 수 있습니다. 별표(*)만 사용하면 모든 URI가 허용됩니다.", + "LabelMore": "더보기", + "LabelMoreInfo": "정보 더 보기", + "LabelName": "이름", + "LabelNarrator": "내레이터", + "LabelNarrators": "내레이터", + "LabelNew": "신규", + "LabelNewPassword": "새 비밀번호", + "LabelNewestAuthors": "최신 작가", + "LabelNewestEpisodes": "최신 에피소드", + "LabelNextBackupDate": "다음 백업 날짜", + "LabelNextChapters": "다음장은 다음과 같음:", + "LabelNextScheduledRun": "다음 예정된 실행", + "LabelNoApiKeys": "API 키 없음", + "LabelNoCustomMetadataProviders": "사용자 정의 메타데이터 공급자 없음", + "LabelNoEpisodesSelected": "선택된 에피소드가 없습니다", + "LabelNotFinished": "완료되지 않음", + "LabelNotStarted": "시작되지 않음", + "LabelNotes": "노트", + "LabelNotificationAppriseURL": "Apprise URL(s)", + "LabelNotificationAvailableVariables": "사용 가능한 변수", + "LabelNotificationBodyTemplate": "본문 템플릿", + "LabelNotificationEvent": "알림 이벤트", + "LabelNotificationTitleTemplate": "제목 템플릿", + "LabelNotificationsMaxFailedAttempts": "최대 실패 시도 횟수", + "LabelNotificationsMaxFailedAttemptsHelp": "이 횟수만큼 알림을 보내지 못하면 알림이 비활성화됩니다.", + "LabelNotificationsMaxQueueSize": "알림 이벤트에 대한 최대 대기열 크기", + "LabelNotificationsMaxQueueSizeHelp": "이벤트는 초당 1회로 제한됩니다. 대기열 크기가 최대치에 도달하면 이벤트가 무시됩니다. 이를 통해 알림 스팸을 방지할 수 있습니다.", + "LabelNumberOfBooks": "전자책의 수", + "LabelNumberOfChapters": "챕터의 수:", + "LabelNumberOfEpisodes": "# 의 에피소드", + "LabelOpenIDAdvancedPermsClaimDescription": "애플리케이션 내 사용자 작업에 대한 고급 권한을 포함하는 OpenID 클레임의 이름입니다. 이 권한은 관리자가 아닌 역할(구성된 경우)에 적용됩니다. 응답에서 클레임이 누락되면 ABS에 대한 액세스가 거부됩니다. 단일 옵션이 누락되면 false로 처리됩니다. ID 공급자의 클레임이 예상 구조와 일치하는지 확인하세요.", + "LabelOpenIDClaims": "고급 그룹 및 권한 할당을 비활성화하고 자동으로 '사용자' 그룹을 할당하려면 다음 옵션을 비워 두세요.", + "LabelOpenIDGroupClaimDescription": "사용자 그룹 목록이 포함된 OpenID 클레임의 이름입니다. 일반적으로 그룹이라고 합니다. 구성된 경우, 애플리케이션은 사용자의 그룹 멤버십을 기반으로 역할을 자동으로 할당합니다. 단, 클레임에서 그룹 이름이 대소문자를 구분하지 않고 'admin', 'user' 또는 'guest'로 지정되어야 합니다. 클레임에는 목록이 포함되어야 하며, 사용자가 여러 그룹에 속하는 경우 애플리케이션은 가장 높은 수준의 액세스 권한에 해당하는 역할을 할당합니다. 일치하는 그룹이 없으면 액세스가 거부됩니다.", + "LabelOpenRSSFeed": "RSS 피드 열기", + "LabelOverwrite": "덮어쓰기", + "LabelPaginationPageXOfY": "{1} 중 {0}페이지", + "LabelPassword": "비밀번호", + "LabelPath": "경로", + "LabelPermanent": "영구적인", + "LabelPermissionsAccessAllLibraries": "모든 라이브러리에 접속 가능", + "LabelPermissionsAccessAllTags": "모든 태그에 액세스 가능", + "LabelPermissionsAccessExplicitContent": "명시적 콘텐츠에 액세스할 수 있음", + "LabelPermissionsCreateEreader": "전자책 리더기를 만들기 가능", + "LabelPermissionsDelete": "삭제 가능", + "LabelPermissionsDownload": "다운로드 가능", + "LabelPermissionsUpdate": "엡데이트 가능", + "LabelPermissionsUpload": "업로드 가능", + "LabelPersonalYearReview": "올해의 리뷰 ({0})", + "LabelPhotoPathURL": "사진 경로/URL", + "LabelPlayMethod": "플레이 방법", + "LabelPlaybackRateIncrementDecrement": "재생 속도 증가/감소량", + "LabelPlayerChapterNumberMarker": "{0} of {1}", + "LabelPlaylists": "재생목록", + "LabelPodcast": "팟케스트", + "LabelPodcastSearchRegion": "팟캐스트 검색 영역", + "LabelPodcastType": "팟캐스트 유형", + "LabelPodcasts": "팟케스트", + "LabelPort": "포트", + "LabelPrefixesToIgnore": "무시할 접두사(대소문자 구분 없음)", + "LabelPreventIndexing": "iTunes 및 Google Podcast 디렉토리에서 피드가 인덱싱되는 것을 방지합니다.", + "LabelPrimaryEbook": "기본 전자책", + "LabelProgress": "진행", + "LabelProvider": "공급자", + "LabelProviderAuthorizationValue": "권한 헤더 값", + "LabelPubDate": "출판 날짜", + "LabelPublishYear": "출판 연도", + "LabelPublishedDate": "출판됨 {0}", + "LabelPublishedDecade": "출판된지 10년", + "LabelPublishedDecades": "출판된지 10년이상", + "LabelPublisher": "출판사", + "LabelPublishers": "출판사", + "LabelRSSFeedCustomOwnerEmail": "사용자 정의 이메일", + "LabelRSSFeedCustomOwnerName": "사용자 정의 이름", + "LabelRSSFeedOpen": "RSS 피드 열기", + "LabelRSSFeedPreventIndexing": "인덱싱 방지", + "LabelRSSFeedSlug": "RSS 피드 슬러그", + "LabelRSSFeedURL": "RSS 피드 URL", + "LabelRandomly": "무작위로", + "LabelReAddSeriesToContinueListening": "계속 듣기 위해 시리즈를 다시 추가하세요", + "LabelRead": "읽기", + "LabelReadAgain": "다시 읽기", + "LabelReadEbookWithoutProgress": "진행 상황을 유지하지 않고 전자책을 읽으세요", + "LabelRecentSeries": "최근 시리즈", + "LabelRecentlyAdded": "최근 추가됨", + "LabelRecommended": "추천", + "LabelRedo": "되돌리기", + "LabelRegion": "지역", + "LabelReleaseDate": "출시일", + "LabelRemoveAllMetadataAbs": "모든 metadata.abs 파일을 제거합니다.", + "LabelRemoveAllMetadataJson": "모든 metadata.json 파일을 제거합니다.", + "LabelRemoveAudibleBranding": "챕터에서 Audible 인트로와 아웃트로 제거", + "LabelRemoveCover": "커버 제거", + "LabelRemoveMetadataFile": "라이브러리 항목 폴더에서 메타데이터 파일 제거", + "LabelRemoveMetadataFileHelp": "{0} 폴더에서 모든 metadata.json 및 metadata.abs 파일을 제거합니다.", + "LabelRowsPerPage": "페이지당 행 수", + "LabelSearchTerm": "검색어", + "LabelSearchTitle": "제목 검색", + "LabelSearchTitleOrASIN": "제목 또는 ASIN 검색", + "LabelSeason": "시즌", + "LabelSeasonNumber": "시즌 #{0}", + "LabelSelectAll": "모두 선택", + "LabelSelectAllEpisodes": "모든 에피소드 선택", + "LabelSelectEpisodesShowing": "{0}개의 에피소드를 선택", + "LabelSelectUser": "사용자 선택", + "LabelSelectUsers": "사용자 선택", + "LabelSendEbookToDevice": "...에 전자책을 보내기", + "LabelSequence": "순서", + "LabelSerial": "일련번호", + "LabelSeries": "시리즈", + "LabelSeriesName": "시리즈 이름", + "LabelSeriesProgress": "시리즈 진행 상황", + "LabelServerLogLevel": "서버 로그 수준", + "LabelServerYearReview": "서버 연간 리뷰 ({0})", + "LabelSetEbookAsPrimary": "기본으로 설정", + "LabelSetEbookAsSupplementary": "보충으로 설정", + "LabelSettingsAllowIframe": "iframe에 포함 허용", + "LabelSettingsAudiobooksOnly": "오디오북만", + "LabelSettingsAudiobooksOnlyHelp": "이 설정을 활성화하면 오디오북 폴더 내부에 있는 경우를 제외하고 전자책 파일이 무시되며, 오디오북 폴더 내부에 있는 경우 해당 파일은 보충 전자책으로 설정됩니다.", + "LabelSettingsBookshelfViewHelp": "나무 선반을 활용한 스큐어모픽 디자인", + "LabelSettingsChromecastSupport": "Chromecast 지원", + "LabelSettingsDateFormat": "날짜 형식", + "LabelSettingsEnableWatcher": "라이브러리의 변경 사항을 자동으로 감시", + "LabelSettingsEnableWatcherForLibrary": "라이브러리의 변경 사항을 자동으로 감시", + "LabelSettingsEnableWatcherHelp": "파일 변경 사항이 감지되면 항목을 자동으로 추가/업데이트합니다. *서버 재시작이 필요합니다.", + "LabelSettingsEpubsAllowScriptedContent": "epub에서 스크립트 콘텐츠 허용", + "LabelSettingsEpubsAllowScriptedContentHelp": "epub 파일에서 스크립트를 실행하도록 허용합니다. epub 파일 출처를 신뢰하지 않는 한 이 설정을 비활성화하는 것이 좋습니다.", + "LabelSettingsExperimentalFeatures": "실험적 특징", + "LabelSettingsExperimentalFeaturesHelp": "여러분의 피드백과 테스트에 도움이 될 만한 개발 중인 기능입니다. 클릭하여 Github 토론을 시작하세요.", + "LabelSettingsFindCovers": "커버 찾기", + "LabelSettingsFindCoversHelp": "오디오북에 내장된 표지나 폴더 내에 표지 이미지가 없는 경우 스캐너는 표지를 찾으려고 시도합니다.
참고: 이렇게 하면 스캔 시간이 길어집니다.", + "LabelSettingsHideSingleBookSeries": "단일 책 시리즈 숨기기", + "LabelSettingsHideSingleBookSeriesHelp": "단일 책으로 구성된 시리즈는 시리즈 페이지와 홈페이지 선반에서 숨겨집니다.", + "LabelSettingsHomePageBookshelfView": "홈페이지 책꽂이 보기 사용", + "LabelSettingsLibraryBookshelfView": "라이브러리에 bookshelf view 사용", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "완료율이 다음보다 큼", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "남은 시간은 (초) 미만입니다.", + "LabelSettingsLibraryMarkAsFinishedWhen": "미디어 항목을 완료된 것으로 표시", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Continue Series의 이전 책 건너뛰기", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "시리즈 계속하기 홈페이지 선반에는 시리즈 중 아직 시작하지 않은 첫 번째 책이 표시됩니다. 시리즈 중 최소 한 권은 완료되었고 진행 중인 책은 없습니다. 이 설정을 활성화하면 시작하지 않은 첫 번째 책 대신 가장 늦게 완료된 책부터 시리즈가 이어집니다.", + "LabelSettingsParseSubtitles": "Parse subtitles", + "LabelSettingsParseSubtitlesHelp": "오디오북 폴더 이름에서 자막을 추출합니다.
자막은 \" - \"로 구분해야 합니다.
즉, \"책 제목 - 여기에 자막이 있습니다\"에는 \"여기에 자막이 있습니다\"라는 자막이 있습니다.", + "LabelSettingsPreferMatchedMetadata": "일치하는 메타데이터를 선호", + "LabelSettingsPreferMatchedMetadataHelp": "빠른 매칭을 사용하면 매칭된 데이터가 항목 세부 정보보다 우선합니다. 기본적으로 빠른 매칭은 누락된 세부 정보만 채웁니다.", + "LabelSettingsSkipMatchingBooksWithASIN": "이미 ASIN이 있는 일치하는 책을 건너뜁니다.", + "LabelSettingsSkipMatchingBooksWithISBN": "이미 ISBN이 있는 일치하는 책을 건너뜁니다.", + "LabelSettingsSortingIgnorePrefixes": "정렬 시 접두사 무시", + "LabelSettingsSortingIgnorePrefixesHelp": "즉, 접두사 \"the\"의 경우 책 제목 \"The Book Title\"은 \"Book Title, The\"로 정렬됩니다.", + "LabelSettingsSquareBookCovers": "정사각형 책 표지를 사용", + "LabelSettingsSquareBookCoversHelp": "표준 1.6:1 책 표지보다 정사각형 표지를 사용하는 것을 선호합니다.", + "LabelSettingsStoreCoversWithItem": "품목과 함께 매장 커버", + "LabelSettingsStoreCoversWithItemHelp": "기본적으로 표지는 /metadata/items에 저장됩니다. 이 설정을 활성화하면 표지가 라이브러리 항목 폴더에 저장됩니다. \"cover\"라는 이름의 파일 하나만 저장됩니다.", + "LabelSettingsStoreMetadataWithItem": "항목과 함께 메타데이터 저장", + "LabelSettingsStoreMetadataWithItemHelp": "기본적으로 메타데이터 파일은 /metadata/items에 저장되며 이 설정을 활성화하면 라이브러리 항목 폴더에 메타데이터 파일이 저장됩니다.", + "LabelSettingsTimeFormat": "시간 형식", + "LabelShare": "공유", + "LabelShareDownloadableHelp": "공유 링크가 있는 사용자는 라이브러리 항목의 zip 파일을 다운로드할 수 있습니다.", + "LabelShareOpen": "공유 열기", + "LabelShareURL": "URL 공유", + "LabelShowAll": "모두 표시", + "LabelShowSeconds": "초 표시", + "LabelShowSubtitles": "자막 표시", + "LabelSize": "크기", + "LabelSleepTimer": "슬립 타이머", + "LabelSlug": "Slug", + "LabelSortAscending": "오름차순", + "LabelSortDescending": "내림차순", + "LabelSortPubDate": "게시 날짜 정렬", + "LabelStart": "시작", + "LabelStartTime": "시작 시간", + "LabelStarted": "시작됨", + "LabelStartedAt": "시작일", + "LabelStartedDate": "{0} 시작됨", + "LabelStatsAudioTracks": "오디오 트랙", + "LabelStatsAuthors": "작가", + "LabelStatsBestDay": "최고의 날", + "LabelStatsDailyAverage": "일일 평균", + "LabelStatsDays": "일", + "LabelStatsDaysListened": "청취 일수", + "LabelStatsHours": "시간", + "LabelStatsInARow": "in a row", + "LabelStatsItemsFinished": "완료된 항목", + "LabelStatsItemsInLibrary": "라이브러리의 항목", + "LabelStatsMinutes": "분", + "LabelStatsMinutesListening": "청취 분수", + "LabelStatsOverallDays": "전체 일수", + "LabelStatsOverallHours": "전체 시간", + "LabelStatsWeekListening": "주간 청취", + "LabelSubtitle": "자막", + "LabelSupportedFileTypes": "지원되는 파일 유형", + "LabelTag": "태그", + "LabelTags": "태그", + "LabelTagsAccessibleToUser": "사용자가 접근 가능한 태그", + "LabelTagsNotAccessibleToUser": "사용자가 액세스할 수 없는 태그", + "LabelTasks": "실행 중인 작업", + "LabelTextEditorBulletedList": "글머리 기호 목록", + "LabelTextEditorLink": "링크", + "LabelTextEditorNumberedList": "번호가 매겨진 목록", + "LabelTextEditorUnlink": "언링크", + "LabelTheme": "테마", + "LabelThemeDark": "Dark", + "LabelThemeLight": "Light", + "LabelThemeSepia": "Sepia", + "LabelTimeBase": "시간 기준", + "LabelTimeDurationXHours": "{0}시간", + "LabelTimeDurationXMinutes": "{0}분", + "LabelTimeDurationXSeconds": "{0}초", + "LabelTimeInMinutes": "시간(분)", + "LabelTimeLeft": "{0}개 남음", + "LabelTimeListened": "들은 시간", + "LabelTimeListenedToday": "오늘 청취 시간", + "LabelTimeRemaining": "{0} 남음", + "LabelTimeToShift": "이동 시간(초)", + "LabelTitle": "제목", + "LabelToolsEmbedMetadata": "메타데이터 삽입", + "LabelToolsEmbedMetadataDescription": "오디오 파일에 커버 이미지와 챕터를 포함한 메타데이터를 포함합니다.", + "LabelToolsM4bEncoder": "M4B 인코더", + "LabelToolsMakeM4b": "M4B 오디오북 파일 만들기", + "LabelToolsMakeM4bDescription": "내장된 메타데이터, 표지 이미지, 챕터가 포함된 .M4B 오디오북 파일을 생성합니다.", + "LabelToolsSplitM4b": "M4B를 MP3로 분할", + "LabelToolsSplitM4bDescription": "챕터별로 분할된 M4B에서 내장된 메타데이터, 표지 이미지, 챕터를 사용하여 MP3를 만듭니다.", + "LabelTotalDuration": "총 기간", + "LabelTotalTimeListened": "총 청취 시간", + "LabelTrackFromFilename": "파일 이름에서 추적", + "LabelTrackFromMetadata": "메타데이터에서 추적", + "LabelTracks": "트랙", + "LabelTracksMultiTrack": "멀티트랙", + "LabelTracksNone": "트랙 없음", + "LabelTracksSingleTrack": "싱글트랙", + "LabelTrailer": "트레일러", + "LabelType": "유형", + "LabelUnabridged": "완본", + "LabelUndo": "실행 취소", + "LabelUnknown": "미상", + "LabelUnknownPublishDate": "알 수 없는 게시 날짜", + "LabelUpdateCover": "커버 업데이트", + "LabelUpdateCoverHelp": "일치하는 항목이 발견되면 선택한 책의 기존 표지를 덮어쓸 수 있습니다.", + "LabelUpdateDetails": "세부 정보 업데이트", + "LabelUpdateDetailsHelp": "일치하는 항목이 발견되면 선택한 책에 대한 기존 세부 정보를 덮어쓸 수 있습니다.", + "LabelUpdatedAt": "업데이트됨", + "LabelUploaderDragAndDrop": "파일 또는 폴더 드래그 앤 드롭", + "LabelUploaderDragAndDropFilesOnly": "드래그 앤 드롭 파일", + "LabelUploaderDropFiles": "파일 드롭", + "LabelUploaderItemFetchMetadataHelp": "제목, 저자 및 시리즈를 자동으로 가져옵니다.", + "LabelUseAdvancedOptions": "고급 옵션 사용", + "LabelUseChapterTrack": "챕터 트랙 사용", + "LabelUseFullTrack": "전체 트랙 사용", + "LabelUseZeroForUnlimited": "무제한을 원하시면 0을 사용", + "LabelUser": "사용자", + "LabelUsername": "사용자 이름", + "LabelValue": "값", + "LabelVersion": "버전", + "LabelViewBookmarks": "북마크 보기", + "LabelViewChapters": "챕터 보기", + "LabelViewPlayerSettings": "플레이어 설정 보기", + "LabelViewQueue": "플레이어 대기열 보기", + "LabelVolume": "볼륨", + "LabelWebRedirectURLsDescription": "로그인 후 웹 앱으로 다시 리디렉션할 수 있도록 OAuth 공급자에서 다음 URL을 승인하세요.", + "LabelWebRedirectURLsSubfolder": "리디렉션 URL의 하위 폴더", + "LabelWeekdaysToRun": "주중에 실행", + "LabelXBooks": "{0}권의 책", + "LabelXItems": "{0}개 항목", + "LabelYearReviewHide": "연간 리뷰 숨기기", + "LabelYearReviewShow": "올해의 리뷰 보기", + "LabelYourAudiobookDuration": "오디오북 길이", + "LabelYourBookmarks": "내 북마크", + "LabelYourPlaylists": "내 재생목록", + "LabelYourProgress": "내 진행 상황", + "MessageAddToPlayerQueue": "플레이어 대기열에 추가", + "MessageAppriseDescription": "이 기능을 사용하려면 Apprise API 인스턴스를 실행 중이거나 동일한 요청을 처리할 API가 필요합니다.
Apprise API URL은 알림을 보낼 전체 URL 경로여야 합니다. 예를 들어, API 인스턴스가 http://192.168.1.1:8337에서 제공되는 경우 http://192.168.1.1:8337/notify를 입력합니다.", + "MessageAsinCheck": "Amazon이 아닌 올바른 Audible 지역의 ASIN을 사용하고 있는지 확인하세요.", + "MessageAuthenticationLegacyTokenWarning": "기존 API 토큰은 향후 제거될 예정입니다. 대신 API 키를 사용하세요.", + "MessageAuthenticationOIDCChangesRestart": "OIDC 변경 사항을 적용하려면 저장한 후 서버를 다시 시작하세요.", + "MessageAuthenticationSecurityMessage": "보안을 위해 인증 방식이 개선되었습니다. 모든 사용자는 다시 로그인해야 합니다.", + "MessageBackupsDescription": "백업에는 사용자, 사용자 진행 상황, 라이브러리 항목 세부 정보, 서버 설정, 그리고 /metadata/items/metadata/authors에 저장된 이미지가 포함됩니다. 라이브러리 폴더에 저장된 파일은 백업에 포함되지 않습니다.", + "MessageBackupsLocationEditNote": "참고: 백업 위치를 업데이트해도 기존 백업은 이동되거나 수정되지 않습니다.", + "MessageBackupsLocationNoEditNote": "참고: 백업 위치는 환경 변수를 통해 설정되며 여기서는 변경할 수 없습니다.", + "MessageBackupsLocationPathEmpty": "백업 위치 경로는 비워둘 수 없습니다.", + "MessageBatchEditPopulateMapDetailsAllHelp": "활성화된 필드에 모든 항목의 데이터를 채웁니다. 여러 값이 있는 필드는 병합됩니다.", + "MessageBatchEditPopulateMapDetailsItemHelp": "이 항목의 데이터로 활성화된 지도 세부 정보 필드를 채웁니다.", + "MessageBatchQuickMatchDescription": "빠른 매칭은 선택한 항목에 대해 누락된 표지와 메타데이터를 추가하려고 시도합니다. 빠른 매칭이 기존 표지 및/또는 메타데이터를 덮어쓰도록 허용하려면 아래 옵션을 활성화하세요.", + "MessageBookshelfNoCollections": "아직 컬렉션을 만들지 않았습니다.", + "MessageBookshelfNoCollectionsHelp": "소장 자료는 공개되어 있습니다. 라이브러리에 접속할 수 있는 모든 이용자가 열람할 수 있습니다.", + "MessageBookshelfNoRSSFeeds": "RSS 피드가 열려 있지 않습니다", + "MessageBookshelfNoResultsForFilter": "필터 \"{0}: {1}\"에 대한 결과가 없습니다.", + "MessageBookshelfNoResultsForQuery": "쿼리에 대한 결과가 없습니다", + "MessageBookshelfNoSeries": "시리즈가 없습니다", + "MessageBulkChapterPattern": "이 번호 매기기 패턴을 사용하여 몇 개의 챕터를 추가하시겠습니까?", + "MessageChapterEndIsAfter": "오디오북이 끝난 후 챕터가 끝납니다.", + "MessageChapterErrorFirstNotZero": "첫 번째 챕터는 0에서 시작해야 합니다.", + "MessageChapterErrorStartGteDuration": "잘못된 시작 시간은 오디오북 길이보다 짧아야 합니다.", + "MessageChapterErrorStartLtPrev": "잘못된 시작 시간은 이전 챕터 시작 시간보다 크거나 같아야 합니다.", + "MessageChapterStartIsAfter": "챕터 시작은 오디오북이 끝난 후입니다.", + "MessageChaptersNotFound": "챕터를 찾을 수 없습니다", + "MessageCheckingCron": "cron을 확인 중입니다...", + "MessageConfirmCloseFeed": "이 피드를 닫으시겠습니까?", + "MessageConfirmDeleteApiKey": "API 키 \"{0}\"을 삭제하시겠습니까?", + "MessageConfirmDeleteBackup": "{0}에 대한 백업을 삭제하시겠습니까?", + "MessageConfirmDeleteDevice": "e-reader 장치 \"{0}\"을(를) 삭제하시겠습니까?", + "MessageConfirmDeleteFile": "이렇게 하면 파일 시스템에서 파일이 삭제됩니다. 정말 삭제하시겠습니까?", + "MessageConfirmDeleteLibrary": "라이브러리 \"{0}\"을 영구적으로 삭제하시겠습니까?", + "MessageConfirmDeleteLibraryItem": "이렇게 하면 라이브러리 항목이 데이터베이스와 파일 시스템에서 삭제됩니다. 정말 삭제하시겠습니까?", + "MessageConfirmDeleteLibraryItems": "이렇게 하면 데이터베이스와 파일 시스템에서 {0}개의 라이브러리 항목이 삭제됩니다. 정말 삭제하시겠습니까?", + "MessageConfirmDeleteMetadataProvider": "사용자 지정 메타데이터 공급자 \"{0}\"을(를) 삭제하시겠습니까?", + "MessageConfirmDeleteNotification": "이 알림을 삭제하시겠습니까?", + "MessageConfirmDeleteSession": "이 세션을 삭제하시겠습니까?", + "MessageConfirmEmbedMetadataInAudioFiles": "{0} 오디오 파일에 메타데이터를 포함하시겠습니까?", + "MessageConfirmForceReScan": "강제로 다시 스캔하시겠습니까?", + "MessageConfirmMarkAllEpisodesFinished": "모든 에피소드를 완료로 표시하시겠습니까?", + "MessageConfirmMarkAllEpisodesNotFinished": "모든 에피소드를 완료되지 않은 것으로 표시하시겠습니까?", + "MessageConfirmMarkItemFinished": "\"{0}\"을(를) 완료로 표시하시겠습니까?", + "MessageConfirmMarkItemNotFinished": "\"{0}\"을(를) 완료되지 않은 것으로 표시하시겠습니까?", + "MessageConfirmMarkSeriesFinished": "이 시리즈의 모든 책을 완료로 표시하시겠습니까?", + "MessageConfirmMarkSeriesNotFinished": "이 시리즈의 모든 책을 미완료로 표시하시겠습니까?", + "MessageConfirmNotificationTestTrigger": "테스트 데이터로 이 알림을 트리거할까요?", + "MessageConfirmPurgeCache": "캐시를 제거하면 /metadata/cache에 있는 전체 디렉토리가 삭제됩니다.

캐시 디렉토리를 제거하시겠습니까?", + "MessageConfirmPurgeItemsCache": "항목 캐시를 삭제하면 /metadata/cache/items에 있는 전체 디렉터리가 삭제됩니다.
정말입니까?", + "MessageConfirmQuickEmbed": "경고! 빠른 삽입 기능은 오디오 파일을 백업하지 않습니다. 오디오 파일을 백업해 두세요.

계속하시겠습니까?", + "MessageConfirmQuickMatchEpisodes": "빠른 매칭 에피소드는 일치하는 에피소드가 발견되면 세부 정보를 덮어씁니다. 매칭되지 않은 에피소드만 업데이트됩니다. 계속하시겠습니까?", + "MessageConfirmReScanLibraryItems": "{0}개 항목을 다시 스캔하시겠습니까?", + "MessageConfirmRemoveAllChapters": "모든 챕터를 제거하시겠습니까?", + "MessageConfirmRemoveAuthor": "작가 \"{0}\"을(를) 삭제하시겠습니까?", + "MessageConfirmRemoveCollection": "컬렉션 \"{0}\"을 제거하시겠습니까?", + "MessageConfirmRemoveEpisode": "에피소드 \"{0}\"을(를) 삭제하시겠습니까?", + "MessageConfirmRemoveEpisodeNote": "참고: \"Hard delete file\"를 전환하지 않는 한 오디오 파일은 삭제되지 않습니다.", + "MessageConfirmRemoveEpisodes": "{0}개의 에피소드를 삭제하시겠습니까?", + "MessageConfirmRemoveListeningSessions": "{0}개의 청취 세션을 제거하시겠습니까?", + "MessageConfirmRemoveMetadataFiles": "라이브러리 항목 폴더에서 모든 metadata.{0} 파일을 제거하시겠습니까?", + "MessageConfirmRemoveNarrator": "내레이터 \"{0}\"을(를) 제거하시겠습니까?", + "MessageConfirmRemovePlaylist": "재생목록 \"{0}\"을(를) 제거하시겠습니까?", + "MessageConfirmRenameGenre": "모든 항목의 장르 이름을 \"{0}\"에서 \"{1}\"로 바꾸시겠습니까?", + "MessageConfirmRenameGenreMergeNote": "참고: 이 장르는 이미 존재하므로 병합됩니다.", + "MessageConfirmRenameGenreWarning": "경고! 대소문자가 다른 유사한 장르 \"{0}\"이 이미 존재합니다.", + "MessageConfirmRenameTag": "모든 항목에 대해 태그 \"{0}\"의 이름을 \"{1}\"로 바꾸시겠습니까?", + "MessageConfirmRenameTagMergeNote": "참고: 이 태그는 이미 존재하므로 병합됩니다.", + "MessageConfirmRenameTagWarning": "경고! 대소문자가 다른 유사한 태그 \"{0}\"이 이미 존재합니다.", + "MessageConfirmResetProgress": "진행 상황을 재설정하시겠습니까?", + "MessageConfirmSendEbookToDevice": "{0}개의 전자책 \"{1}\"을 장치 \"{2}\"로 보내시겠습니까?", + "MessageConfirmUnlinkOpenId": "이 사용자를 OpenID에서 연결 해제하시겠습니까?", + "MessageDaysListenedInTheLastYear": "지난 1년 동안 {0}일 동안 청취됨", + "MessageDownloadingEpisode": "에피소드 다운로드 중", + "MessageDragFilesIntoTrackOrder": "파일을 올바른 트랙 순서로 끌어다 놓으세요", + "MessageEmbedFailed": "Embed 실패했습니다!", + "MessageEmbedFinished": "Embed 완료!", + "MessageEmbedQueue": "메타데이터 임베드 대기 중(대기열에 {0} 있음)", + "MessageEpisodesQueuedForDownload": "{0}개의 에피소드가 다운로드 대기 중입니다.", + "MessageEreaderDevices": "전자책을 안전하게 전달하려면 아래 나열된 각 기기에 대해 위의 이메일 주소를 유효한 발신자로 추가해야 할 수도 있습니다.", + "MessageFeedURLWillBe": "피드 URL은 {0}입니다.", + "MessageFetching": "가져오는 중...", + "MessageForceReScanDescription": "모든 파일을 새로 스캔하는 것처럼 다시 스캔합니다. 오디오 파일 ID3 태그, OPF 파일, 텍스트 파일은 새 파일처럼 스캔됩니다.", + "MessageHeatmapListeningTimeTooltip": "{0} {1}에서 청취 중", + "MessageHeatmapNoListeningSessions": "{0}에 대한 청취 세션이 없습니다.", + "MessageImportantNotice": "중요 공지!", + "MessageInsertChapterBelow": "아래에 챕터를 삽입하세요", + "MessageInvalidAsin": "잘못된 ASIN", + "MessageItemsSelected": "{0}개 항목이 선택됨", + "MessageItemsUpdated": "{0}개 항목이 업데이트됨.", + "MessageJoinUsOn": "가입하기", + "MessageLoading": "로딩중...", + "MessageLoadingFolders": "폴더를 로딩 중...", + "MessageLogsDescription": "로그는 /metadata/logs에 JSON 파일로 저장됩니다. 충돌 로그는 /metadata/logs/crash_logs.txt에 저장됩니다.", + "MessageM4BFailed": "M4B 실패!", + "MessageM4BFinished": "M4B 완료!", + "MessageMapChapterTitles": "타임스탬프를 조정하지 않고 기존 오디오북 장에 챕터 제목을 매핑합니다.", + "MessageMarkAllEpisodesFinished": "모든 에피소드를 완료로 표시", + "MessageMarkAllEpisodesNotFinished": "모든 에피소드를 완료되지 않은 것으로 표시", + "MessageMarkAsFinished": "완료로 표시", + "MessageMarkAsNotFinished": "완료되지 않음으로 표시", + "MessageMatchBooksDescription": "라이브러리에 있는 책과 선택한 검색 제공업체의 책을 비교하고, 빈 세부 정보와 표지 그림을 채웁니다. 세부 정보를 덮어쓰지 않습니다.", + "MessageNoAudioTracks": "오디오 트랙 없음", + "MessageNoAuthors": "작가 없음", + "MessageNoBackups": "백업 없음", + "MessageNoBookmarks": "북마크 없음", + "MessageNoChapters": "챕터 없음", + "MessageNoCollections": "컬렉션 없음", + "MessageNoCoversFound": "커버를 찾을 수 없음", + "MessageNoDescription": "설명 없음", + "MessageNoDevices": "장치가 없음", + "MessageNoDownloadsInProgress": "현재 진행 중인 다운로드가 없습니다.", + "MessageNoDownloadsQueued": "대기 중인 다운로드가 없습니다", + "MessageNoEpisodeMatchesFound": "일치하는 에피소드가 없습니다.", + "MessageNoEpisodes": "에피소드 없음", + "MessageNoFoldersAvailable": "사용 가능한 폴더가 없습니다", + "MessageNoGenres": "장르 없음", + "MessageNoIssues": "이슈 없음", + "MessageNoItems": "항목 없음", + "MessageNoItemsFound": "항목을 찾을 수 없음", + "MessageNoListeningSessions": "청취 세션 없음", + "MessageNoLogs": "로그 없음", + "MessageNoMediaProgress": "진행중인 미디어 없음", + "MessageNoNotifications": "알림 없음", + "MessageNoPodcastFeed": "잘못된 팟캐스트: 피드 없음", + "MessageNoPodcastsFound": "팟캐스트를 찾을 수 없음", + "MessageNoResults": "결과 없음", + "MessageNoSearchResultsFor": "\"{0}\"에 대한 검색 결과가 없습니다.", + "MessageNoSeries": "시리즈 없음", + "MessageNoTags": "태그 없음", + "MessageNoTasksRunning": "실행 중인 작업이 없음", + "MessageNoUpdatesWereNecessary": "업데이트가 필요하지 않음", + "MessageNoUserPlaylists": "재생목록이 없음", + "MessageNoUserPlaylistsHelp": "재생목록은 비공개입니다. 재생목록을 만든 사용자만 볼 수 있습니다.", + "MessageNotYetImplemented": "아직 구현되지 않음", + "MessageOpmlPreviewNote": "참고: 이는 파싱된 OPML 파일의 미리보기입니다. 실제 팟캐스트 제목은 RSS 피드에서 가져옵니다.", + "MessageOr": "또는", + "MessagePauseChapter": "챕터 재생 일시 정지", + "MessagePlayChapter": "챕터의 시작 부분을 들어보세요", + "MessagePlaylistCreateFromCollection": "컬렉션에서 재생 목록 만들기", + "MessagePleaseWait": "기다리세요...", + "MessagePodcastHasNoRSSFeedForMatching": "팟캐스트에 매칭에 사용할 RSS 피드 URL이 없습니다.", + "MessagePodcastSearchField": "검색어 또는 RSS 피드 URL을 입력하세요", + "MessageQuickEmbedInProgress": "빠른 임베드 진행 중", + "MessageQuickEmbedQueue": "빠른 임베드 대기 중(대기 중인 {0}개)", + "MessageQuickMatchAllEpisodes": "모든 에피소드 퀵 매치", + "MessageQuickMatchDescription": "빈 항목 세부 정보를 채우고 '{0}'의 첫 번째 일치 결과로 덮습니다. '일치하는 메타데이터 선호' 서버 설정이 활성화되어 있지 않으면 세부 정보를 덮어쓰지 않습니다.", + "MessageRemoveChapter": "챕터 제거", + "MessageRemoveEpisodes": "{0}개의 에피소드를 제거합니다.", + "MessageRemoveFromPlayerQueue": "플레이어 대기열에서 제거", + "MessageRemoveUserWarning": "사용자 \"{0}\"을 영구적으로 삭제하시겠습니까?", + "MessageReportBugsAndContribute": "버그 보고, 기능 요청 및 기여", + "MessageResetChaptersConfirm": "챕터를 재설정하고 변경 사항을 취소하시겠습니까?", + "MessageRestoreBackupConfirm": "생성된 백업을 복원하시겠습니까?", + "MessageRestoreBackupWarning": "백업을 복원하면 /config에 있는 전체 데이터베이스와 /metadata/items 및 /metadata/authors에 있는 표지 이미지가 덮어씌워집니다.

백업은 라이브러리 폴더의 어떤 파일도 수정하지 않습니다. 라이브러리 폴더에 표지 그림과 메타데이터를 저장하도록 서버 설정을 활성화한 경우, 해당 내용은 백업되거나 덮어씌워지지 않습니다.

서버를 사용하는 모든 클라이언트는 자동으로 새로 고쳐집니다.", + "MessageScheduleLibraryScanNote": "대부분의 사용자는 이 기능을 비활성화하고 라이브러리 변경 사항 자동 감시 설정을 활성화하는 것이 좋습니다. 라이브러리 폴더의 변경 사항을 자동으로 감지합니다. 파일 시스템(예: NFS)에서 라이브러리 변경 사항 자동 감시 기능이 작동하지 않는 경우 이 기능을 활성화하세요.", + "MessageScheduleRunEveryWeekdayAtTime": "{1}에서 {0}마다 실행", + "MessageSearchResultsFor": "검색 결과", + "MessageSelected": "{0}개 선택됨", + "MessageSeriesSequenceCannotContainSpaces": "시리즈 시퀀스에는 공백을 포함할 수 없습니다.", + "MessageServerCouldNotBeReached": "서버에 접속할 수 없습니다", + "MessageSetChaptersFromTracksDescription": "각 오디오 파일을 장으로 사용하고 챕터 제목을 오디오 파일 이름으로 사용하여 챕터를 설정합니다.", + "MessageShareExpirationWillBe": "만료일은 {0}입니다.", + "MessageShareExpiresIn": "{0}에 만료됨", + "MessageShareURLWillBe": "공유 URL은 {0}입니다.", + "MessageStartPlaybackAtTime": "{1}에서 \"{0}\"의 재생을 시작하시겠습니까?", + "MessageTaskAudioFileNotWritable": "오디오 파일 \"{0}\"은 쓸 수 없습니다.", + "MessageTaskCanceledByUser": "사용자가 작업을 취소했습니다.", + "MessageTaskDownloadingEpisodeDescription": "에피소드 \"{0}\" 다운로드 중", + "MessageTaskEmbeddingMetadata": "메타데이터 임베딩", + "MessageTaskEmbeddingMetadataDescription": "오디오북 \"{0}\"에 메타데이터 삽입", + "MessageTaskEncodingM4b": "M4B 인코딩", + "MessageTaskEncodingM4bDescription": "오디오북 \"{0}\"을 단일 m4b 파일로 인코딩", + "MessageTaskFailed": "실패함", + "MessageTaskFailedToBackupAudioFile": "오디오 파일 \"{0}\"을 백업하지 못했습니다.", + "MessageTaskFailedToCreateCacheDirectory": "캐시 디렉토리를 생성하지 못했습니다.", + "MessageTaskFailedToEmbedMetadataInFile": "파일 \"{0}\"에 메타데이터를 삽입하는 데 실패했습니다.", + "MessageTaskFailedToMergeAudioFiles": "오디오 파일을 병합하지 못했습니다.", + "MessageTaskFailedToMoveM4bFile": "m4b 파일을 이동하지 못했습니다.", + "MessageTaskFailedToWriteMetadataFile": "메타데이터 파일을 쓰지 못했습니다.", + "MessageTaskMatchingBooksInLibrary": "라이브러리 \"{0}\"에서 일치하는 책", + "MessageTaskNoFilesToScan": "검사할 파일이 없음", + "MessageTaskOpmlImport": "OPML 가져오기", + "MessageTaskOpmlImportDescription": "{0} RSS 피드에서 팟캐스트 만들기", + "MessageTaskOpmlImportFeed": "OPML 가져오기 피드", + "MessageTaskOpmlImportFeedDescription": "RSS 피드 \"{0}\"을(를) 가져오는 중", + "MessageTaskOpmlImportFeedFailed": "팟캐스트 피드를 가져오지 못했습니다.", + "MessageTaskOpmlImportFeedPodcastDescription": "팟캐스트 \"{0}\" 생성 중", + "MessageTaskOpmlImportFeedPodcastExists": "팟캐스트가 이미 경로에 존재합니다.", + "MessageTaskOpmlImportFeedPodcastFailed": "팟캐스트를 만들지 못했습니다", + "MessageTaskOpmlImportFinished": "{0}개의 팟캐스트가 추가되었습니다.", + "MessageTaskOpmlParseFailed": "OPML 파일을 구문 분석하지 못했습니다.", + "MessageTaskOpmlParseFastFail": "잘못된 OPML 파일 태그를 찾을 수 없거나 태그를 찾을 수 없습니다.", + "MessageTaskOpmlParseNoneFound": "OPML 파일에서 피드를 찾을 수 없습니다.", + "MessageTaskScanItemsAdded": "{0} 추가됨", + "MessageTaskScanItemsMissing": "{0} 누락됨", + "MessageTaskScanItemsUpdated": "{0} 업데이트됨", + "MessageTaskScanNoChangesNeeded": "변경이 필요하지 않습니다", + "MessageTaskScanningFileChanges": "\"{0}\"의 파일 변경 사항 스캔 중", + "MessageTaskScanningLibrary": "\"{0}\" 라이브러리 스캔 중", + "MessageTaskTargetDirectoryNotWritable": "대상 디렉토리에 쓸 수 없습니다", + "MessageThinking": "생각중...", + "MessageUploaderItemFailed": "업로드에 실패했습니다", + "MessageUploaderItemSuccess": "성공적으로 업로드되었습니다!", + "MessageUploading": "업로드 중...", + "MessageValidCronExpression": "유효한 cron 표현식", + "MessageWatcherIsDisabledGlobally": "서버 설정에서 Watcher가 전역적으로 비활성화되었습니다.", + "MessageXLibraryIsEmpty": "{0} 라이브러리가 비어 있습니다!", + "MessageYourAudiobookDurationIsLonger": "오디오북 길이가 검색된 길이보다 깁니다.", + "MessageYourAudiobookDurationIsShorter": "오디오북 길이가 검색된 길이보다 짧습니다.", + "NoteChangeRootPassword": "루트 사용자는 빈 비밀번호를 가질 수 있는 유일한 사용자입니다.", + "NoteChapterEditorTimes": "참고: 첫 번째 장의 시작 시간은 0:00으로 유지되어야 하며 마지막 장의 시작 시간은 이 오디오북 길이를 초과할 수 없습니다.", + "NoteFolderPicker": "참고: 이미 매핑된 폴더는 표시되지 않습니다.", + "NoteRSSFeedPodcastAppsHttps": "경고: 대부분의 팟캐스트 앱에서는 RSS 피드 URL이 HTTPS를 사용해야 합니다.", + "NoteRSSFeedPodcastAppsPubDate": "경고: 1개 이상의 에피소드에 게시 날짜가 없습니다. 일부 팟캐스트 앱에서는 게시 날짜가 필요합니다.", + "NoteUploaderFoldersWithMediaFiles": "미디어 파일이 있는 폴더는 별도의 라이브러리 항목으로 처리됩니다.", + "NoteUploaderOnlyAudioFiles": "오디오 파일만 업로드하는 경우 각 오디오 파일은 별도의 오디오북으로 처리됩니다.", + "NoteUploaderUnsupportedFiles": "지원되지 않는 파일은 무시됩니다. 폴더를 선택하거나 삭제할 때 항목 폴더에 없는 다른 파일은 무시됩니다.", + "NotificationOnBackupCompletedDescription": "백업이 완료되면 트리거됩니다.", + "NotificationOnBackupFailedDescription": "백업이 실패하면 트리거됨", + "NotificationOnEpisodeDownloadedDescription": "팟캐스트 에피소드가 자동으로 다운로드될 때 트리거됩니다.", + "NotificationOnRSSFeedDisabledDescription": "실패한 시도가 너무 많아 자동 에피소드 다운로드가 비활성화되면 트리거됩니다.", + "NotificationOnRSSFeedFailedDescription": "자동 에피소드 다운로드에 대한 RSS 피드 요청이 실패하면 트리거됩니다.", + "NotificationOnTestDescription": "알림 시스템 테스트를 위한 이벤트", + "PlaceholderBulkChapterInput": "챕터 제목을 입력하거나 번호를 사용하세요(예: '에피소드 1', '챕터 10', '1.')", + "PlaceholderNewCollection": "새로운 컬렉션 이름", + "PlaceholderNewFolderPath": "새 폴더 경로", + "PlaceholderNewPlaylist": "새로운 재생 목록 이름", + "PlaceholderSearch": "검색..", + "PlaceholderSearchEpisode": "에피소드 검색..", + "StatsAuthorsAdded": "작가가 추가되었습니다", + "StatsBooksAdded": "추가된 책", + "StatsBooksAdditional": "추가된 내용은 다음과 같습니다…", + "StatsBooksFinished": "책 완성", + "StatsBooksFinishedThisYear": "올해 읽은 책이 몇 권 있어요…", + "StatsBooksListenedTo": "듣는 책", + "StatsCollectionGrewTo": "너의 책 컬렉션이 다음과 같이 늘어났습니다…", + "StatsSessions": "세션", + "StatsSpentListening": "보낸 청취시간", + "StatsTopAuthor": "인기 작가", + "StatsTopAuthors": "인기 작가들", + "StatsTopGenre": "인기 장르", + "StatsTopGenres": "인기 장르", + "StatsTopMonth": "월간 인기", + "StatsTopNarrator": "인기 내레이터", + "StatsTopNarrators": "인기 내레이터들", + "StatsTotalDuration": "총 기간은…", + "StatsYearInReview": "올해 리뷰", + "ToastAccountUpdateSuccess": "계정이 업데이트되었습니다", + "ToastAppriseUrlRequired": "Apprise URL을 입력해야 합니다.", + "ToastAsinRequired": "ASIN이 필요합니다", + "ToastAuthorImageRemoveSuccess": "작가 이미지가 삭제되었습니다.", + "ToastAuthorNotFound": "작가 \"{0}\"을(를) 찾을 수 없습니다.", + "ToastAuthorRemoveSuccess": "작가 삭제됨", + "ToastAuthorSearchNotFound": "작가를 찾을 수 없습니다", + "ToastAuthorUpdateMerged": "작가 병합됨", + "ToastAuthorUpdateSuccess": "작가 업데이트됨", + "ToastAuthorUpdateSuccessNoImageFound": "작가 업데이트됨(이미지 없음)", + "ToastBackupAppliedSuccess": "백업이 적용되었음", + "ToastBackupCreateFailed": "백업을 생성하지 못했습니다", + "ToastBackupCreateSuccess": "백업이 생성되었습니다", + "ToastBackupDeleteFailed": "백업을 삭제하지 못했습니다.", + "ToastBackupDeleteSuccess": "백업이 삭제되었습니다", + "ToastBackupInvalidMaxKeep": "보관할 백업 수가 올바르지 않습니다.", + "ToastBackupInvalidMaxSize": "잘못된 최대 백업 크기", + "ToastBackupRestoreFailed": "백업을 복원하지 못했습니다", + "ToastBackupUploadFailed": "백업 업로드에 실패했습니다", + "ToastBackupUploadSuccess": "백업 업로드됨", + "ToastBatchApplyDetailsToItemsSuccess": "항목에 적용된 세부 정보", + "ToastBatchDeleteFailed": "일괄 삭제에 실패했습니다", + "ToastBatchDeleteSuccess": "일괄 삭제 성공", + "ToastBatchQuickMatchFailed": "일괄 퀵 매치에 실패했습니다!", + "ToastBatchQuickMatchStarted": "{0}권의 책에 대한 일괄 퀵 매치가 시작되었습니다!", + "ToastBatchUpdateFailed": "일괄 업데이트에 실패했습니다", + "ToastBatchUpdateSuccess": "일괄 업데이트 성공", + "ToastBookmarkCreateFailed": "북마크를 생성하지 못했습니다.", + "ToastBookmarkCreateSuccess": "북마크 추가됨", + "ToastBookmarkRemoveSuccess": "북마크가 삭제되었습니다", + "ToastBulkChapterInvalidCount": "1~150 사이의 숫자를 입력하세요", + "ToastCachePurgeFailed": "캐시를 정리하지 못했습니다.", + "ToastCachePurgeSuccess": "캐시가 성공적으로 삭제되었습니다.", + "ToastChapterLocked": "챕터가 잠겼습니다.", + "ToastChapterStartTimeAdjusted": "챕터 시작 시간이 {0}초로 조정되었습니다.", + "ToastChaptersAllLocked": "모든 챕터는 잠겨 있습니다. 일부 챕터를 잠금 해제하여 시간을 이동하세요.", + "ToastChaptersHaveErrors": "챕터에 오류가 있습니다", + "ToastChaptersInvalidShiftAmountLast": "잘못된 이동량입니다. 마지막 장의 시작 시간이 이 오디오북의 길이를 초과합니다.", + "ToastChaptersInvalidShiftAmountStart": "잘못된 이동량입니다. 첫 번째 챕터의 길이는 0 또는 음수이며 두 번째 챕터로 덮어쓰여집니다. 두 번째 챕터의 시작 길이를 늘리세요.", + "ToastChaptersMustHaveTitles": "챕터에는 제목이 있어야 합니다.", + "ToastChaptersRemoved": "챕터가 삭제되었습니다", + "ToastChaptersUpdated": "챕터 업데이트됨", + "ToastCollectionItemsAddFailed": "컬렉션에 항목을 추가하는 데 실패했습니다.", + "ToastCollectionRemoveSuccess": "컬렉션이 제거되었습니다", + "ToastCollectionUpdateSuccess": "컬렉션이 업데이트되었습니다", + "ToastConnectionNotAvailable": "연결할 수 없습니다. 나중에 다시 시도해 주세요.", + "ToastCoverSearchFailed": "커버 검색에 실패했습니다", + "ToastCoverUpdateFailed": "커버 업데이트에 실패했습니다", + "ToastDateTimeInvalidOrIncomplete": "날짜와 시간이 잘못되었거나 불완전합니다.", + "ToastDeleteFileFailed": "파일을 삭제하지 못했습니다", + "ToastDeleteFileSuccess": "파일이 삭제되었습니다", + "ToastDeviceAddFailed": "장치를 추가하지 못했습니다", + "ToastDeviceNameAlreadyExists": "해당 이름의 전자책 단말기가 이미 존재합니다.", + "ToastDeviceTestEmailFailed": "테스트 이메일을 보내지 못했습니다.", + "ToastDeviceTestEmailSuccess": "테스트 이메일이 전송되었습니다.", + "ToastEmailSettingsUpdateSuccess": "이메일 설정이 업데이트되었습니다.", + "ToastEncodeCancelFailed": "인코딩 취소에 실패했습니다", + "ToastEncodeCancelSucces": "인코딩 취소됨", + "ToastEpisodeDownloadQueueClearFailed": "대기열을 지우는 데 실패했습니다.", + "ToastEpisodeDownloadQueueClearSuccess": "에피소드 다운로드 대기열이 지워졌습니다.", + "ToastEpisodeUpdateSuccess": "{0}개 에피소드 업데이트됨", + "ToastErrorCannotShare": "이 기기에서는 기본적으로 공유할 수 없습니다.", + "ToastFailedToCreate": "생성에 실패했습니다", + "ToastFailedToDelete": "삭제에 실패했습니다", + "ToastFailedToLoadData": "데이터를 로드하는 데 실패했습니다", + "ToastFailedToMatch": "매칭에 실패했습니다", + "ToastFailedToShare": "공유에 실패했습니다", + "ToastFailedToUpdate": "업데이트에 실패했습니다", + "ToastInvalidImageUrl": "잘못된 이미지 URL", + "ToastInvalidMaxEpisodesToDownload": "다운로드할 수 있는 최대 에피소드 수가 올바르지 않습니다.", + "ToastInvalidUrl": "잘못된 URL", + "ToastInvalidUrls": "하나 이상의 URL이 잘못되었습니다.", + "ToastItemCoverUpdateSuccess": "아이템 커버 업데이트됨", + "ToastItemDeletedFailed": "항목을 삭제하지 못했습니다.", + "ToastItemDeletedSuccess": "삭제된 항목", + "ToastItemDetailsUpdateSuccess": "항목 상세정보 업데이트됨", + "ToastItemMarkedAsFinishedFailed": "완료로 표시하지 못했습니다.", + "ToastItemMarkedAsFinishedSuccess": "완료로 표시된 항목", + "ToastItemMarkedAsNotFinishedFailed": "완료되지 않음으로 표시하는 데 실패했습니다.", + "ToastItemMarkedAsNotFinishedSuccess": "완료되지 않음으로 표시된 항목", + "ToastItemUpdateSuccess": "항목이 업데이트되었습니다", + "ToastLibraryCreateFailed": "라이브러리를 생성하지 못했습니다.", + "ToastLibraryCreateSuccess": "라이브러리 \"{0}\"이 생성되었습니다.", + "ToastLibraryDeleteFailed": "라이브러리 삭제에 실패했습니다", + "ToastLibraryDeleteSuccess": "라이브러리 삭제됨", + "ToastLibraryScanFailedToStart": "스캔을 시작하지 못했습니다", + "ToastLibraryScanStarted": "라이브러리 스캔이 시작되었습니다", + "ToastLibraryUpdateSuccess": "라이브러리 \"{0}\"이(가) 업데이트되었습니다.", + "ToastMatchAllAuthorsFailed": "모든 작가를 일치시키지 못했습니다.", + "ToastMetadataFilesRemovedError": "메타데이터를 제거하는 중 오류가 발생했습니다.{0} 파일", + "ToastMetadataFilesRemovedNoneFound": "메타데이터가 없습니다.{0} 라이브러리에서 파일을 찾았습니다.", + "ToastMetadataFilesRemovedNoneRemoved": "메타데이터가 없습니다.{0} 파일이 제거되었습니다.", + "ToastMetadataFilesRemovedSuccess": "{0} 메타데이터.{1} 파일 제거됨", + "ToastMustHaveAtLeastOnePath": "최소한 하나의 경로가 있어야 합니다", + "ToastNameEmailRequired": "이름과 이메일이 필요합니다", + "ToastNameRequired": "이름이 필요합니다", + "ToastNewApiKeyUserError": "사용자를 선택해야 합니다", + "ToastNewEpisodesFound": "{0}개의 새로운 에피소드가 발견되었습니다.", + "ToastNewUserCreatedFailed": "계정 생성에 실패했습니다: \"{0}\"", + "ToastNewUserCreatedSuccess": "새로운 계정이 생성되었습니다", + "ToastNewUserLibraryError": "최소한 하나의 라이브러리를 선택해야 합니다.", + "ToastNewUserPasswordError": "비밀번호가 있어야 합니다. 루트 사용자만 빈 비밀번호를 가질 수 있습니다.", + "ToastNewUserTagError": "최소한 하나의 태그를 선택해야 합니다.", + "ToastNewUserUsernameError": "사용자 이름을 입력하세요", + "ToastNoNewEpisodesFound": "새로운 에피소드가 없습니다", + "ToastNoRSSFeed": "팟캐스트에 RSS 피드가 없습니다", + "ToastNoUpdatesNecessary": "업데이트가 필요하지 않습니다", + "ToastNotificationCreateFailed": "알림을 생성하지 못했습니다.", + "ToastNotificationDeleteFailed": "알림 삭제에 실패했습니다", + "ToastNotificationFailedMaximum": "실패한 최대 시도 횟수는 0 이상이어야 합니다.", + "ToastNotificationQueueMaximum": "최대 알림 대기열은 0 이상이어야 합니다.", + "ToastNotificationSettingsUpdateSuccess": "알림 설정이 업데이트되었습니다.", + "ToastNotificationTestTriggerFailed": "테스트 알림을 트리거하지 못했습니다.", + "ToastNotificationTestTriggerSuccess": "테스트 알림이 트리거되었습니다.", + "ToastNotificationUpdateSuccess": "알림이 업데이트되었습니다", + "ToastPlaylistCreateFailed": "플레이리스트를 생성하지 못했습니다", + "ToastPlaylistCreateSuccess": "플레이리스트가 생성되었습니다", + "ToastPlaylistRemoveSuccess": "재생목록이 삭제되었습니다", + "ToastPlaylistUpdateSuccess": "플레이리스트가 업데이트되었습니다", + "ToastPodcastCreateFailed": "팟캐스트를 만들지 못했습니다", + "ToastPodcastCreateSuccess": "팟캐스트가 성공적으로 생성되었습니다.", + "ToastPodcastEpisodeUpdated": "에피소드 업데이트됨", + "ToastPodcastGetFeedFailed": "팟캐스트 피드를 가져오지 못했습니다.", + "ToastPodcastNoEpisodesInFeed": "RSS 피드에서 에피소드를 찾을 수 없습니다.", + "ToastPodcastNoRssFeed": "팟캐스트에 RSS 피드가 없습니다", + "ToastProgressIsNotBeingSynced": "진행 상황이 동기화되지 않습니다. 재생을 다시 시작하세요.", + "ToastProviderCreatedFailed": "공급자를 추가하지 못했습니다", + "ToastProviderCreatedSuccess": "새로운 공급자가 추가되었습니다", + "ToastProviderNameAndUrlRequired": "이름과 URL이 필요합니다", + "ToastProviderRemoveSuccess": "제공자 제거됨", + "ToastRSSFeedCloseFailed": "RSS 피드를 닫지 못했습니다.", + "ToastRSSFeedCloseSuccess": "RSS 피드가 닫혔습니다", + "ToastRemoveFailed": "제거에 실패했습니다", + "ToastRemoveItemFromCollectionFailed": "컬렉션에서 항목을 제거하지 못했습니다.", + "ToastRemoveItemFromCollectionSuccess": "컬렉션에서 항목이 제거되었습니다.", + "ToastRemoveItemsWithIssuesFailed": "이슈가 있는 라이브러리 항목을 제거하지 못했습니다.", + "ToastRemoveItemsWithIssuesSuccess": "이슈가 있는 라이브러리 항목을 제거했습니다.", + "ToastRenameFailed": "이름 바꾸기에 실패했습니다", + "ToastRescanFailed": "{0}에 대한 재스캔에 실패했습니다.", + "ToastRescanRemoved": "재스캔 완료 항목이 제거되었습니다.", + "ToastRescanUpToDate": "재스캔 완료 항목이 최신 상태였습니다.", + "ToastRescanUpdated": "재스캔 완료 항목이 업데이트되었습니다.", + "ToastScanFailed": "라이브러리 항목 스캔에 실패했습니다.", + "ToastSelectAtLeastOneUser": "최소한 한 명의 사용자를 선택하세요", + "ToastSendEbookToDeviceFailed": "기기로 전자책을 보내지 못했습니다.", + "ToastSendEbookToDeviceSuccess": "전자책이 기기 \"{0}\"로 전송되었습니다.", + "ToastSeriesSubmitFailedSameName": "같은 이름의 두 시리즈를 추가할 수 없습니다.", + "ToastSeriesUpdateFailed": "시리즈 업데이트에 실패했습니다", + "ToastSeriesUpdateSuccess": "시리즈 업데이트 성공", + "ToastServerSettingsUpdateSuccess": "서버 설정이 업데이트되었습니다.", + "ToastSessionCloseFailed": "세션을 닫지 못했습니다", + "ToastSessionDeleteFailed": "세션 삭제에 실패했습니다", + "ToastSessionDeleteSuccess": "세션이 삭제되었습니다", + "ToastSleepTimerDone": "슬립 타이머 완료... zZzzZz", + "ToastSlugMustChange": "슬러그에 잘못된 문자가 포함되어 있습니다.", + "ToastSlugRequired": "슬러그가 필요합니다", + "ToastSocketConnected": "소켓 연결됨", + "ToastSocketDisconnected": "소켓이 연결해제", + "ToastSocketFailedToConnect": "소켓 연결에 실패했습니다", + "ToastSortingPrefixesEmptyError": "최소 1개의 정렬 접두사가 있어야 합니다.", + "ToastSortingPrefixesUpdateSuccess": "정렬 접두사가 업데이트되었습니다({0}개 항목)", + "ToastTitleRequired": "제목은 필수입니다", + "ToastUnknownError": "알 수 없는 오류", + "ToastUnlinkOpenIdFailed": "OpenID에서 사용자 연결을 해제하지 못했습니다.", + "ToastUnlinkOpenIdSuccess": "OpenID에서 사용자 연결이 해제되었습니다.", + "ToastUploaderFilepathExistsError": "파일 경로 \"{0}\"이(가) 서버에 이미 있습니다.", + "ToastUploaderItemExistsInSubdirectoryError": "항목 \"{0}\"이(가) 업로드 경로의 하위 디렉토리를 사용하고 있습니다.", + "ToastUserDeleteFailed": "사용자 삭제에 실패했습니다", + "ToastUserDeleteSuccess": "사용자가 삭제되었습니다", + "ToastUserPasswordChangeSuccess": "비밀번호가 성공적으로 변경되었습니다", + "ToastUserPasswordMismatch": "비밀번호가 일치하지 않습니다", + "ToastUserPasswordMustChange": "새 비밀번호는 이전 비밀번호와 일치할 수 없습니다.", + "ToastUserRootRequireName": "루트 사용자 이름을 입력해야 합니다", + "TooltipAddChapters": "챕터 추가", + "TooltipAddOneSecond": "1초 추가", + "TooltipAdjustChapterStart": "시작 시간을 조정하려면 클릭하세요", + "TooltipLockAllChapters": "모든 챕터 잠금", + "TooltipLockChapter": "장 잠금(범위를 위해 Shift+클릭)", + "TooltipSubtractOneSecond": "1초 빼기", + "TooltipUnlockAllChapters": "모든 챕터 잠금 해제", + "TooltipUnlockChapter": "챕터 잠금 해제(범위를 보려면 Shift+클릭)" +} From 1552c250df07066c7def2da30b11d37b7c324e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Renaldas=20Repe=C4=8Dka?= Date: Tue, 25 Nov 2025 17:01:30 +0100 Subject: [PATCH 084/285] Translated using Weblate (Lithuanian) Currently translated at 59.8% (696 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/lt/ --- client/strings/lt.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/strings/lt.json b/client/strings/lt.json index 9ebec5190..b5ca66630 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -18,6 +18,7 @@ "ButtonChooseAFolder": "Pasirinkite aplanką", "ButtonChooseFiles": "Pasirinkite failus", "ButtonClearFilter": "Valyti filtrą", + "ButtonClose": "Uždaryti", "ButtonCloseFeed": "Uždaryti srautą", "ButtonCloseSession": "Uždaryti Atidarytą sesiją", "ButtonCollections": "Kolekcijos", @@ -251,7 +252,7 @@ "LabelDuration": "Trukmė", "LabelDurationFound": "Rasta trukmė:", "LabelEbook": "Elektroninė knyga", - "LabelEbooks": "Elektroninės knygos", + "LabelEbooks": "El. knygos", "LabelEdit": "Redaguoti", "LabelEmail": "El. paštas", "LabelEmailSettingsFromAddress": "Siuntėjo adresas", @@ -261,6 +262,7 @@ "LabelEmbeddedCover": "Įterptas viršelis", "LabelEnable": "Įjungti", "LabelEnd": "Pabaiga", + "LabelEndOfChapter": "Skyriaus pabaiga", "LabelEpisode": "Epizodas", "LabelEpisodeTitle": "Epizodo pavadinimas", "LabelEpisodeType": "Epizodo tipas", @@ -282,8 +284,8 @@ "LabelGenre": "Žanras", "LabelGenres": "Žanrai", "LabelHardDeleteFile": "Galutinai ištrinti failą", - "LabelHasEbook": "Turi e-knygą", - "LabelHasSupplementaryEbook": "Turi papildomą e-knygą", + "LabelHasEbook": "Turi el. knygą", + "LabelHasSupplementaryEbook": "Turi papildomą el. knygą", "LabelHost": "Serveris", "LabelHour": "Valanda", "LabelIcon": "Piktograma", From d12f7276036db0f7d0b8422c7fb85f1405872254 Mon Sep 17 00:00:00 2001 From: Artur Date: Tue, 25 Nov 2025 13:04:29 +0100 Subject: [PATCH 085/285] Translated using Weblate (Polish) Currently translated at 89.2% (1038 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 944492681..316e84234 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -588,7 +588,7 @@ "LabelSettingsChromecastSupport": "Wsparcie Chromecast", "LabelSettingsDateFormat": "Format daty", "LabelSettingsEnableWatcher": "Automatyczne monitorowanie bibliotek w poszukiwaniu zmian", - "LabelSettingsEnableWatcherForLibrary": "Automatyczne skanowanie biblioteki w poszukiwaniu zmian", + "LabelSettingsEnableWatcherForLibrary": "Automatyczne monitorowanie biblioteki w poszukiwaniu zmian", "LabelSettingsEnableWatcherHelp": "Włącza automatyczne dodawanie/aktualizację pozycji gdy wykryte zostaną zmiany w plikach. Wymaga restartu serwera", "LabelSettingsEpubsAllowScriptedContent": "Zezwalanie na skrypty w plikach epub", "LabelSettingsEpubsAllowScriptedContentHelp": "Zezwala plikom epub na wykonywanie skryptów. Zaleca się mieć to ustawienie wyłączone, chyba że ma się zaufanie do źródła plików epub.", @@ -911,8 +911,10 @@ "MessageTaskFailedToMergeAudioFiles": "Nie udało się połączyć plików audio", "MessageTaskFailedToMoveM4bFile": "Nie udało się przenieść pliku m4b", "MessageTaskFailedToWriteMetadataFile": "Niepowodzenie zapisania pliku metadanych", + "MessageTaskMatchingBooksInLibrary": "Pasujące książki w bibliotece \"{0}\"", "MessageTaskNoFilesToScan": "Brak plików do skanowania", "MessageTaskOpmlImport": "Importuj OPML", + "MessageTaskOpmlImportDescription": "Tworzenie {0} podcastów z kanałów RSS", "MessageTaskScanItemsAdded": "Dodano {0}", "MessageTaskScanItemsMissing": "Brakuje {0}", "MessageTaskScanItemsUpdated": "Zaktualizowano {0}", From 6b8464c27003ea983e18cf3752462f23c14bce1d Mon Sep 17 00:00:00 2001 From: Ivan Smoliakov Date: Sat, 29 Nov 2025 01:02:19 +0100 Subject: [PATCH 086/285] 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/ --- client/strings/ru.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/ru.json b/client/strings/ru.json index 8bebb2835..e0b048642 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -383,7 +383,7 @@ "LabelFolders": "Папки", "LabelFontBold": "Жирный", "LabelFontBoldness": "Жирность шрифта", - "LabelFontFamily": "Семейство шрифтов", + "LabelFontFamily": "Семейство шрифта", "LabelFontItalic": "Курсив", "LabelFontScale": "Масштаб шрифта", "LabelFontStrikethrough": "Зачеркнутый", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками", "LabelSettingsChromecastSupport": "Поддержка Chromecast", "LabelSettingsDateFormat": "Формат даты", - "LabelSettingsEnableWatcher": "Автоматическое сканирование библиотек на изменение", - "LabelSettingsEnableWatcherForLibrary": "Автоматическое сканирование библиотеки на изменение", + "LabelSettingsEnableWatcher": "Автоматически отслеживать изменения в библиотеках", + "LabelSettingsEnableWatcherForLibrary": "Автоматический просмотр библиотеки на изменение", "LabelSettingsEnableWatcherHelp": "Включает автоматическое добавление/обновление элементов при обнаружении изменений файлов. *Требуется перезапуск сервера", "LabelSettingsEpubsAllowScriptedContent": "Разрешение содержимого epub с скриптами", "LabelSettingsEpubsAllowScriptedContentHelp": "Разрешить файлам epub выполнять скрипты. Рекомендуется отключать этот параметр, если вы не доверяете источнику файлов epub.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Вы уверены, что хотите сбросить главы и отменить внесенные изменения?", "MessageRestoreBackupConfirm": "Вы уверены, что хотите восстановить резервную копию, созданную", "MessageRestoreBackupWarning": "Восстановление резервной копии перезапишет всю базу данных, расположенную в /config, и обложки изображений в /metadata/items и /metadata/authors.

Бэкапы не изменяют файлы в папках библиотеки. Если вы включили параметры сервера для хранения обложек и метаданных в папках библиотеки, то они не резервируются и не перезаписываются.

Все клиенты, использующие ваш сервер, будут автоматически обновлены.", - "MessageScheduleLibraryScanNote": "Большинству пользователей рекомендуется отключить эту функцию и включить функцию просмотра папок. Программа просмотра папок автоматически обнаружит изменения в папках вашей библиотеки. Программа просмотра папок работает не для каждой файловой системы (например, NFS), поэтому вместо этого можно использовать запланированные проверки библиотеки.", + "MessageScheduleLibraryScanNote": "Большинству пользователей рекомендуется отключить эту функцию и оставить включённой функцию \"Автоматически отслеживать изменения в библиотеках\" - она будет автоматически обнаруживать изменения в папках вашей библиотеки. Включите эту функцию если \"Автоматически отслеживать изменения в библиотеках\" не работает для вашей файловой системы (например, NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Запуск каждые {0} по {1}", "MessageSearchResultsFor": "Результаты поиска для", "MessageSelected": "{0} выбрано", From 9441346b0ae5311954d7604c3d0b73251224c190 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 29 Nov 2025 16:32:16 +0100 Subject: [PATCH 087/285] Added translation using Weblate (Icelandic) --- client/strings/is.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 client/strings/is.json diff --git a/client/strings/is.json b/client/strings/is.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/client/strings/is.json @@ -0,0 +1 @@ +{} From cee9b9d8e34df5256cefb3233b77df97bd68f326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Sat, 29 Nov 2025 17:17:47 +0100 Subject: [PATCH 088/285] Translated using Weblate (Finnish) Currently translated at 97.5% (1134 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index 421d5df43..bf2659ba1 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -383,7 +383,7 @@ "LabelFolders": "Kansiot", "LabelFontBold": "Lihavoitu", "LabelFontBoldness": "Kirjasintyyppien lihavointi", - "LabelFontFamily": "Kirjasinperhe", + "LabelFontFamily": "Fonttiperhe", "LabelFontItalic": "Kursiivi", "LabelFontScale": "Kirjasintyyppien skaalautuminen", "LabelFontStrikethrough": "Yliviivattu", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeuomorfinen muotoilu puisilla hyllyillä", "LabelSettingsChromecastSupport": "Chromecast-tuki", "LabelSettingsDateFormat": "Päivämäärän muoto", - "LabelSettingsEnableWatcher": "Skannaa kirjastot automaattisesti muutoksien varalta", - "LabelSettingsEnableWatcherForLibrary": "Skannaa kirjastot automaattisesti muutoksien varalta", + "LabelSettingsEnableWatcher": "Vahdi kirjastoja automaattisesti muutoksien varalta", + "LabelSettingsEnableWatcherForLibrary": "Vahdi kirjastoja automaattisesti muutoksien varalta", "LabelSettingsEnableWatcherHelp": "Ottaa käyttöön kohteiden automaattisen lisäämisen ja päivityksen kun tiedostomuutoksia havaitaan. *Tarvitsee palvelimen uudelleenkäynnistyksen", "LabelSettingsEpubsAllowScriptedContent": "Salli komentosarjamuotoinen sisältö epubissa", "LabelSettingsEpubsAllowScriptedContentHelp": "Salli epub-tiedostojen suorittaa komentosarjoja. On suositeltavaa pitää tämä asetus pois käytöstä, ellet luota epub-tiedostojen lähteeseen.", @@ -738,6 +738,8 @@ "MessageAppriseDescription": "Käyttääksesi tätä toimintoa tarvitset Apprise API -instanssin tai rajapinnan joka käsittelee samoja pyyntöjä.
Apprise rajapinnan osoite tulee olla täysi URL polku ilmoituksen lähetykseen, esim. jos rajapinta on osoitteessa http://192.168.1.1:8337,niin arvoksi tulee antaa http://192.168.1.1:8337/notify.", "MessageAsinCheck": "Varmista, että käytät ASIN-tunnusta oikealta Audible-alueelta, ei Amazonista.", "MessageAuthenticationLegacyTokenWarning": "Vanhat API-tunnukset poistetaan tulevaisuudessa. Käytä sen sijaan API-avaimia.", + "MessageAuthenticationOIDCChangesRestart": "Käynnistä palvelin uudelleen tallennuksen jälkeen ottaaksesi OIDC-muutokset käyttöön.", + "MessageAuthenticationSecurityMessage": "Tunnistautumisen tietoturvaa on parannettu. Kaikkien käyttäjien tulee kirjautua sisään uudelleen.", "MessageBackupsDescription": "Varmuuskopiot sisältävät käyttäjät, käyttäjien edistymisen, kirjastokohteiden tiedot, palvelinasetukset ja /metadata/items- ja /metadata/authors -kansioihin tallennetut kuvat. Varmuuskopiot eivät sisällä kirjastosi kansioihin tallennettuja tiedostoja.", "MessageBackupsLocationEditNote": "Huomautus: Varmuuskopion sijainnin päivittäminen ei siirrä tai muokkaa olemassa olevia varmuuskopioita", "MessageBackupsLocationNoEditNote": "Huomautus: Varmuuskopion sijainti asetetaan ympäristömuuttujan kautta, eikä sitä voi muuttaa tässä.", @@ -751,6 +753,7 @@ "MessageBookshelfNoResultsForFilter": "Ei tuloksia suodattimelle \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Ei tuloksia kyselylle", "MessageBookshelfNoSeries": "Sinulla ei ole sarjoja", + "MessageBulkChapterPattern": "Kuinka monta lukua haluaisit lisätä tällä numerointimallilla?", "MessageChapterEndIsAfter": "Luvun loppu sijaitsee äänikirjan lopun jälkeen", "MessageChapterErrorFirstNotZero": "Ensimmäisen luvun tulee alkaa nollasta", "MessageChapterErrorStartGteDuration": "Epäkelvollinen aloitusaika; on oltava lyhyempi kuin äänikirjan kesto", @@ -759,6 +762,7 @@ "MessageChaptersNotFound": "Kappaleita ei löydy", "MessageCheckingCron": "Tarkistetaan cronia...", "MessageConfirmCloseFeed": "Oletko varma, että haluat sulkea tämän syötteen?", + "MessageConfirmDeleteApiKey": "Haluatko varmasti poistaa API-avaimen \"{0}\"?", "MessageConfirmDeleteBackup": "Oletko varma, että haluat poistaa varmuuskopion {0}:lle?", "MessageConfirmDeleteDevice": "Oletko varma, että haluat poistaa s-lukulaitteen \"{0}\"?", "MessageConfirmDeleteFile": "Tämä poistaa tiedoston tiedostojärjestelmästäsi. Oletko varma?", From d4fdb47c7fe6ebb4e4cc7d3525c10196dc3afa59 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 29 Nov 2025 22:02:08 +0100 Subject: [PATCH 089/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 5f7f81cc4..8398a7211 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -383,7 +383,7 @@ "LabelFolders": "Verzeichnisse", "LabelFontBold": "Fett", "LabelFontBoldness": "Schriftstärke", - "LabelFontFamily": "Schrift Familie", + "LabelFontFamily": "Schriftfamilie", "LabelFontItalic": "Kursiv", "LabelFontScale": "Schriftgröße", "LabelFontStrikethrough": "Durchgestrichen", From 3b6419bc1b3d2819a9dc02a738ea38fa92ebdb16 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 29 Nov 2025 22:02:47 +0100 Subject: [PATCH 090/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index f2c795980..4841ec9f6 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -383,9 +383,9 @@ "LabelFolders": "Dossiers", "LabelFontBold": "Gras", "LabelFontBoldness": "Graisse de la police", - "LabelFontFamily": "Polices de caractères", + "LabelFontFamily": "Famille de caractères", "LabelFontItalic": "Italique", - "LabelFontScale": "Taille de la police de caractère", + "LabelFontScale": "Taille de la police", "LabelFontStrikethrough": "Barrer", "LabelFormat": "Format", "LabelFull": "Complet", From 329e3c71795398d93a5e12fe7045f7ca4d0c098e Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 29 Nov 2025 16:32:57 -0600 Subject: [PATCH 091/285] Add Korean language option --- client/plugins/i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index e4c179016..83e748f25 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -22,6 +22,7 @@ const languageCodeMap = { it: { label: 'Italiano', dateFnsLocale: 'it' }, lt: { label: 'Lietuvių', dateFnsLocale: 'lt' }, hu: { label: 'Magyar', dateFnsLocale: 'hu' }, + ko: { label: '한국어', dateFnsLocale: 'ko' }, nl: { label: 'Nederlands', dateFnsLocale: 'nl' }, no: { label: 'Norsk', dateFnsLocale: 'no' }, pl: { label: 'Polski', dateFnsLocale: 'pl' }, From 5f1edcb60969cbede473c5b5a6c88316d86ab935 Mon Sep 17 00:00:00 2001 From: mikiher Date: Mon, 1 Dec 2025 18:00:34 +0200 Subject: [PATCH 092/285] Wrap ensureDir in try-catch blocks --- server/managers/BackupManager.js | 31 ++++++++++++++++++++--- server/managers/CacheManager.js | 13 +++++++--- server/managers/LogManager.js | 31 +++++++++++++---------- server/managers/MigrationManager.js | 7 ++++- server/managers/PlaybackSessionManager.js | 7 ++++- 5 files changed, 67 insertions(+), 22 deletions(-) diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index 102521b71..562748c36 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -48,9 +48,34 @@ class BackupManager { } async init() { - const backupsDirExists = await fs.pathExists(this.backupPath) - if (!backupsDirExists) { - await fs.ensureDir(this.backupPath) + // Validate backupPath before attempting to create it + if (!this.backupPath || typeof this.backupPath !== 'string' || this.backupPath.length < 2) { + Logger.error(`[BackupManager] Invalid backup path configured: "${this.backupPath}". Falling back to default.`) + // Reset to default backup path + global.ServerSettings.backupPath = Path.join(global.MetadataPath, 'backups') + } + + try { + const backupsDirExists = await fs.pathExists(this.backupPath) + if (!backupsDirExists) { + await fs.ensureDir(this.backupPath) + } + } catch (error) { + Logger.error(`[BackupManager] Failed to ensure backup directory at "${this.backupPath}": ${error.message}`) + // Attempt to fall back to default path + const defaultBackupPath = Path.join(global.MetadataPath, 'backups') + if (this.backupPath !== defaultBackupPath) { + Logger.info(`[BackupManager] Attempting to use default backup path: "${defaultBackupPath}"`) + global.ServerSettings.backupPath = defaultBackupPath + try { + await fs.ensureDir(defaultBackupPath) + } catch (fallbackError) { + Logger.error(`[BackupManager] Failed to create default backup directory: ${fallbackError.message}`) + throw new Error(`[BackupManager] Failed to create default backup directory at "${defaultBackupPath}"`, { cause: fallbackError }) + } + } else { + throw new Error(`[BackupManager] Failed to ensure backup directory at "${this.backupPath}"`, { cause: error }) + } } await this.loadBackups() diff --git a/server/managers/CacheManager.js b/server/managers/CacheManager.js index b44b65de3..38c0415e9 100644 --- a/server/managers/CacheManager.js +++ b/server/managers/CacheManager.js @@ -24,10 +24,15 @@ class CacheManager { this.ImageCachePath = Path.join(this.CachePath, 'images') this.ItemCachePath = Path.join(this.CachePath, 'items') - await fs.ensureDir(this.CachePath) - await fs.ensureDir(this.CoverCachePath) - await fs.ensureDir(this.ImageCachePath) - await fs.ensureDir(this.ItemCachePath) + try { + await fs.ensureDir(this.CachePath) + await fs.ensureDir(this.CoverCachePath) + await fs.ensureDir(this.ImageCachePath) + await fs.ensureDir(this.ItemCachePath) + } catch (error) { + Logger.error(`[CacheManager] Failed to create cache directories at "${this.CachePath}": ${error.message}`) + throw new Error(`[CacheManager] Failed to create cache directories at "${this.CachePath}"`, { cause: error }) + } } async handleCoverCache(res, libraryItemId, options = {}) { diff --git a/server/managers/LogManager.js b/server/managers/LogManager.js index 731dfe70a..07bb7d401 100644 --- a/server/managers/LogManager.js +++ b/server/managers/LogManager.js @@ -37,8 +37,13 @@ class LogManager { } async ensureLogDirs() { - await fs.ensureDir(this.DailyLogPath) - await fs.ensureDir(this.ScanLogPath) + try { + await fs.ensureDir(this.DailyLogPath) + await fs.ensureDir(this.ScanLogPath) + } catch (error) { + console.error(`[LogManager] Failed to create log directories at "${this.DailyLogPath}": ${error.message}`) + throw new Error(`[LogManager] Failed to create log directories at "${this.DailyLogPath}"`, { cause: error }) + } } /** @@ -102,20 +107,20 @@ class LogManager { } /** - * - * @param {string} filename + * + * @param {string} filename */ async removeLogFile(filename) { const fullPath = Path.join(this.DailyLogPath, filename) const exists = await fs.pathExists(fullPath) if (!exists) { Logger.error(TAG, 'Invalid log dne ' + fullPath) - this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename) + this.dailyLogFiles = this.dailyLogFiles.filter((dlf) => dlf !== filename) } else { try { await fs.unlink(fullPath) Logger.info(TAG, 'Removed daily log: ' + filename) - this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename) + this.dailyLogFiles = this.dailyLogFiles.filter((dlf) => dlf !== filename) } catch (error) { Logger.error(TAG, 'Failed to unlink log file ' + fullPath) } @@ -123,8 +128,8 @@ class LogManager { } /** - * - * @param {LogObject} logObj + * + * @param {LogObject} logObj */ async logToFile(logObj) { // Fatal crashes get logged to a separate file @@ -152,8 +157,8 @@ class LogManager { } /** - * - * @param {LogObject} logObj + * + * @param {LogObject} logObj */ async logCrashToFile(logObj) { const line = JSON.stringify(logObj) + '\n' @@ -161,18 +166,18 @@ class LogManager { const logsDir = Path.join(global.MetadataPath, 'logs') await fs.ensureDir(logsDir) const crashLogPath = Path.join(logsDir, 'crash_logs.txt') - return fs.writeFile(crashLogPath, line, { flag: "a+" }).catch((error) => { + return fs.writeFile(crashLogPath, line, { flag: 'a+' }).catch((error) => { console.log('[LogManager] Appended crash log', error) }) } /** * Most recent 5000 daily logs - * + * * @returns {string} */ getMostRecentCurrentDailyLogs() { return this.currentDailyLog?.logs.slice(-5000) || '' } } -module.exports = LogManager \ No newline at end of file +module.exports = LogManager diff --git a/server/managers/MigrationManager.js b/server/managers/MigrationManager.js index 8635def10..e302038ff 100644 --- a/server/managers/MigrationManager.js +++ b/server/managers/MigrationManager.js @@ -38,7 +38,12 @@ class MigrationManager { if (!(await fs.pathExists(this.configPath))) throw new Error(`Config path does not exist: ${this.configPath}`) this.migrationsDir = path.join(this.configPath, 'migrations') - await fs.ensureDir(this.migrationsDir) + try { + await fs.ensureDir(this.migrationsDir) + } catch (error) { + Logger.error(`[MigrationManager] Failed to create migrations directory at "${this.migrationsDir}": ${error.message}`) + throw new Error(`[MigrationManager] Failed to create migrations directory at "${this.migrationsDir}"`, { cause: error }) + } this.serverVersion = this.extractVersionFromTag(serverVersion) if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index fb12fedaa..1e95727f1 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -459,7 +459,12 @@ class PlaybackSessionManager { * Remove all stream folders in `/metadata/streams` */ async removeOrphanStreams() { - await fs.ensureDir(this.StreamsPath) + try { + await fs.ensureDir(this.StreamsPath) + } catch (error) { + Logger.error(`[PlaybackSessionManager] Failed to create streams directory at "${this.StreamsPath}": ${error.message}`) + throw new Error(`[PlaybackSessionManager] Failed to create streams directory at "${this.StreamsPath}"`, { cause: error }) + } try { const streamsInPath = await fs.readdir(this.StreamsPath) for (const streamId of streamsInPath) { From d7bfccdc4a43a3f682849062b1effafb00812fee Mon Sep 17 00:00:00 2001 From: mikiher Date: Mon, 1 Dec 2025 18:54:43 +0200 Subject: [PATCH 093/285] BackupManager: Remove backup fallback logic --- server/managers/BackupManager.js | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index 562748c36..2697f94ea 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -48,13 +48,6 @@ class BackupManager { } async init() { - // Validate backupPath before attempting to create it - if (!this.backupPath || typeof this.backupPath !== 'string' || this.backupPath.length < 2) { - Logger.error(`[BackupManager] Invalid backup path configured: "${this.backupPath}". Falling back to default.`) - // Reset to default backup path - global.ServerSettings.backupPath = Path.join(global.MetadataPath, 'backups') - } - try { const backupsDirExists = await fs.pathExists(this.backupPath) if (!backupsDirExists) { @@ -62,20 +55,7 @@ class BackupManager { } } catch (error) { Logger.error(`[BackupManager] Failed to ensure backup directory at "${this.backupPath}": ${error.message}`) - // Attempt to fall back to default path - const defaultBackupPath = Path.join(global.MetadataPath, 'backups') - if (this.backupPath !== defaultBackupPath) { - Logger.info(`[BackupManager] Attempting to use default backup path: "${defaultBackupPath}"`) - global.ServerSettings.backupPath = defaultBackupPath - try { - await fs.ensureDir(defaultBackupPath) - } catch (fallbackError) { - Logger.error(`[BackupManager] Failed to create default backup directory: ${fallbackError.message}`) - throw new Error(`[BackupManager] Failed to create default backup directory at "${defaultBackupPath}"`, { cause: fallbackError }) - } - } else { - throw new Error(`[BackupManager] Failed to ensure backup directory at "${this.backupPath}"`, { cause: error }) - } + throw new Error(`[BackupManager] Failed to ensure backup directory at "${this.backupPath}"`, { cause: error }) } await this.loadBackups() From 219a9fc6d54be0a73e9cc199a55a671572c95011 Mon Sep 17 00:00:00 2001 From: Netleak Date: Sun, 30 Nov 2025 00:09:07 +0100 Subject: [PATCH 094/285] Translated using Weblate (Czech) 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/cs/ --- client/strings/cs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index 4b20c2a59..f70d6693c 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -589,7 +589,7 @@ "LabelSettingsChromecastSupport": "Podpora Chromecastu", "LabelSettingsDateFormat": "Formát data", "LabelSettingsEnableWatcher": "Automaticky skenovat změny v knihovnách", - "LabelSettingsEnableWatcherForLibrary": "Automaticky skenovat změny v knihovně", + "LabelSettingsEnableWatcherForLibrary": "Automaticky sledovat změny v knihovně", "LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru", "LabelSettingsEpubsAllowScriptedContent": "Povolení skriptovaného obsahu v epubu", "LabelSettingsEpubsAllowScriptedContentHelp": "Povolení spouštění skriptů v souborech epub. Doporučujeme toto nastavení vypnout, pokud nedůvěřujete zdroji souborů epub.", @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Přidávání položek do kolekce selhalo", "ToastCollectionRemoveSuccess": "Kolekce odstraněna", "ToastCollectionUpdateSuccess": "Kolekce aktualizována", + "ToastConnectionNotAvailable": "Připojení není k dispozici. Zkuste to prosím znovu později", + "ToastCoverSearchFailed": "Hledání obálky se nezdařilo", "ToastCoverUpdateFailed": "Aktualizace obálky selhala", "ToastDateTimeInvalidOrIncomplete": "Datum a čas jsou chybné nebo nekompletní", "ToastDeleteFileFailed": "Nepodařilo se smazat soubor", From 991d25f6284eea320aa8247080ca72e37bed0419 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 1 Dec 2025 17:13:15 -0600 Subject: [PATCH 095/285] Version bump v2.31.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 0d2699ed1..2d41d39ef 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.30.0", + "version": "2.31.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.30.0", + "version": "2.31.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 50fd0b713..33d66c276 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.30.0", + "version": "2.31.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 8eea3abbd..d703a8edc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.30.0", + "version": "2.31.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.30.0", + "version": "2.31.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 0b4ee4137..0aaf4f5af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.30.0", + "version": "2.31.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 648983708ed2c461a286f153917615d60788cffe Mon Sep 17 00:00:00 2001 From: sir-wilhelm Date: Fri, 12 Dec 2025 04:02:20 +0000 Subject: [PATCH 096/285] Sort the playlist sections alphabetically. --- client/components/modals/playlists/AddCreateModal.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/components/modals/playlists/AddCreateModal.vue b/client/components/modals/playlists/AddCreateModal.vue index e695ccb0c..f8543f1de 100644 --- a/client/components/modals/playlists/AddCreateModal.vue +++ b/client/components/modals/playlists/AddCreateModal.vue @@ -97,7 +97,10 @@ export default { ...playlist } }) - .sort((a, b) => (a.isItemIncluded ? -1 : 1)) + .sort((a, b) => { + if (a.isItemIncluded !== b.isItemIncluded) return a.isItemIncluded ? -1 : 1 + return a.name.localeCompare(b.name) + }) }, isBatch() { return this.selectedPlaylistItems.length > 1 From 503f4611b221a5bde19024e657021670df204478 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 12 Dec 2025 17:24:01 -0600 Subject: [PATCH 097/285] Update tooltip with plaintext prop --- client/components/cards/LazyBookCard.vue | 6 +++--- client/components/stats/DailyListeningChart.vue | 2 +- client/components/ui/Tooltip.vue | 15 ++++++++++++--- client/pages/audiobook/_id/chapters.vue | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index d06f083ba..51f657dbc 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -78,7 +78,7 @@
- +
priority_high
@@ -121,12 +121,12 @@
- +

{{ displayTitle }}

- +

{{ displaySubtitle }}

{{ displayLineTwo || ' ' }}

diff --git a/client/components/stats/DailyListeningChart.vue b/client/components/stats/DailyListeningChart.vue index f3a50d11c..55752fc5d 100644 --- a/client/components/stats/DailyListeningChart.vue +++ b/client/components/stats/DailyListeningChart.vue @@ -14,7 +14,7 @@
- +
diff --git a/client/components/ui/Tooltip.vue b/client/components/ui/Tooltip.vue index e6ab0117b..c858cbfec 100644 --- a/client/components/ui/Tooltip.vue +++ b/client/components/ui/Tooltip.vue @@ -22,7 +22,8 @@ export default { type: Number, default: 0 }, - disabled: Boolean + disabled: Boolean, + plaintext: Boolean }, data() { return { @@ -46,7 +47,11 @@ export default { methods: { updateText() { if (this.tooltip) { - this.tooltip.innerHTML = this.text + if (this.plaintext) { + this.tooltip.textContent = this.text + } else { + this.tooltip.innerHTML = this.text + } this.setTooltipPosition(this.tooltip) } }, @@ -58,7 +63,11 @@ export default { tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white text-xs rounded-sm shadow-lg max-w-xs text-center hidden sm:block' tooltip.style.zIndex = 100 tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)' - tooltip.innerHTML = this.text + if (this.plaintext) { + tooltip.textContent = this.text + } else { + tooltip.innerHTML = this.text + } tooltip.addEventListener('mouseover', this.cancelHide) tooltip.addEventListener('mouseleave', this.hideTooltip) diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index 4d6c0e41d..e91a8846d 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -126,7 +126,7 @@
{{ elapsedTime }}s
- + From bcfe1e96478a977b74a3b4267852d707496b8995 Mon Sep 17 00:00:00 2001 From: thehijacker Date: Tue, 2 Dec 2025 12:57:43 +0100 Subject: [PATCH 098/285] Translated using Weblate (Slovenian) 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/sl/ --- client/strings/sl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/sl.json b/client/strings/sl.json index 962ccaa80..3b7ce53a2 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -383,7 +383,7 @@ "LabelFolders": "Mape", "LabelFontBold": "Krepko", "LabelFontBoldness": "Krepkost pisave", - "LabelFontFamily": "Družina pisave", + "LabelFontFamily": "Družina pisav", "LabelFontItalic": "Ležeče", "LabelFontScale": "Merilo pisave", "LabelFontStrikethrough": "Prečrtano", From 5de942aefbf915297e91e87f6ed44bec906126c6 Mon Sep 17 00:00:00 2001 From: Napitauki Date: Mon, 1 Dec 2025 23:09:52 +0100 Subject: [PATCH 099/285] Translated using Weblate (Finnish) Currently translated at 97.6% (1136 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index bf2659ba1..db152c9f3 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -930,7 +930,7 @@ "MessageTaskScanningFileChanges": "Tarkastetaan tiedoston muutoksia \"{0}\":sta", "MessageTaskScanningLibrary": "Tarkastetaan kirjastoa \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "Kohdehakemisto ei ole kirjoitettava", - "MessageThinking": "Ajattellaan...", + "MessageThinking": "Ajatellaan...", "MessageUploaderItemFailed": "Lataaminen ulospäin epäonnistui", "MessageUploaderItemSuccess": "Onnistuneesti ladattu! ulospäin!", "MessageUploading": "Ladataan! ulospäin...", From a33e87db9965f2aabc370699c66de520a1488ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Tue, 2 Dec 2025 17:39:45 +0100 Subject: [PATCH 100/285] Translated using Weblate (Finnish) Currently translated at 97.6% (1136 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index db152c9f3..600db265a 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -815,6 +815,7 @@ "MessageFeedURLWillBe": "Syötteen URL tulee olemaan {0}", "MessageFetching": "Haetaan...", "MessageForceReScanDescription": "skannaa kaikki tiedostot uudelleen kuten uusi tarkistus. Äänitiedoston ID3-tunnisteet, OPF-tiedostot ja tekstitiedostot skannataan uusina.", + "MessageHeatmapListeningTimeTooltip": "{0} kuunnellaan on {1}", "MessageImportantNotice": "Tärkeä huomautus!", "MessageInsertChapterBelow": "Syötä luku alle", "MessageInvalidAsin": "Virheellinen ASIN", @@ -885,7 +886,7 @@ "MessageResetChaptersConfirm": "Oletko varma, että haluat nollata luvut ja kumota tekemäsi muutokset?", "MessageRestoreBackupConfirm": "Oletko varma, että haluat palauttaa varmuuskopion, joka on luotu", "MessageRestoreBackupWarning": "Varmuuskopion palauttaminen korvaa koko /config:ssa sijaitsevan tietokannan, ja kansikuvat /metadata/items & /metadata/authors:ssa.

Varmuuskopiot eivät muuta kirjastokansioissasi olevia tiedostoja. Jos olet ottanut käyttöön palvelinasetuksissa kansikuvien ja metatietojen tallentamisen kirjaston kansioihin, niitä ei varmuuskopioida tai korvata.

Kaikki palvelintasi käyttävät asiakkaat virkistetään automaattisesti.", - "MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja säilyttää kansiotarkkailu päällä. Kansiotarkkailu havaitsee automaattisesti tiedostomuutokset kirjaston kansioissa. Kansiotarkkailu ei toimi kaikille tiedostojärjestelmille (kuten NFS), jolloin voidaan käyttää ajastettuja kirjastoskannauksia.", + "MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja \"Tarkkaile kirjaston muutoksia automaattisesti\" -asetus pidetään käytössä - se havaitsee muutokset kirjastokansioissasi automaattisesti. Ota tämä ominaisuus käyttöön, jos \"Tarkkaile kirjaston muutoksia automaattisesti\" ei toimi tiedostojärjestelmässäsi (kuten NFS).\"", "MessageScheduleRunEveryWeekdayAtTime": "Suorita joka {0} klo {1}", "MessageSearchResultsFor": "Hakutulokset haulle", "MessageSelected": "{0} valittuna", From ed5766b4ab13f8b11cfce088c3fb7ada0c64dd0c Mon Sep 17 00:00:00 2001 From: Alessandro Burzio Date: Mon, 1 Dec 2025 23:05:14 +0100 Subject: [PATCH 101/285] Translated using Weblate (Italian) 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/it/ --- client/strings/it.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/it.json b/client/strings/it.json index 2354bc0ae..46ecb0f53 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -383,7 +383,7 @@ "LabelFolders": "Cartelle", "LabelFontBold": "Grassetto", "LabelFontBoldness": "Grassetto", - "LabelFontFamily": "Famiglia di caratteri", + "LabelFontFamily": "Famiglia caratteri", "LabelFontItalic": "Corsivo", "LabelFontScale": "Dimensione font", "LabelFontStrikethrough": "Barrato", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Design con scaffali in legno", "LabelSettingsChromecastSupport": "Supporto a Chromecast", "LabelSettingsDateFormat": "Formato Data", - "LabelSettingsEnableWatcher": "Scansiona le librerie Automaticamente per trovare modifiche", - "LabelSettingsEnableWatcherForLibrary": "Scansiona la libreria Automaticamente per trovare modifiche", + "LabelSettingsEnableWatcher": "Controlla automaticamente le modifiche alle librerie", + "LabelSettingsEnableWatcherForLibrary": "Controlla automaticamente le modifiche alle librerie", "LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server", "LabelSettingsEpubsAllowScriptedContent": "Consenti contenuti con script negli epub", "LabelSettingsEpubsAllowScriptedContentHelp": "Consenti ai file epub di eseguire script. Si consiglia di mantenere questa impostazione disabilitata a meno che non si ritenga attendibile l'origine dei file epub.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?", "MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su", "MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.

I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.

Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.", - "MessageScheduleLibraryScanNote": "Per la maggior parte degli utenti, si consiglia di lasciare questa funzionalità disabilitata e di mantenere abilitata l'impostazione di folder watcher. Il folder watcher rileverà automaticamente le modifiche nelle cartelle della libreria. Il folder watcher non funziona per ogni file system (come NFS), quindi è possibile utilizzare le scansioni pianificate della libreria.", + "MessageScheduleLibraryScanNote": "Per la maggior parte degli utenti, si consiglia di lasciare questa funzione disabilitata e mantenere abilitata l'impostazione “Controlla automaticamente le modifiche nella libreria”: in questo modo verranno rilevate automaticamente le modifiche nelle cartelle della libreria. Abilita questa funzione se “Controlla automaticamente le modifiche nella libreria” non funziona con il tuo file system (come NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Esegui ogni {0} alle {1}", "MessageSearchResultsFor": "cerca risultati per", "MessageSelected": "{0} selezionati", From 2e2d857ce0508852e3d3cf4858c23c3499a8e36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Dobra=C4=8Da?= Date: Sat, 6 Dec 2025 08:16:28 +0100 Subject: [PATCH 102/285] 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/ --- client/strings/hr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/hr.json b/client/strings/hr.json index f22feb656..eccd12563 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -197,7 +197,7 @@ "HeaderSetBackupSchedule": "Zakazivanje sigurnosne pohrane", "HeaderSettings": "Postavke", "HeaderSettingsDisplay": "Prikaz", - "HeaderSettingsExperimental": "Eksperimentalne funkcije", + "HeaderSettingsExperimental": "Eksperimentalne značajke", "HeaderSettingsGeneral": "Općenito", "HeaderSettingsScanner": "Skener", "HeaderSettingsSecurity": "Sigurnost", @@ -383,7 +383,7 @@ "LabelFolders": "Mape", "LabelFontBold": "Podebljano", "LabelFontBoldness": "Debljina slova", - "LabelFontFamily": "Skupina fontova", + "LabelFontFamily": "Skup pisma", "LabelFontItalic": "Kurziv", "LabelFontScale": "Veličina slova", "LabelFontStrikethrough": "Precrtano", @@ -444,7 +444,7 @@ "LabelListenAgain": "Ponovno poslušaj", "LabelLogLevelDebug": "Debug", "LabelLogLevelInfo": "Info", - "LabelLogLevelWarn": "Warn", + "LabelLogLevelWarn": "Upozorenje", "LabelLookForNewEpisodesAfterDate": "Traži nove nastavke nakon ovog datuma", "LabelLowestPriority": "Najniži prioritet", "LabelMatchConfidence": "Pouzdanost", @@ -452,7 +452,7 @@ "LabelMatchExistingUsersByDescription": "Rabi se za povezivanje postojećih korisnika. Nakon što se spoje, korisnike se prepoznaje temeljem jedinstvene oznake vašeg pružatelja SSO usluga", "LabelMaxEpisodesToDownload": "Najveći broj nastavaka za preuzimanje. 0 za neograničeno.", "LabelMaxEpisodesToDownloadPerCheck": "Najveći broj novih nastavaka za preuzimanje po provjeri", - "LabelMaxEpisodesToKeep": "Najveći broj nastavaka za čuvanje", + "LabelMaxEpisodesToKeep": "Najveći # nastavaka za čuvanje", "LabelMaxEpisodesToKeepHelp": "Ako je vrijednost 0, nema ograničenja broja. Nakon automatskog preuzimanja novog nastavka ova funkcija briše najstariji nastavak ako ih ima više od zadanog broja. Ovo briše samo jedan nastavak po novom preuzetom nastavku.", "LabelMediaPlayer": "Reproduktor medijskih sadržaja", "LabelMediaType": "Vrsta medija", From afb4108c308f6be11fde15fd2a2203434b3c9a73 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 6 Dec 2025 22:31:14 +0100 Subject: [PATCH 103/285] Added translation using Weblate (Greek) --- client/strings/el.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 client/strings/el.json diff --git a/client/strings/el.json b/client/strings/el.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/client/strings/el.json @@ -0,0 +1 @@ +{} From ee6016999560023c1c802a8e2f5194677d25ad41 Mon Sep 17 00:00:00 2001 From: zard Kim Date: Mon, 8 Dec 2025 05:44:44 +0100 Subject: [PATCH 104/285] Translated using Weblate (Korean) 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/ko/ --- client/strings/ko.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/strings/ko.json b/client/strings/ko.json index a4dea5de4..a2a4dc92a 100644 --- a/client/strings/ko.json +++ b/client/strings/ko.json @@ -57,7 +57,7 @@ "ButtonNextItemInQueue": "대기열의 다음 항목", "ButtonOk": "확인", "ButtonOpenFeed": "피드 열기", - "ButtonOpenManager": "오픈 매니저", + "ButtonOpenManager": "매니저 열기", "ButtonPause": "일시정지", "ButtonPlay": "재생", "ButtonPlayAll": "모두 재생", @@ -301,7 +301,7 @@ "LabelCoverProvider": "커버 제공자", "LabelCreatedAt": "생성일", "LabelCronExpression": "Cron Expression", - "LabelCurrent": "현재재", + "LabelCurrent": "현재", "LabelCurrently": "현재:", "LabelCustomCronExpression": "사용자 정의 Cron 표현식:", "LabelDatetime": "일시", @@ -320,7 +320,7 @@ "LabelDownload": "다운로드", "LabelDownloadNEpisodes": "{0}개 에피소드 다운로드", "LabelDownloadable": "다운로드 가능", - "LabelDuration": "Duration", + "LabelDuration": "기간", "LabelDurationComparisonExactMatch": "(정확히 일치)", "LabelDurationComparisonLonger": "({0} 더 길음)", "LabelDurationComparisonShorter": "({0} 더 짧음)", @@ -436,7 +436,7 @@ "LabelLibraryFilterSublistEmpty": "{0} 없음", "LabelLibraryItem": "라이브러리 항목", "LabelLibraryName": "라이브러리 이름", - "LabelLibrarySortByProgress": "Progress: 마지막 업데이트", + "LabelLibrarySortByProgress": "진행상황: 마지막 업데이트", "LabelLibrarySortByProgressFinished": "진행 상황: 완료", "LabelLibrarySortByProgressStarted": "진행 상황: 시작됨", "LabelLimit": "한계", @@ -606,7 +606,7 @@ "LabelSettingsLibraryMarkAsFinishedWhen": "미디어 항목을 완료된 것으로 표시", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Continue Series의 이전 책 건너뛰기", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "시리즈 계속하기 홈페이지 선반에는 시리즈 중 아직 시작하지 않은 첫 번째 책이 표시됩니다. 시리즈 중 최소 한 권은 완료되었고 진행 중인 책은 없습니다. 이 설정을 활성화하면 시작하지 않은 첫 번째 책 대신 가장 늦게 완료된 책부터 시리즈가 이어집니다.", - "LabelSettingsParseSubtitles": "Parse subtitles", + "LabelSettingsParseSubtitles": "자막 파싱", "LabelSettingsParseSubtitlesHelp": "오디오북 폴더 이름에서 자막을 추출합니다.
자막은 \" - \"로 구분해야 합니다.
즉, \"책 제목 - 여기에 자막이 있습니다\"에는 \"여기에 자막이 있습니다\"라는 자막이 있습니다.", "LabelSettingsPreferMatchedMetadata": "일치하는 메타데이터를 선호", "LabelSettingsPreferMatchedMetadataHelp": "빠른 매칭을 사용하면 매칭된 데이터가 항목 세부 정보보다 우선합니다. 기본적으로 빠른 매칭은 누락된 세부 정보만 채웁니다.", @@ -616,7 +616,7 @@ "LabelSettingsSortingIgnorePrefixesHelp": "즉, 접두사 \"the\"의 경우 책 제목 \"The Book Title\"은 \"Book Title, The\"로 정렬됩니다.", "LabelSettingsSquareBookCovers": "정사각형 책 표지를 사용", "LabelSettingsSquareBookCoversHelp": "표준 1.6:1 책 표지보다 정사각형 표지를 사용하는 것을 선호합니다.", - "LabelSettingsStoreCoversWithItem": "품목과 함께 매장 커버", + "LabelSettingsStoreCoversWithItem": "항목에 있는 커버로 저장", "LabelSettingsStoreCoversWithItemHelp": "기본적으로 표지는 /metadata/items에 저장됩니다. 이 설정을 활성화하면 표지가 라이브러리 항목 폴더에 저장됩니다. \"cover\"라는 이름의 파일 하나만 저장됩니다.", "LabelSettingsStoreMetadataWithItem": "항목과 함께 메타데이터 저장", "LabelSettingsStoreMetadataWithItemHelp": "기본적으로 메타데이터 파일은 /metadata/items에 저장되며 이 설정을 활성화하면 라이브러리 항목 폴더에 메타데이터 파일이 저장됩니다.", @@ -965,11 +965,11 @@ "PlaceholderSearchEpisode": "에피소드 검색..", "StatsAuthorsAdded": "작가가 추가되었습니다", "StatsBooksAdded": "추가된 책", - "StatsBooksAdditional": "추가된 내용은 다음과 같습니다…", + "StatsBooksAdditional": "추가된 내용은 포함…", "StatsBooksFinished": "책 완성", "StatsBooksFinishedThisYear": "올해 읽은 책이 몇 권 있어요…", "StatsBooksListenedTo": "듣는 책", - "StatsCollectionGrewTo": "너의 책 컬렉션이 다음과 같이 늘어났습니다…", + "StatsCollectionGrewTo": "나의 책 컬렉션이 늘어난…", "StatsSessions": "세션", "StatsSpentListening": "보낸 청취시간", "StatsTopAuthor": "인기 작가", From 646c861bcc0e11adf9c0abccc69f76c490885463 Mon Sep 17 00:00:00 2001 From: kfctatertot Date: Mon, 8 Dec 2025 00:33:20 +0100 Subject: [PATCH 105/285] Translated using Weblate (Arabic) Currently translated at 95.9% (1116 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/ --- client/strings/ar.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/ar.json b/client/strings/ar.json index e3e13a859..a176b9d8b 100644 --- a/client/strings/ar.json +++ b/client/strings/ar.json @@ -355,7 +355,7 @@ "LabelExample": "مثال", "LabelExpandSeries": "توسيع السلاسل", "LabelExpandSubSeries": "توسيع السلاسل الفرعية", - "LabelExplicit": "صريح", + "LabelExplicit": "محتوى صريح", "LabelExplicitChecked": "صريح (محدد)", "LabelExplicitUnchecked": "غير صريح (غير محدد)", "LabelExportOPML": "تصدير OPML", @@ -374,7 +374,7 @@ "LabelFolders": "مجلدات", "LabelFontBold": "عريض", "LabelFontBoldness": "تعريض الخط", - "LabelFontFamily": "عائلة الخط", + "LabelFontFamily": "عائلة الخطوط", "LabelFontItalic": "مائل", "LabelFontScale": "نطاق الخط", "LabelFontStrikethrough": "يتوسطه خط", @@ -570,7 +570,7 @@ "LabelSettingsBookshelfViewHelp": "تصميم يحاكي الواقع مع رفوف خشبية", "LabelSettingsChromecastSupport": "دعم Chromecast", "LabelSettingsDateFormat": "تنسيق التاريخ", - "LabelSettingsEnableWatcher": "فحص المكتبات تلقائيًا بحثًا عن تغييرات", + "LabelSettingsEnableWatcher": "مراقبة المكتبات تلقائياً بحثاً عن تغييرات", "LabelSettingsEnableWatcherForLibrary": "فحص المكتبة تلقائيًا بحثًا عن تغييرات", "LabelSettingsEnableWatcherHelp": "يمكّن الإضافة/التحديث التلقائي للعناصر عند اكتشاف تغييرات في الملفات. *يتطلب إعادة تشغيل الخادم", "LabelSettingsEpubsAllowScriptedContent": "السماح بالمحتوى النصي في ملفات epub", @@ -861,7 +861,7 @@ "MessageResetChaptersConfirm": "هل أنت متأكد أنك تريد إعادة تعيين الفصول والتراجع عن التغييرات التي أجريتها؟", "MessageRestoreBackupConfirm": "هل أنت متأكد أنك تريد استعادة النسخ الاحتياطي الذي تم إنشاؤه في", "MessageRestoreBackupWarning": "ستؤدي استعادة النسخ الاحتياطي إلى الكتابة فوق قاعدة البيانات بأكملها الموجودة في /config وصور الأغلفة في /metadata/items و /metadata/authors.

لا تعدل النسخ الاحتياطية أي ملفات في مجلدات مكتبتك. إذا قمت بتمكين إعدادات الخادم لتخزين صور الأغلفة والبيانات الوصفية في مجلدات مكتبتك، فلن يتم نسخها احتياطيًا أو الكتابة فوقها.

سيتم تحديث جميع العملاء الذين يستخدمون الخادم الخاص بك تلقائيًا.", - "MessageScheduleLibraryScanNote": "بالنسبة لمعظم المستخدمين، يوصى بترك هذه الميزة معطلة وإبقاء إعداد مراقب المجلدات ممكّنًا. سيكتشف مراقب المجلدات تلقائيًا التغييرات في مجلدات مكتبتك. لا يعمل مراقب المجلدات مع كل نظام ملفات (مثل NFS)، لذا يمكن استخدام عمليات فحص المكتبة المجدولة بدلاً من ذلك.", + "MessageScheduleLibraryScanNote": "لمعظم المستخدمين، موصى بترك هذه الميزة معطلة وإبقاء ممكّنة الأعداد، ”قم بمراقبة المكتبة تلقائاً للتغييرات“. سوف يقم بالكشف التلقائي عن تغييرات في مجلدات مكتبتك. لو لم يعمل الإعداد، \"قم بمراقبة المكتبة تلقائاً للتغييرات،“مع نظمة ملفاتك المستخدمة (مثل NFS على سبيل المثال)، فأمكِن هذه الميزة.", "MessageScheduleRunEveryWeekdayAtTime": "تشغيل كل {0} في الساعة {1}", "MessageSearchResultsFor": "نتائج البحث عن", "MessageSelected": "تم تحديد {0}", From 8bdcabf9731550c242c4d1f5fd1444b099b81a47 Mon Sep 17 00:00:00 2001 From: kfctatertot Date: Mon, 8 Dec 2025 01:27:29 +0100 Subject: [PATCH 106/285] Translated using Weblate (Spanish) Currently translated at 96.8% (1126 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/strings/es.json b/client/strings/es.json index 53bfad693..3f0ff806b 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -242,6 +242,10 @@ "LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados", "LabelAllUsersIncludingGuests": "Todos los usuarios e invitados", "LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca", + "LabelApiKeyCreated": "La clave de API “{0}” se ha creado con éxito.", + "LabelApiKeyCreatedDescription": "Asegúrate de copiar la clave de API ahora, no la volverás a ver otra vez.", + "LabelApiKeyUser": "Actuar en nombre del usuario", + "LabelApiKeyUserDescription": "Esta clave de API tendrá los mismos permisos que el usuario al que representa. En los registros se verá como si la solicitud la hubiera hecho el usuario directamente.", "LabelApiToken": "Token de la API", "LabelAppend": "Adjuntar", "LabelAudioBitrate": "Tasa de bits del audio (por ejemplo, 128k)", @@ -291,6 +295,7 @@ "LabelContinueListening": "Seguir escuchando", "LabelContinueReading": "Continuar leyendo", "LabelContinueSeries": "Continuar series", + "LabelCorsAllowed": "Orígenes CORS Permitidos", "LabelCover": "Cubierta", "LabelCoverImageURL": "URL de imagen de cubierta", "LabelCoverProvider": "Proveedor de cubiertas", @@ -524,7 +529,7 @@ "LabelPublishers": "Editores", "LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", - "LabelRSSFeedOpen": "Suministro RSS abierto", + "LabelRSSFeedOpen": "Fuente RSS Abierta", "LabelRSSFeedPreventIndexing": "Evitar indización", "LabelRSSFeedSlug": "«Slug» de suministro RSS", "LabelRSSFeedURL": "URL de suministro RSS", From f7d7c9a4f571fd69afab05db3a55f60ccb85ce26 Mon Sep 17 00:00:00 2001 From: A L Date: Mon, 8 Dec 2025 13:03:30 +0100 Subject: [PATCH 107/285] Translated using Weblate (Bulgarian) 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/bg/ --- client/strings/bg.json | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/client/strings/bg.json b/client/strings/bg.json index ca14035f8..dacd62087 100644 --- a/client/strings/bg.json +++ b/client/strings/bg.json @@ -378,6 +378,7 @@ "LabelFilterByUser": "Филтриране по Потребител", "LabelFindEpisodes": "Намери Епизоди", "LabelFinished": "Дата на приключване", + "LabelFinishedDate": "Приключено на {0}", "LabelFolder": "Папка", "LabelFolders": "Папки", "LabelFontBold": "Получерно", @@ -435,7 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Не {0}", "LabelLibraryItem": "Елемент на Библиотека", "LabelLibraryName": "Име на Библиотека", - "LabelLibrarySortByProgress": "Прогресът е обновен", + "LabelLibrarySortByProgress": "Прогрес: Последно Обновен", + "LabelLibrarySortByProgressFinished": "Прогрес: Приключено", + "LabelLibrarySortByProgressStarted": "Прогрес: Започнато", "LabelLimit": "Лимит", "LabelLineSpacing": "Междуредие", "LabelListenAgain": "Слушай отново", @@ -585,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове", "LabelSettingsChromecastSupport": "Chromecast поддръжка", "LabelSettingsDateFormat": "Формат на Дата", - "LabelSettingsEnableWatcher": "Автоматично сканиране на библиотеките за промени", - "LabelSettingsEnableWatcherForLibrary": "Автоматично сканиране на библиотеката за промени", + "LabelSettingsEnableWatcher": "Автоматично преглеждане на библиотеките за промени", + "LabelSettingsEnableWatcherForLibrary": "Автоматично преглеждане на библиотеката за промени", "LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра", "LabelSettingsEpubsAllowScriptedContent": "Позволи скриптово съдържание в epub-и", "LabelSettingsEpubsAllowScriptedContentHelp": "Позволи epub файловете да изпълняват скриптове. Препоръчително е да бъде изключено освен ако не се доверявате на източника на epub файловете.", @@ -635,6 +638,7 @@ "LabelStartTime": "Начално Време", "LabelStarted": "Стартирано", "LabelStartedAt": "Стартирано на", + "LabelStartedDate": "Започнато {0}", "LabelStatsAudioTracks": "Аудио Канали", "LabelStatsAuthors": "Автори", "LabelStatsBestDay": "Най-добър ден", @@ -780,6 +784,7 @@ "MessageConfirmPurgeCache": "Изчистването на кеша ще изтрие цялата директория в /metadata/cache.

Сигурни ли сте, че искате да премахнете директорията на кеша?", "MessageConfirmPurgeItemsCache": "Изчистването на кеша на елементите ще изтрие цялата директория в /metadata/cache/items.
Сигурни ли сте?", "MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове.

Искате ли да продължите?", + "MessageConfirmQuickMatchEpisodes": "Бързото сравняване на епизоди ще презапише детайлите, ако се намери съвпадение. Само не съвпаднали епизоди ще бъдат обновени. Сигурни ли сте?", "MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?", "MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?", "MessageConfirmRemoveAuthor": "Сигурни ли сте, че искате да премахнете автор \"{0}\"?", @@ -788,6 +793,7 @@ "MessageConfirmRemoveEpisodeNote": "Забележка: Това няма да доведе до изтриване на аудио файла, освен ако не активирате опцията \"Твърдо изтриване на файла\"", "MessageConfirmRemoveEpisodes": "Сигурни ли сте, че искате да премахнете {0} епизода?", "MessageConfirmRemoveListeningSessions": "Сигурни ли сте, че искате да премахнете {0} слушателски сесии?", + "MessageConfirmRemoveMetadataFiles": "Сигурни ли сте, че искате да премахнете всичките метаданни. {0} файлове във папките на Вашата библиотека?", "MessageConfirmRemoveNarrator": "Сигурни ли сте, че искате да премахнете разказвач \"{0}\"?", "MessageConfirmRemovePlaylist": "Сигурни ли сте, че искате да премахнете плейлиста \"{0}\"?", "MessageConfirmRenameGenre": "Сигурни ли сте, че искате да преименувате жанра \"{0}\" на \"{1}\" за всички елементи?", @@ -862,6 +868,7 @@ "MessageNoUserPlaylists": "Нямате създадени плейлисти", "MessageNoUserPlaylistsHelp": "Плейлистите за частни. Само създалият ги потребител ще може да ги вижда.", "MessageNotYetImplemented": "Още не е изпълнено", + "MessageOpmlPreviewNote": "Забележка: Това е преглед на анализирания OPML файл. Действителното заглавие на подкаста ще бъде взето от RSS фийда.", "MessageOr": "или", "MessagePauseChapter": "Пауза на глава", "MessagePlayChapter": "Пусни налчалото на глава", @@ -871,6 +878,7 @@ "MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес", "MessageQuickEmbedInProgress": "Бързото вграждане е в процес на изпълнение", "MessageQuickEmbedQueue": "Поставено в опашката за бързо вграждане ({0} в опашката)", + "MessageQuickMatchAllEpisodes": "Бързо Сравняване на Всички Епизоди", "MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.", "MessageRemoveChapter": "Премахни глава", "MessageRemoveEpisodes": "Премахни {0} епизод(и)", @@ -880,16 +888,23 @@ "MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?", "MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на", "MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.

Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.

Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.", + "MessageScheduleLibraryScanNote": "За повече потребители се препоръчва да оставят този фийчър изключен и да оставят настройката \"Автоматично преглеждане за промени в библиотеката\" включена - тя автоматично ще засече промени в папките на вашата библиотека. Включете тази настройка ако \"Автоматично преглеждане за промени в библиотеката\" не рабови на вашата файлова система (например NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}", "MessageSearchResultsFor": "Резултати от търсенето за", "MessageSelected": "{0} избрани", + "MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации.", "MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат", "MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла", + "MessageShareExpirationWillBe": "Изтичането ще бъде на {0}", "MessageShareExpiresIn": "Изтича след {0}", + "MessageShareURLWillBe": "URL за споделяне ще бъде {0}", "MessageStartPlaybackAtTime": "Започни възпроизвеждане на \"{0}\" в {1}?", + "MessageTaskAudioFileNotWritable": "На Аудио файл \"{0}\" не може да се записва", + "MessageTaskCanceledByUser": "Задачата е отказана от потребител", "MessageTaskDownloadingEpisodeDescription": "Изтегляне на епизод \"{0}\"", "MessageTaskEmbeddingMetadata": "Вграждане на метаданни", "MessageTaskEmbeddingMetadataDescription": "Вграждане на метаданни в аудиокнига \"{0}\"", + "MessageTaskEncodingM4b": "Кодиране M4B", "MessageTaskEncodingM4bDescription": "Кодиране на аудиокнига \"{0}\" в единичен m4b файл", "MessageTaskFailed": "Неуспешно", "MessageTaskFailedToBackupAudioFile": "Неуспешно създаване на разервно копие на аудио файл \"{0}\"", @@ -902,7 +917,9 @@ "MessageTaskNoFilesToScan": "Няма файлове за сканиране", "MessageTaskOpmlImport": "OPML импортиране", "MessageTaskOpmlImportDescription": "Създаване на подкасти от {0} RSS хранилки", + "MessageTaskOpmlImportFeed": "OPML импортиран фийд", "MessageTaskOpmlImportFeedDescription": "Импортиране на RSS хранилка \"{0}\"", + "MessageTaskOpmlImportFeedFailed": "Неуспешно взимане на подкаст фийд", "MessageTaskOpmlImportFeedPodcastDescription": "Създаване на подкаст \"{0}\"", "MessageTaskOpmlImportFeedPodcastExists": "На този път вече съществува подкаст", "MessageTaskOpmlImportFeedPodcastFailed": "Неуспешно създаване на подкаст", @@ -938,6 +955,7 @@ "NotificationOnBackupFailedDescription": "Изпълнява се при неуспешено създаване на резервно копие", "NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод", "NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити", + "NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод", "PlaceholderNewCollection": "Ново име на колекцията", "PlaceholderNewFolderPath": "Нов път на папката", "PlaceholderNewPlaylist": "Ново име на плейлиста", From 092c504eb1db754eed758f48ad3a6c85d484fba1 Mon Sep 17 00:00:00 2001 From: Alberto Coronado Date: Thu, 11 Dec 2025 05:30:19 +0100 Subject: [PATCH 108/285] Translated using Weblate (Spanish) Currently translated at 97.5% (1134 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/strings/es.json b/client/strings/es.json index 3f0ff806b..4a611b3de 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -359,6 +359,10 @@ "LabelExample": "Ejemplo", "LabelExpandSeries": "Ampliar serie", "LabelExpandSubSeries": "Expandir la subserie", + "LabelExpired": "Expirado", + "LabelExpiresAt": "Expira El", + "LabelExpiresInSeconds": "Expira en (segundos)", + "LabelExpiresNever": "Nunca", "LabelExplicit": "Explícito", "LabelExplicitChecked": "Explícito (marcado)", "LabelExplicitUnchecked": "No Explícito (sin marcar)", @@ -773,6 +777,7 @@ "MessageConfirmRemoveAuthor": "¿Confirma que quiere quitar el autor «{0}»?", "MessageConfirmRemoveCollection": "¿Confirma que quiere quitar la colección «{0}»?", "MessageConfirmRemoveEpisode": "¿Confirma que quiere quitar el episodio «{0}»?", + "MessageConfirmRemoveEpisodeNote": "Nota: Esto no borra el archivo de audio a menos que se active la opción \"Borrado definitivo del archivo\"", "MessageConfirmRemoveEpisodes": "¿Confirma que quiere quitar {0} episodios?", "MessageConfirmRemoveListeningSessions": "¿Confirma que quiere quitar {0} sesiones de escucha?", "MessageConfirmRemoveMetadataFiles": "¿Confirma que quiere quitar todos los archivos metadata.{0} en las carpetas de elementos de su biblioteca?", @@ -934,6 +939,8 @@ "NotificationOnBackupCompletedDescription": "Se activa cuando se completa una copia de seguridad", "NotificationOnBackupFailedDescription": "Se activa cuando falla una copia de seguridad", "NotificationOnEpisodeDownloadedDescription": "Se activa cuando se descarga automáticamente un episodio de un podcast", + "NotificationOnRSSFeedDisabledDescription": "Se activa cuando las descargas automáticas de episodios se desactivan debido a varios intentos fallidos", + "NotificationOnRSSFeedFailedDescription": "Se activa cuando la solicitud a la fuente RSS falla durante una descarga automática de episodio", "NotificationOnTestDescription": "Evento para probar el sistema de notificaciones", "PlaceholderBulkChapterInput": "Ingrese título de capítulo o use numeración (ej. 'Episodio 1', 'Capítulo 10', '1.')", "PlaceholderNewCollection": "Nuevo nombre de la colección", @@ -1072,6 +1079,7 @@ "ToastPlaylistUpdateSuccess": "Lista de reproducción actualizada", "ToastPodcastCreateFailed": "No se pudo crear el pódcast", "ToastPodcastCreateSuccess": "Se creó el pódcast correctamente", + "ToastPodcastEpisodeUpdated": "Episodio actualizado", "ToastPodcastGetFeedFailed": "No se puede obtener el podcast", "ToastPodcastNoEpisodesInFeed": "No se han encontrado episodios en el feed del RSS", "ToastPodcastNoRssFeed": "El pódcast no tiene suministro RSS", From 70e6efc3d0b6babd2f6086347a0ea6b1724b4c4b Mon Sep 17 00:00:00 2001 From: Mario Date: Wed, 10 Dec 2025 12:21:51 +0100 Subject: [PATCH 109/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 8398a7211..970c94233 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -13,7 +13,7 @@ "ButtonBack": "Zurück", "ButtonBatchEditPopulateFromExisting": "Auffüllen aus vorhandenem", "ButtonBatchEditPopulateMapDetails": "Kartendetails auffüllen", - "ButtonBrowseForFolder": "Ordnersuche", + "ButtonBrowseForFolder": "Ordner auswählen", "ButtonCancel": "Abbrechen", "ButtonCancelEncode": "Konvertierung abbrechen", "ButtonChangeRootPassword": "Hauptpasswort ändern", From c1e21d31ee0c99414b4173b3d6a8fd817668247a Mon Sep 17 00:00:00 2001 From: lambolighting Date: Thu, 11 Dec 2025 14:19:06 +0100 Subject: [PATCH 110/285] Translated using Weblate (Greek) Currently translated at 1.1% (13 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/ --- client/strings/el.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/client/strings/el.json b/client/strings/el.json index 0967ef424..2527e1988 100644 --- a/client/strings/el.json +++ b/client/strings/el.json @@ -1 +1,15 @@ -{} +{ + "ButtonAdd": "Προσθήκη", + "ButtonAuthors": "Συγγραφείς", + "ButtonBack": "Πίσω", + "ButtonCancel": "Ακύρωση", + "ButtonClearFilter": "Διαγραφή Φίλτρου", + "ButtonCloseFeed": "Κλείσιμο Τροφοδοσίας", + "ButtonCollections": "Συλλογές", + "ButtonCreate": "Δημιουργία", + "ButtonDelete": "Διαγραφή", + "ButtonHome": "Αρχική", + "ButtonIssues": "Θέματα", + "ButtonLatest": "Τελευταία", + "ButtonLibrary": "Βιβλιοθήκη" +} From dc2398a07284c3e075764ceac58eecc4c85d391b Mon Sep 17 00:00:00 2001 From: Lucas Jaksys Date: Fri, 12 Dec 2025 17:53:33 +0100 Subject: [PATCH 111/285] Translated using Weblate (Portuguese (Brazil)) 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/pt_BR/ --- client/strings/pt-br.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 9c8754c44..c6c9781cf 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -383,7 +383,7 @@ "LabelFolders": "Pastas", "LabelFontBold": "Negrito", "LabelFontBoldness": "Intensidade do negrito", - "LabelFontFamily": "Família de fonte", + "LabelFontFamily": "Família de fontes", "LabelFontItalic": "Itálico", "LabelFontScale": "Escala de fonte", "LabelFontStrikethrough": "Tachado", From 0ecbb1c3f448748a5c63cd4bc917f284abfc6d25 Mon Sep 17 00:00:00 2001 From: MODI NAVON Date: Fri, 12 Dec 2025 10:09:19 +0100 Subject: [PATCH 112/285] Translated using Weblate (Hebrew) Currently translated at 73.0% (850 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/ --- client/strings/he.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/strings/he.json b/client/strings/he.json index 9525ca6cd..0efc1ec9c 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -126,6 +126,7 @@ "HeaderAudiobookTools": "כלים לניהול קבצי ספרים קוליים", "HeaderAuthentication": "אימות", "HeaderBackups": "גיבויים", + "HeaderBulkChapterModal": "הוסף מספר פרקים", "HeaderChangePassword": "שנה סיסמה", "HeaderChapters": "פרקים", "HeaderChooseAFolder": "בחר תיקייה", @@ -197,6 +198,7 @@ "HeaderSettingsExperimental": "תכונות ניסיוניות", "HeaderSettingsGeneral": "כללי", "HeaderSettingsScanner": "סורק", + "HeaderSettingsSecurity": "אבטחה", "HeaderSettingsWebClient": "מערך", "HeaderSleepTimer": "טיימר שינה", "HeaderStatsLargestItems": "הפריטים הגדולים ביותר", @@ -237,6 +239,7 @@ "LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים", "LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים", "LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך", + "LabelApiKeyUser": "פעל בשם המשתמש", "LabelApiToken": "טוקן API", "LabelAppend": "הוסף לסוף", "LabelAudioBitrate": "קצב סיביות (לדוגמא 128k)", From 10a4777ddf4e51945a425a5f57a3c0cfc362512a Mon Sep 17 00:00:00 2001 From: lambolighting Date: Sat, 13 Dec 2025 11:01:47 +0100 Subject: [PATCH 113/285] Translated using Weblate (Greek) Currently translated at 9.0% (105 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/ --- client/strings/el.json | 94 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/client/strings/el.json b/client/strings/el.json index 2527e1988..9d12d3096 100644 --- a/client/strings/el.json +++ b/client/strings/el.json @@ -1,15 +1,107 @@ { "ButtonAdd": "Προσθήκη", + "ButtonAddApiKey": "Προσθήκη Κλειδιού API", + "ButtonAddChapters": "Προσθήκη Κεφαλαίων", + "ButtonAddDevice": "Προσθήκη Συσκευής", + "ButtonAddLibrary": "Προσθήκη Βιβλιοθήκης", + "ButtonAddPodcasts": "Προσθήκη Podcasts", + "ButtonAddUser": "Προσθήκη Χρήστη", + "ButtonAddYourFirstLibrary": "Πρόσθεσε την πρώτη σου βιβλιοθήκη", + "ButtonApply": "Εφαρμογή", + "ButtonApplyChapters": "Εφαρμογή Κεφαλαίων", "ButtonAuthors": "Συγγραφείς", "ButtonBack": "Πίσω", + "ButtonBatchEditPopulateFromExisting": "Συμπλήρωση από υπάρχοντα", + "ButtonBatchEditPopulateMapDetails": "Συμπλήρωση λεπτομερειών χάρτη", + "ButtonBrowseForFolder": "Περιήγηση για Φάκελο", "ButtonCancel": "Ακύρωση", + "ButtonCancelEncode": "Ακύρωση Κωδικοποίησης", + "ButtonChangeRootPassword": "Αλλαγή Κωδικού Πρόσβασης Root", + "ButtonCheckAndDownloadNewEpisodes": "Έλεγχος και Κατέβασμα Νέων Επεισοδίων", + "ButtonChooseAFolder": "Επιλογή φακέλου", + "ButtonChooseFiles": "Επιλογή αρχείων", "ButtonClearFilter": "Διαγραφή Φίλτρου", + "ButtonClose": "Κλείσιμο", "ButtonCloseFeed": "Κλείσιμο Τροφοδοσίας", + "ButtonCloseSession": "Κλείσιμο Ανοιχτής Συνεδρίας", "ButtonCollections": "Συλλογές", + "ButtonConfigureScanner": "Ρύθμιση Παραμέτρων Σαρωτή", "ButtonCreate": "Δημιουργία", + "ButtonCreateBackup": "Δημιουργία Αντιγράφου Ασφαλείας", "ButtonDelete": "Διαγραφή", + "ButtonDownloadQueue": "Ουρά", + "ButtonEdit": "Επεξεργασία", + "ButtonEditChapters": "Επεξεργασία Κεφαλαίων", + "ButtonEditPodcast": "Επεξεργασία Podcast", + "ButtonEnable": "Ενεργοποίηση", + "ButtonForceReScan": "Αναγκαστική Επανάληψη Σάρωσης", + "ButtonFullPath": "Πλήρης Διαδρομή", + "ButtonHide": "Απόκρυψη", "ButtonHome": "Αρχική", "ButtonIssues": "Θέματα", + "ButtonJumpBackward": "Μεταπήδηση Πίσω", + "ButtonJumpForward": "Μεταπήδηση Μπροστά", "ButtonLatest": "Τελευταία", - "ButtonLibrary": "Βιβλιοθήκη" + "ButtonLibrary": "Βιβλιοθήκη", + "ButtonLogout": "Αποσύνδεση", + "ButtonLookup": "Εύρεση", + "ButtonManageTracks": "Διαχείριση Κομματιών", + "ButtonMapChapterTitles": "Χαρτογράφηση Τίτλων Κεφαλαίων", + "ButtonMatchAllAuthors": "Αντιστοίχιση Όλων των Συγγραφέων", + "ButtonMatchBooks": "Αντιστοίχιση Βιβλίων", + "ButtonNext": "Επόμενο", + "ButtonNextChapter": "Επόμενο Κεφάλαιο", + "ButtonNextItemInQueue": "Επόμενο Αντικείμενο στην Ουρά", + "ButtonOk": "Εντάξει", + "ButtonOpenFeed": "Άνοιγμα Τροφοδοσίας", + "ButtonOpenManager": "Άνοιγμα Διαχειριστή", + "ButtonPause": "Παύση", + "ButtonPlay": "Αναπαραγωγή", + "ButtonPlayAll": "Αναπαραγωγή Όλων", + "ButtonPlaying": "Αναπαράγεται", + "ButtonPlaylists": "Λίστες Αναπαραγωγής", + "ButtonPrevious": "Προηγούμενο", + "ButtonPreviousChapter": "Προηγούμενο Κεφάλαιο", + "ButtonQueueAddItem": "Προσθήκη στην ουρά", + "ButtonQueueRemoveItem": "Αφαίρεση απ'την ουρά", + "ButtonQuickMatch": "Γρήγορη Αντιστοίχηση", + "ButtonReScan": "Επανασάρωση", + "ButtonRead": "Ανάγνωση", + "ButtonReadLess": "Ανάγνωση λιγότερων", + "ButtonReadMore": "Διάβασε περισσότερα", + "ButtonRefresh": "Ανανέωση", + "ButtonRemove": "Αφαίρεση", + "ButtonRemoveAll": "Αφαίρεση Όλων", + "ButtonRemoveAllLibraryItems": "Αφαίρεση Όλων των Αντικειμέων Βιβλιοθήκης", + "ButtonReset": "Επαναφορά", + "ButtonResetToDefault": "Επαναφορά στις προεπιλογές", + "ButtonRestore": "Επαναφορά", + "ButtonSave": "Αποθήκευση", + "ButtonSaveAndClose": "Αποθήκευση και Κλείσιμο", + "ButtonScan": "Σάρψση", + "ButtonSearch": "Αναζήτηση", + "ButtonSeries": "Σειρά", + "ButtonSubmit": "Υποβολή", + "ButtonYes": "Ναι", + "HeaderAccount": "Λογαριασμός", + "HeaderAdvanced": "Για Προχωρημένους", + "HeaderAudioTracks": "Κομμάτια Ήχου", + "HeaderChapters": "Κεφάλαια", + "HeaderCollection": "Συλλογή", + "HeaderCollectionItems": "Αντικείμενα Συλλογής", + "HeaderDetails": "Λεπτομέρειες", + "HeaderEbookFiles": "Αρχεία Ebook", + "HeaderEpisodes": "Επεισόδια", + "HeaderEreaderSettings": "Ρυθμίσεις Ereader", + "HeaderLatestEpisodes": "Τελευταία Επεισόδια", + "HeaderLibraries": "Βιβλιοθήκες", + "HeaderOpenRSSFeed": "Άνοιγμα Τροφοδοσίας RSS", + "HeaderPlaylist": "Λίστα Αναπαραγωγής", + "HeaderPlaylistItems": "Αντικείμενα Λίστας Αναπαραγωγής", + "HeaderRSSFeedGeneral": "Λεπτομέρειες RSS", + "HeaderRSSFeedIsOpen": "Η Τροφοδοσία RSS είναι Ανοιχτή", + "HeaderSettings": "Ρυθμίσεις", + "HeaderStatsMinutesListeningChart": "Λεπτά Ακρόασης (τελευταίες 7 ημέρες)", + "HeaderStatsRecentSessions": "Πρόσφατες Συνεδρίες", + "HeaderTableOfContents": "Πίνακας Περιεχομένων" } From 076ece6fe748fa4b0b44ea3c8e89cb940333781f Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 21 Dec 2025 14:45:04 -0600 Subject: [PATCH 114/285] Auto-formatting --- server/scanner/Scanner.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index cf5c87b78..af4405987 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -260,16 +260,16 @@ class Scanner { // Update filter data Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id) } - await Database.bookAuthorModel - .create({ - authorId: author.id, - bookId: libraryItem.media.id - }) - .then(() => { - Logger.info(`[Scanner] quickMatchBookBuildUpdatePayload: Added author "${author.name}" to "${libraryItem.media.title}"`) - libraryItem.media.authors.push(author) - hasAuthorUpdates = true - }) + await Database.bookAuthorModel + .create({ + authorId: author.id, + bookId: libraryItem.media.id + }) + .then(() => { + Logger.info(`[Scanner] quickMatchBookBuildUpdatePayload: Added author "${author.name}" to "${libraryItem.media.title}"`) + libraryItem.media.authors.push(author) + hasAuthorUpdates = true + }) } const authorsRemoved = libraryItem.media.authors.filter((a) => !matchData.author.find((ma) => ma.toLowerCase() === a.name.toLowerCase())) if (authorsRemoved.length) { From 7b37c98e88309f67b467fea5fcf71e4736984a0d Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 21 Dec 2025 15:38:34 -0600 Subject: [PATCH 115/285] Book tags genres dedupe (#4927) * Update Audible provider dedupe genres/tags and return tags as array * Update custom metadata provider to dedupe tags/genres and return tags as array --- server/providers/Audible.js | 13 ++++++++---- server/providers/CustomProviderAdapter.js | 25 +++++++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/server/providers/Audible.js b/server/providers/Audible.js index 2c12ffc1a..6ba01aa83 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -57,8 +57,13 @@ class Audible { }) } - const genresFiltered = genres ? genres.filter((g) => g.type == 'genre').map((g) => g.name) : [] - const tagsFiltered = genres ? genres.filter((g) => g.type == 'tag').map((g) => g.name) : [] + let genresCleaned = null + let tagsCleaned = null + + if (genres && Array.isArray(genres)) { + genresCleaned = [...new Set(genres.filter((g) => g.type == 'genre').map((g) => g.name))] + tagsCleaned = [...new Set(genres.filter((g) => g.type == 'tag').map((g) => g.name))] + } return { title, @@ -71,8 +76,8 @@ class Audible { cover: image, asin, isbn, - genres: genresFiltered.length ? genresFiltered : null, - tags: tagsFiltered.length ? tagsFiltered.join(', ') : null, + genres: genresCleaned.length ? genresCleaned : null, + tags: tagsCleaned.length ? tagsCleaned : null, series: series.length ? series : null, language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null, duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0, diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index c079a1280..5c8cad75a 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -89,6 +89,27 @@ class CustomProviderAdapter { }) .filter((s) => s !== undefined) } + /** + * Validates and dedupes tags/genres array + * Can be comma separated string or array of strings + * @param {string|string[]} tagsGenres + * @returns {string[]} + */ + const validateTagsGenresArray = (tagsGenres) => { + if (!tagsGenres || (typeof tagsGenres !== 'string' && !Array.isArray(tagsGenres))) return undefined + + // If string, split by comma and trim each item + if (typeof tagsGenres === 'string') tagsGenres = tagsGenres.split(',') + // If array, ensure all items are strings + else if (!tagsGenres.every((t) => typeof t === 'string')) return undefined + + // Trim and filter out empty strings + tagsGenres = tagsGenres.map((t) => t.trim()).filter(Boolean) + if (!tagsGenres.length) return undefined + + // Dedup + return [...new Set(tagsGenres)] + } // re-map keys to throw out return matches.map((match) => { @@ -105,8 +126,8 @@ class CustomProviderAdapter { cover: toStringOrUndefined(cover), isbn: toStringOrUndefined(isbn), asin: toStringOrUndefined(asin), - genres: Array.isArray(genres) && genres.every((g) => typeof g === 'string') ? genres : undefined, - tags: toStringOrUndefined(tags), + genres: validateTagsGenresArray(genres), + tags: validateTagsGenresArray(tags), series: validateSeriesArray(series), language: toStringOrUndefined(language), duration: !isNaN(duration) && duration !== null ? Number(duration) : undefined From 3c2eec8279d82185cd858bd0a856b865160e2099 Mon Sep 17 00:00:00 2001 From: FiendFEARing Date: Wed, 17 Dec 2025 01:55:49 +0100 Subject: [PATCH 116/285] 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/ --- client/strings/zh-cn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 1a95256ab..64c41619e 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -275,7 +275,7 @@ "LabelBonus": "额外", "LabelBooks": "图书", "LabelButtonText": "按钮文本", - "LabelByAuthor": "由 {0}", + "LabelByAuthor": "作者: {0}", "LabelChangePassword": "修改密码", "LabelChannels": "声道", "LabelChapterCount": "{0} 章节", From ac08e897ee93cd2457d13716d10be94e5d0b0535 Mon Sep 17 00:00:00 2001 From: Ahetek Date: Mon, 15 Dec 2025 21:54:40 +0100 Subject: [PATCH 117/285] Translated using Weblate (Polish) Currently translated at 89.2% (1038 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 316e84234..32c9766ca 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -233,8 +233,8 @@ "LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji", "LabelAddToPlaylist": "Dodaj do playlisty", "LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty", - "LabelAddedAt": "Dodano w", - "LabelAddedDate": "Dodano", + "LabelAddedAt": "Dodano", + "LabelAddedDate": "Dodano {0}", "LabelAdminUsersOnly": "Tylko użytkownicy administracyjni", "LabelAll": "Wszystkie", "LabelAllEpisodesDownloaded": "Wszystkie odcinki pobrane", From cc48d9f26dc3dc0c2b096b9ba892c501609bfbd9 Mon Sep 17 00:00:00 2001 From: lambolighting Date: Fri, 19 Dec 2025 11:21:03 +0100 Subject: [PATCH 118/285] Translated using Weblate (Greek) Currently translated at 26.9% (313 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/ --- client/strings/el.json | 210 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 1 deletion(-) diff --git a/client/strings/el.json b/client/strings/el.json index 9d12d3096..881bd971b 100644 --- a/client/strings/el.json +++ b/client/strings/el.json @@ -49,6 +49,7 @@ "ButtonMapChapterTitles": "Χαρτογράφηση Τίτλων Κεφαλαίων", "ButtonMatchAllAuthors": "Αντιστοίχιση Όλων των Συγγραφέων", "ButtonMatchBooks": "Αντιστοίχιση Βιβλίων", + "ButtonNevermind": "Άστο", "ButtonNext": "Επόμενο", "ButtonNextChapter": "Επόμενο Κεφάλαιο", "ButtonNextItemInQueue": "Επόμενο Αντικείμενο στην Ουρά", @@ -62,8 +63,13 @@ "ButtonPlaylists": "Λίστες Αναπαραγωγής", "ButtonPrevious": "Προηγούμενο", "ButtonPreviousChapter": "Προηγούμενο Κεφάλαιο", + "ButtonProbeAudioFile": "Ανάλυση Αρχείου Ήχου", + "ButtonPurgeAllCache": "Εκκαθάριση Όλης της Προσωρινής Μνήμης", + "ButtonPurgeItemsCache": "Εκκαθάριση της Μνήμης Αντικειμένων", "ButtonQueueAddItem": "Προσθήκη στην ουρά", "ButtonQueueRemoveItem": "Αφαίρεση απ'την ουρά", + "ButtonQuickEmbed": "Γρήγορη Ενσωμάτωση", + "ButtonQuickEmbedMetadata": "Γρήγορη Ενσωμάτωση Μεταδεδομένων", "ButtonQuickMatch": "Γρήγορη Αντιστοίχηση", "ButtonReScan": "Επανασάρωση", "ButtonRead": "Ανάγνωση", @@ -73,35 +79,237 @@ "ButtonRemove": "Αφαίρεση", "ButtonRemoveAll": "Αφαίρεση Όλων", "ButtonRemoveAllLibraryItems": "Αφαίρεση Όλων των Αντικειμέων Βιβλιοθήκης", + "ButtonRemoveFromContinueListening": "Αφαίρεση από τη Συνέχεια Ακρόασης", + "ButtonRemoveFromContinueReading": "Αφαίρεση από τη Συνέχεια Ανάγνωσης", + "ButtonRemoveSeriesFromContinueSeries": "Αφαίρεση Σειράς από τη Συνέχεια Σειράς", "ButtonReset": "Επαναφορά", "ButtonResetToDefault": "Επαναφορά στις προεπιλογές", "ButtonRestore": "Επαναφορά", "ButtonSave": "Αποθήκευση", "ButtonSaveAndClose": "Αποθήκευση και Κλείσιμο", + "ButtonSaveTracklist": "Αποθήκευση Λίστας Κομματιών", "ButtonScan": "Σάρψση", + "ButtonScanLibrary": "Σάρωση Βιβλιοθήκης", + "ButtonScrollLeft": "Κύλιση Αριστερά", + "ButtonScrollRight": "Κύλιση Δεξιά", "ButtonSearch": "Αναζήτηση", + "ButtonSelectFolderPath": "Επιλογή Διαδρομής Φακέλου", "ButtonSeries": "Σειρά", + "ButtonSetChaptersFromTracks": "Ορισμός κεφαλαίων από τα κομμάτια", + "ButtonShare": "Κοινοποίηση", + "ButtonShiftTimes": "Χρόνοι Μετακίνησης", + "ButtonShow": "Εμφάνιση", + "ButtonStartM4BEncode": "Έναρξη Κωδικοποίησης M4B", + "ButtonStats": "Στατιστικά", "ButtonSubmit": "Υποβολή", + "ButtonTest": "Δοκιμή", + "ButtonUpload": "Μεταφόρτωση", + "ButtonUploadBackup": "Μεταφόρτωση Αντιγράφου Ασφαλείας", + "ButtonUploadCover": "Μεταφόρτωση Εξωφύλλου", + "ButtonUploadOPMLFile": "Μεταφόρτωση Αρχείου OPML", + "ButtonUserDelete": "Διαγραφή Χρήστη {0}", + "ButtonUserEdit": "Επεξεργασίας χρήστη {0}", + "ButtonViewAll": "Εμφάνιση Όλων", "ButtonYes": "Ναι", + "ErrorUploadLacksTitle": "Πρέπει να έχει τίτλο", "HeaderAccount": "Λογαριασμός", "HeaderAdvanced": "Για Προχωρημένους", + "HeaderApiKeys": "Κλειδιά API", "HeaderAudioTracks": "Κομμάτια Ήχου", + "HeaderBackups": "Αντίγραφα Ασφαλείας", + "HeaderBulkChapterModal": "Προσθήκη Πολλαπλών Κεφαλαίων", + "HeaderChangePassword": "Αλλαγή Κωδικού Πρόσβασης", "HeaderChapters": "Κεφάλαια", + "HeaderChooseAFolder": "Επιλογή Φακέλου", "HeaderCollection": "Συλλογή", "HeaderCollectionItems": "Αντικείμενα Συλλογής", + "HeaderCover": "Εξώφυλλο", + "HeaderCurrentDownloads": "Τρέχουσες Λήψεις", "HeaderDetails": "Λεπτομέρειες", + "HeaderDownloadQueue": "Ουρά Λήψης", "HeaderEbookFiles": "Αρχεία Ebook", + "HeaderEmail": "Ηλεκτρονικό Ταχυδρομίο", + "HeaderEmailSettings": "Ρυθμίσεις Ηλεκτρονικού Ταχυδρομίου", "HeaderEpisodes": "Επεισόδια", "HeaderEreaderSettings": "Ρυθμίσεις Ereader", + "HeaderFiles": "Αρχεία", + "HeaderFindChapters": "Εύρεση Κεφαλαίων", + "HeaderItemFiles": "Αρχεία Αντικειμένων", + "HeaderLastListeningSession": "Τελευταία Συνεδρία Ακρόασης", "HeaderLatestEpisodes": "Τελευταία Επεισόδια", "HeaderLibraries": "Βιβλιοθήκες", + "HeaderLibraryFiles": "Αρχεία Βιβλιοθήκης", + "HeaderLibraryStats": "Στατιστικά Βιβλιοθήκης", + "HeaderListeningSessions": "Συνεδρίες Ακρόασης", + "HeaderListeningStats": "Στατιστικά Ακρόασης", + "HeaderMatch": "Ταύτιση", + "HeaderNewAccount": "Νέος Λογαριασμός", + "HeaderNewApiKey": "Νέο Κλειδί API", + "HeaderNewLibrary": "Νέα Βιβλιοθήκη", + "HeaderNotificationCreate": "Δημιουργία Ειδοποίησης", + "HeaderNotificationUpdate": "Ενημέρωση Ειδοποίησης", + "HeaderNotifications": "Ειδοποιήσεις", "HeaderOpenRSSFeed": "Άνοιγμα Τροφοδοσίας RSS", + "HeaderOtherFiles": "Άλλα Αρχεία", + "HeaderPermissions": "Δικαιώματα", + "HeaderPlayerSettings": "Ρυθμίσεις Αναπαραγωγής", "HeaderPlaylist": "Λίστα Αναπαραγωγής", "HeaderPlaylistItems": "Αντικείμενα Λίστας Αναπαραγωγής", + "HeaderPresets": "Προεπιλογές", "HeaderRSSFeedGeneral": "Λεπτομέρειες RSS", "HeaderRSSFeedIsOpen": "Η Τροφοδοσία RSS είναι Ανοιχτή", + "HeaderRemoveEpisode": "Αφαίρεση Επεισοδίου", + "HeaderSession": "Συνεδρία", + "HeaderSetBackupSchedule": "Ορισμός Προγράμματος Αντιγράφων Ασφαλείας", "HeaderSettings": "Ρυθμίσεις", + "HeaderSettingsDisplay": "Προβολή", + "HeaderSettingsGeneral": "Γενικά", + "HeaderSettingsSecurity": "Ασφάλεια", + "HeaderSleepTimer": "Χρονοδιακόπτης Ύπνου", + "HeaderStatsLargestItems": "Μεγαλύτερα Αντικείμενα", + "HeaderStatsLongestItems": "Μεγαλύτερα Αντικείμενα (ώρες)", "HeaderStatsMinutesListeningChart": "Λεπτά Ακρόασης (τελευταίες 7 ημέρες)", "HeaderStatsRecentSessions": "Πρόσφατες Συνεδρίες", - "HeaderTableOfContents": "Πίνακας Περιεχομένων" + "HeaderStatsTop10Authors": "10 Κορυφαίου Συγγραφείς", + "HeaderStatsTop5Genres": "5 Κορυφαία Είδη", + "HeaderTableOfContents": "Πίνακας Περιεχομένων", + "HeaderTools": "Εργαλεία", + "HeaderUpdateAccount": "Ενημέρωση Λογαριασμού", + "HeaderUpdateApiKey": "Ενημέρωση Κλειδιού API", + "HeaderUpdateAuthor": "Ενημέρωση Συγγραφέα", + "HeaderUpdateDetails": "Ενημέρωση Λεπτομερειεών", + "HeaderUpdateLibrary": "Ενημέρωση Βιβλιοθήκης", + "HeaderUsers": "Χρήστες", + "HeaderYourStats": "Τα Στατιστικά Σας", + "LabelAbridged": "Συνοπτικό", + "LabelAccessibleBy": "Προσβάσιμο από", + "LabelAccountType": "Τύπος Λογαριασμού", + "LabelAccountTypeAdmin": "Διαχειριστής", + "LabelAccountTypeGuest": "Επισκέπτης", + "LabelAccountTypeUser": "Χρήστης", + "LabelAddToCollection": "Προσθήκη σε Συλλογή", + "LabelAddToCollectionBatch": "Προσθήκη {0} Βιβλίων στην Συλλογή", + "LabelAddToPlaylist": "Προσθήκη στην Λίστα Αναπαραγωγής", + "LabelAddedAt": "Προστέθηκε Στις", + "LabelAddedDate": "Προστέθηκε {0}", + "LabelAll": "Όλα", + "LabelAllEpisodesDownloaded": "Όλα τα επεισόδια λήφθηκαν", + "LabelAllUsers": "Όλοι οι Χρήστες", + "LabelAlreadyInYourLibrary": "Υπάρχει ήδη στην βιβλιοθήκη", + "LabelAudioChannels": "Κανάλια Ήχου (1 ή 2)", + "LabelAuthor": "Συγγραφέας", + "LabelAuthorFirstLast": "Συγγραφέας (Όνομα Επώνυμο)", + "LabelAuthorLastFirst": "Συγγραφέας (Επώνυμο, Όνομα)", + "LabelAuthors": "Συγγραφείς", + "LabelAutoDownloadEpisodes": "Αυτόματο Κατέβασμα Επεισοδίων", + "LabelAutoLaunch": "Αυτόματη Εκκίνηση", + "LabelBackupLocation": "Τοποθεσία Αντιγράφου Ασφαλείας", + "LabelBackupsEnableAutomaticBackups": "Αυτόματα αντίγραφα ασφαλείας", + "LabelBackupsNumberToKeep": "Αριθμός αντιγράφων ασφαλείας προς διατήρηση", + "LabelBooks": "Βιβλία", + "LabelButtonText": "Κείμενο Κουμπιού", + "LabelByAuthor": "κατά {0}", + "LabelChangePassword": "Αλλαγή Κωδικού Πρόσβασης", + "LabelChannels": "Κανάλια", + "LabelChapterCount": "{0} Κεφάλαια", + "LabelChapterTitle": "Τίτλος Κεφαλαίου", + "LabelChapters": "Κεφάλαια", + "LabelChaptersFound": "κεφάλαια βρέθηκαν", + "LabelClosePlayer": "Κλείσιμο αναπαραγωγής", + "LabelCollection": "Συλλογή", + "LabelCollections": "Συλλογές", + "LabelComplete": "Ολοκλήρωση", + "LabelConfirmPassword": "Επιβεβαίωση Κωδικού Πρόσβασης", + "LabelContinueListening": "Συνέχεια Ακρόασης", + "LabelContinueReading": "Συνέχεια Ανάγνωσης", + "LabelContinueSeries": "Συνέχεια Σειράς", + "LabelCover": "Εξώφυλλο", + "LabelCoverImageURL": "URL Εικόνας Εξωφύλλου", + "LabelCoverProvider": "Πάροχος Εξωφύλλου", + "LabelCreatedAt": "Δημιουρήθηκε Στις", + "LabelCurrent": "Τρέχων", + "LabelCurrently": "Τρέχων:", + "LabelDays": "Ημέρες", + "LabelDescription": "Περιγραφή", + "LabelDevice": "Συσκευή", + "LabelDeviceInfo": "Πληροφορίες Συσκευής", + "LabelDownload": "Λήψη", + "LabelDownloadNEpisodes": "Λήψη {0} επεισοδίων", + "LabelDuration": "Διάρκεια", + "LabelDurationComparisonExactMatch": "(ακριβής ταύτιση)", + "LabelEbook": "Ebook", + "LabelEbooks": "Ebooks", + "LabelEdit": "Επεξεργασία", + "LabelEmail": "Ηλεκτρονικό Ταχυδρομίο", + "LabelEmailSettingsFromAddress": "Από Διεύθυνση", + "LabelEmailSettingsSecure": "Ασφαλές", + "LabelEmailSettingsTestAddress": "Δοκιμή Διεύθυνσης", + "LabelEmbeddedCover": "Ενσωματωμένο Εξώφυλλο", + "LabelEnable": "Ενεργοποίηση", + "LabelEnd": "Τέλος", + "LabelEndOfChapter": "Τέλος Κεφαλαίου", + "LabelEpisode": "Επεισόδιο", + "LabelFile": "Αρχείο", + "LabelFilename": "Όνομα Αρχείου", + "LabelFinished": "Ολοκληρώθηκε", + "LabelFolder": "Φάκελος", + "LabelFontFamily": "Οικογένεια Γραμματοσειράς", + "LabelGenre": "Είδος", + "LabelGenres": "Είδη", + "LabelHost": "Διακομιστής", + "LabelInProgress": "Σε Εξέλιξη", + "LabelLanguage": "Γλώσσα", + "LabelLayoutSinglePage": "Μονή Σελίδα", + "LabelListenAgain": "Επανάληψη Ακρόασης", + "LabelMediaType": "Τύπος Πολυμέσων", + "LabelMore": "Περισσότερα", + "LabelMoreInfo": "Περισσότερες Πληροφορίες", + "LabelName": "Όνομα", + "LabelNarrator": "Αφηγητής", + "LabelNarrators": "Αφηγητές", + "LabelNewestAuthors": "Πρόσφατοι Συγγραφείς", + "LabelNewestEpisodes": "Πρόσφατα Επεισόδια", + "LabelNotStarted": "Δεν Έχει Ξεκινήσει", + "LabelNumberOfEpisodes": "# Επεισοδίων", + "LabelPassword": "Κωδικός Πρόσβασης", + "LabelPath": "Διαδρομή", + "LabelProgress": "Πρόοδος", + "LabelPublishYear": "Χρονολογία Έκδοσης", + "LabelPublishedDate": "Εκδόθηκε {0}", + "LabelRandomly": "Τυχαία", + "LabelRead": "Ανάγνωση", + "LabelReadAgain": "Ανάγνωση Ξανά", + "LabelRecentSeries": "Πρόσφατη Σειρά", + "LabelRecentlyAdded": "Προστέθηκαν Πρόσφατα", + "LabelSeries": "Σειρά", + "LabelSetEbookAsPrimary": "Ορισμός ως πρωτεύων", + "LabelShowAll": "Εμφάνιση Όλων", + "LabelSize": "Μέγεθος", + "LabelSleepTimer": "Χρονοδιακόπτης Ύπνου", + "LabelStart": "Έναρξη", + "LabelStatsBestDay": "Καλύτερη Ημέρα", + "LabelStatsDailyAverage": "Ημερήσιος Μέσος Όρος", + "LabelStatsDays": "Ημέρες", + "LabelStatsDaysListened": "Ημέρες Ακρόασης", + "LabelStatsInARow": "Σε σειρά", + "LabelStatsItemsFinished": "Ολοκληρωμένα Αντικείμενα", + "LabelStatsMinutes": "λεπτά", + "LabelStatsMinutesListening": "Λεπτά Ακρόασης", + "LabelStatsWeekListening": "Εβδομαδιαία Ακρόαση", + "LabelTheme": "Θέμα", + "LabelThemeDark": "Σκοτεινό", + "LabelThemeLight": "Φωτεινό", + "LabelTimeRemaining": "{0} απομένουν", + "LabelTitle": "Τίτλος", + "LabelTracks": "Κομμάτια", + "LabelType": "Τύπος", + "LabelUnknown": "Άγνωστο", + "LabelUser": "Χρήστης", + "LabelUsername": "Όνομα Χρήστη", + "LabelYourProgress": "Η Πρόοδος Σας", + "MessageDownloadingEpisode": "Λήψη επεισοδίου", + "MessageLoading": "Φόρτωση...", + "MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο", + "MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα", + "MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής" } From 81e96df9c5f8db502e3d9740ee8cb6b718c5a074 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 21 Dec 2025 15:54:07 -0600 Subject: [PATCH 119/285] Version bump v2.32.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 2d41d39ef..568274c80 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.31.0", + "version": "2.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.31.0", + "version": "2.32.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 33d66c276..df71f6256 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.31.0", + "version": "2.32.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index d703a8edc..4c9e9b9ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.31.0", + "version": "2.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.31.0", + "version": "2.32.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 0aaf4f5af..2b0d0b6cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.31.0", + "version": "2.32.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 04eb3bc43733533516566fb3cd9ca5c32e034d4d Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 23 Dec 2025 16:44:29 -0600 Subject: [PATCH 120/285] Fix server crash on audible match #4931 --- server/providers/Audible.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/providers/Audible.js b/server/providers/Audible.js index 6ba01aa83..133d3c0d8 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -57,8 +57,8 @@ class Audible { }) } - let genresCleaned = null - let tagsCleaned = null + let genresCleaned = [] + let tagsCleaned = [] if (genres && Array.isArray(genres)) { genresCleaned = [...new Set(genres.filter((g) => g.type == 'genre').map((g) => g.name))] From 4bf15bbffd395f6257a4718d784b301fb710679f Mon Sep 17 00:00:00 2001 From: Ivan Smoliakov Date: Mon, 22 Dec 2025 11:56:18 +0100 Subject: [PATCH 121/285] 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/ --- client/strings/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/ru.json b/client/strings/ru.json index e0b048642..c84fe9dcf 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -275,7 +275,7 @@ "LabelBonus": "Бонус", "LabelBooks": "Книги", "LabelButtonText": "Текст кнопки", - "LabelByAuthor": "{0}", + "LabelByAuthor": "от {0}", "LabelChangePassword": "Изменить пароль", "LabelChannels": "Ленты", "LabelChapterCount": "{0} Главы", From f3e90bd4206710a2e7f2e6d06edebcc6a1e474d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Mon, 22 Dec 2025 07:13:02 +0100 Subject: [PATCH 122/285] Translated using Weblate (Finnish) 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/fi/ --- client/strings/fi.json | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index 600db265a..dde7009ea 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Kirjat", "LabelButtonText": "Painikkeen teksti", - "LabelByAuthor": "tekijältä {0}", + "LabelByAuthor": "Tekijältä: {0}", "LabelChangePassword": "Vaihda salasana", "LabelChannels": "Kanavat", "LabelChapterCount": "{0} lukua", @@ -790,6 +790,7 @@ "MessageConfirmRemoveAuthor": "Oletko varma, että haluat poistaa tekijän \"{0}\"?", "MessageConfirmRemoveCollection": "Oletko varma, että haluat poistaa kokoelman \"{0}\"?", "MessageConfirmRemoveEpisode": "Oletko varma, että haluat poistaa jakson \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Huomioi: Tämä ei poista äänitiedostoa, ellei \"Poista tiedosto pysyvästi\" -asetusta ole valittuna", "MessageConfirmRemoveEpisodes": "Oletko varma, että haluat poistaa {0} jaksoa?", "MessageConfirmRemoveListeningSessions": "Oletko varma, että haluat poistaa {0} kuuntelukertaa?", "MessageConfirmRemoveMetadataFiles": "Oletko varma, että haluat poistaa kaikki metadata.{0}-tiedostot kirjaston kohdekansioista?", @@ -816,6 +817,7 @@ "MessageFetching": "Haetaan...", "MessageForceReScanDescription": "skannaa kaikki tiedostot uudelleen kuten uusi tarkistus. Äänitiedoston ID3-tunnisteet, OPF-tiedostot ja tekstitiedostot skannataan uusina.", "MessageHeatmapListeningTimeTooltip": "{0} kuunnellaan on {1}", + "MessageHeatmapNoListeningSessions": "Ei kuuntelujaksoja {0}", "MessageImportantNotice": "Tärkeä huomautus!", "MessageInsertChapterBelow": "Syötä luku alle", "MessageInvalidAsin": "Virheellinen ASIN", @@ -886,10 +888,11 @@ "MessageResetChaptersConfirm": "Oletko varma, että haluat nollata luvut ja kumota tekemäsi muutokset?", "MessageRestoreBackupConfirm": "Oletko varma, että haluat palauttaa varmuuskopion, joka on luotu", "MessageRestoreBackupWarning": "Varmuuskopion palauttaminen korvaa koko /config:ssa sijaitsevan tietokannan, ja kansikuvat /metadata/items & /metadata/authors:ssa.

Varmuuskopiot eivät muuta kirjastokansioissasi olevia tiedostoja. Jos olet ottanut käyttöön palvelinasetuksissa kansikuvien ja metatietojen tallentamisen kirjaston kansioihin, niitä ei varmuuskopioida tai korvata.

Kaikki palvelintasi käyttävät asiakkaat virkistetään automaattisesti.", - "MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja \"Tarkkaile kirjaston muutoksia automaattisesti\" -asetus pidetään käytössä - se havaitsee muutokset kirjastokansioissasi automaattisesti. Ota tämä ominaisuus käyttöön, jos \"Tarkkaile kirjaston muutoksia automaattisesti\" ei toimi tiedostojärjestelmässäsi (kuten NFS).\"", + "MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja \"Tarkkaile kirjaston muutoksia automaattisesti\" -asetus pidetään käytössä - se havaitsee muutokset kirjastokansioissasi automaattisesti. Ota tämä ominaisuus käyttöön, jos \"Tarkkaile kirjaston muutoksia automaattisesti\" ei toimi tiedostojärjestelmässäsi (kuten NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Suorita joka {0} klo {1}", "MessageSearchResultsFor": "Hakutulokset haulle", "MessageSelected": "{0} valittuna", + "MessageSeriesSequenceCannotContainSpaces": "Sarjan sekvenssi ei voi sisältää välilyöntejä", "MessageServerCouldNotBeReached": "Palvelimelle ei saatu yhteyttä", "MessageSetChaptersFromTracksDescription": "Aseta luvut käyttämällä kutakin äänitiedostoa lukuna ja luvun otsikkoa äänitiedoston nimenä", "MessageShareExpirationWillBe": "Umpeutuminen on {0}", @@ -951,7 +954,10 @@ "NotificationOnBackupCompletedDescription": "Laukaistu, kun varmuuskopiointi on valmis", "NotificationOnBackupFailedDescription": "Laukaistu, kun varmuuskopiointi epäonnistuu", "NotificationOnEpisodeDownloadedDescription": "Laukaistu, kun podcast-jakso ladataan automaattisesti", + "NotificationOnRSSFeedDisabledDescription": "Laukaistaan, kun automaattiset jaksolataukset poistetaan käytöstä liian monen epäonnistuneen yrityksen vuoksi", + "NotificationOnRSSFeedFailedDescription": "Laukaistaan, kun RRS-syötteen pyyntö epäonnistuu automaattisessa jaksolatauksessa", "NotificationOnTestDescription": "Tapahtuma ilmoitusjärjestelmän testaamista varten", + "PlaceholderBulkChapterInput": "Syötä luvun otsikko tai käytä numerointia (esim. 'Episodi 1', 'Luku 10', '1.')", "PlaceholderNewCollection": "Uusi kokoelman nimi", "PlaceholderNewFolderPath": "Uusi kansion polku", "PlaceholderNewPlaylist": "Uusi soittolistan nimi", @@ -1005,15 +1011,23 @@ "ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui", "ToastBookmarkCreateSuccess": "Kirjanmerkki lisätty", "ToastBookmarkRemoveSuccess": "Kirjanmerkki poistettu", + "ToastBulkChapterInvalidCount": "Syötä numero 1 ja 150 välillä", "ToastCachePurgeFailed": "Välimuistin tyhjentäminen epäonnistui", "ToastCachePurgeSuccess": "Välimuisti tyhjennetty onnistuneesti", + "ToastChapterLocked": "Luku on lukittu.", + "ToastChapterStartTimeAdjusted": "Luvun aloitusaikaa on säädetty {0} sekunnilla", + "ToastChaptersAllLocked": "Kaikki luvut ovat lukittuina. Avaa lukuja vaihtaaksesi niiden aikoja.", "ToastChaptersHaveErrors": "Luvuissa on virheitä", + "ToastChaptersInvalidShiftAmountLast": "Virheellinen siirtomäärä. Viimeisen luvun aloitusaika ylittäisi tämän äänikirjan keston.", + "ToastChaptersInvalidShiftAmountStart": "Virheellinen siirtomäärä. Ensimmäisen luvun pituudeksi tulisi nolla tai negatiivinen arvo, ja toinen luku kirjoittaisi sen päälle. Kasvata toisen luvun aloitusaikaa.", "ToastChaptersMustHaveTitles": "Lukuilla on oltava otsikot", "ToastChaptersRemoved": "Luvut poistettu", "ToastChaptersUpdated": "Luvut päivitetty", "ToastCollectionItemsAddFailed": "Kohteen/kohteiden lisääminen kokoelmaan epäonnistui", "ToastCollectionRemoveSuccess": "Kokoelma poistettu", "ToastCollectionUpdateSuccess": "Kokoelma päivitetty", + "ToastConnectionNotAvailable": "Verkkoyhteyttä ei saatavilla. Yritä hetken päästä uudelleen", + "ToastCoverSearchFailed": "Kansikuvan haku epäonnistui", "ToastCoverUpdateFailed": "Kansikuvan päivitys epäonnistui", "ToastDateTimeInvalidOrIncomplete": "Päivämäärä ja aika ovat epäkelvolliset tai puutteelliset", "ToastDeleteFileFailed": "Tiedoston poistaminen epäonnistui", @@ -1029,6 +1043,8 @@ "ToastEpisodeDownloadQueueClearSuccess": "Jakson latausjono tyhjennetty", "ToastEpisodeUpdateSuccess": "{0} jaksoa päivitetty", "ToastErrorCannotShare": "Ei voi jakaa alkuperäisesti tällä laitteella", + "ToastFailedToCreate": "Luonti epäonnistui", + "ToastFailedToDelete": "Poisto epäonnistui", "ToastFailedToLoadData": "Tietojen lataaminen epäonnistui", "ToastFailedToMatch": "Vastaaminen epäonnistui", "ToastFailedToShare": "Jakaminen epäonnistui", @@ -1036,6 +1052,7 @@ "ToastInvalidImageUrl": "Epäkelvollinen kuvan URL-osoite", "ToastInvalidMaxEpisodesToDownload": "Ladattavien jaksojen enimmäismäärä on epäkelvollinen", "ToastInvalidUrl": "Epäkelvollinen URL-osoite", + "ToastInvalidUrls": "Yksi tai useampi URL on virheellinen", "ToastItemCoverUpdateSuccess": "Kohteen kansikuva päivitetty", "ToastItemDeletedFailed": "Kohteen poistaminen epäonnistui", "ToastItemDeletedSuccess": "Poistettu kohde", @@ -1060,6 +1077,7 @@ "ToastMustHaveAtLeastOnePath": "On oltava vähintään yksi polku", "ToastNameEmailRequired": "Nimi ja sähköpostiosoite vaaditaan", "ToastNameRequired": "Nimi vaaditaan", + "ToastNewApiKeyUserError": "Täytyy valita käyttäjä", "ToastNewEpisodesFound": "{0} uutta jaksoa löydetty", "ToastNewUserCreatedFailed": "Tilin \"{0}\" luominen epäonnistui", "ToastNewUserCreatedSuccess": "Uusi tili luotu", @@ -1084,6 +1102,7 @@ "ToastPlaylistUpdateSuccess": "Soittolista päivitetty", "ToastPodcastCreateFailed": "Podcastin luominen epäonnistui", "ToastPodcastCreateSuccess": "Podcastin luominen onnistui", + "ToastPodcastEpisodeUpdated": "Episodi päivitetty", "ToastPodcastGetFeedFailed": "Podcast-syötteen saaminen epäonnistui", "ToastPodcastNoEpisodesInFeed": "RSS-syötteestä ei löytynyt jaksoja", "ToastPodcastNoRssFeed": "Podcastilla ei ole RSS-syötettä", @@ -1134,5 +1153,13 @@ "ToastUserPasswordChangeSuccess": "Salasana vaihdettu onnistuneesti", "ToastUserPasswordMismatch": "Salasanat eivät täsmää", "ToastUserPasswordMustChange": "Uusi salasana ei voi olla sama kuin vanha salasana", - "ToastUserRootRequireName": "Pääkäyttäjän nimi on pakollinen" + "ToastUserRootRequireName": "Pääkäyttäjän nimi on pakollinen", + "TooltipAddChapters": "Lisää luku tai lukuja", + "TooltipAddOneSecond": "Lisää 1 sekunti", + "TooltipAdjustChapterStart": "Napauta säätääksesi aloitusaikaa", + "TooltipLockAllChapters": "Lukitse kaikki luvut", + "TooltipLockChapter": "Lukitse luku (Shift+napauta valitaksesi alueen)", + "TooltipSubtractOneSecond": "Vähennä 1 sekunti", + "TooltipUnlockAllChapters": "Avaa kaikki luvut", + "TooltipUnlockChapter": "Avaa luku (Shift+napauta valitaksesi alueen)" } From e8668d9f224ef4f3bd97ff7f1af82bfb3127a2b0 Mon Sep 17 00:00:00 2001 From: bittin1ddc447d824349b2 Date: Mon, 22 Dec 2025 12:58:36 +0100 Subject: [PATCH 123/285] Translated using Weblate (Swedish) 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/sv/ --- client/strings/sv.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index 7bd9753b8..d2fb254ed 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -821,7 +821,7 @@ "MessageImportantNotice": "Viktig meddelande!", "MessageInsertChapterBelow": "Infoga kapitel nedanför", "MessageInvalidAsin": "Felaktig ASIN-kod", - "MessageItemsSelected": "{0} objekt markerade", + "MessageItemsSelected": "{0} objekt valda", "MessageItemsUpdated": "{0} objekt uppdaterade", "MessageJoinUsOn": "Anslut dig till oss på", "MessageLoading": "Laddar...", From b387d9484a564bdaf347af93ba46e50c6181256d Mon Sep 17 00:00:00 2001 From: Marcin Date: Tue, 23 Dec 2025 05:54:25 +0100 Subject: [PATCH 124/285] Translated using Weblate (Polish) Currently translated at 89.2% (1038 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 32c9766ca..8b70a1341 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -96,7 +96,7 @@ "ButtonScrollRight": "Przewiń w prawo", "ButtonSearch": "Szukaj", "ButtonSelectFolderPath": "Wybierz ścieżkę folderu", - "ButtonSeries": "Serial", + "ButtonSeries": "Serie", "ButtonSetChaptersFromTracks": "Ustawiaj rozdziały na podstawie utworów", "ButtonShare": "Udostępnij", "ButtonShiftTimes": "Przesunięcie czasowe", From e5c0a9d22c5148a4c1d6c2fee20c9669578eb1e3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 23 Dec 2025 16:51:54 -0600 Subject: [PATCH 125/285] Version bump v2.32.1 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 568274c80..1e2d52c11 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.32.0", + "version": "2.32.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.32.0", + "version": "2.32.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index df71f6256..0eaffb106 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.32.0", + "version": "2.32.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 4c9e9b9ea..08707893d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.32.0", + "version": "2.32.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.32.0", + "version": "2.32.1", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 2b0d0b6cc..3ee3fb391 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.32.0", + "version": "2.32.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 122fc34a75a6730f99736c3ae01186871b3d90ef Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 24 Dec 2025 17:07:05 -0600 Subject: [PATCH 126/285] Fix server crash filtering by decade with collapsed series --- server/utils/queries/libraryItemsBookFilters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 494a9564f..7ae1dc866 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -236,7 +236,7 @@ module.exports = { } else if (group === 'publishedDecades') { const startYear = parseInt(value) const endYear = parseInt(value, 10) + 9 - mediaWhere = Sequelize.where(Sequelize.literal('CAST(`book`.`publishedYear` AS INTEGER)'), { + mediaWhere = Sequelize.where(Sequelize.literal('CAST(publishedYear AS INTEGER)'), { [Sequelize.Op.between]: [startYear, endYear] }) } From e6d99d07f0e5c649d873d7ba6df40be2e0b9a727 Mon Sep 17 00:00:00 2001 From: sir-wilhelm Date: Sun, 28 Dec 2025 11:28:36 -0600 Subject: [PATCH 127/285] Display localized/styled text for selected filter. The selected filter was using the id before. --- client/components/controls/LibraryFilterSelect.vue | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index 62a9b8037..4834a1a25 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -338,6 +338,18 @@ export default { const series = this.series.find((se) => se.id == decoded) if (series) filterValue = series.name } + } else if (parts[0] === 'progress') { + const item = this.progress.find((p) => p.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'tracks') { + const item = this.tracks.find((t) => t.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'ebooks') { + const item = this.ebooks.find((e) => e.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'missing') { + const item = this.missing.find((m) => m.id == decoded) + if (item) filterValue = item.name } else { filterValue = decoded } From 3e4225bced841944b24ffde05ce6a8933a17e131 Mon Sep 17 00:00:00 2001 From: KiwiHour Date: Fri, 9 Jan 2026 14:26:56 +0000 Subject: [PATCH 128/285] Fix aria-label for jumpBackward button --- client/components/player/PlayerPlaybackControls.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/player/PlayerPlaybackControls.vue b/client/components/player/PlayerPlaybackControls.vue index 368e3f35b..c7cbba63c 100644 --- a/client/components/player/PlayerPlaybackControls.vue +++ b/client/components/player/PlayerPlaybackControls.vue @@ -8,7 +8,7 @@
- From da0a64daed44697e020ff734c53cee6e8b7fab43 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 24 Jan 2026 16:57:25 -0700 Subject: [PATCH 129/285] Add: 10 second grace period to access token cycle --- server/auth/TokenManager.js | 56 ++++++++++--- .../v2.33.0-add-last-refresh-token.js | 84 +++++++++++++++++++ server/models/Session.js | 12 +++ 3 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 server/migrations/v2.33.0-add-last-refresh-token.js diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index faa6774a3..5d8683553 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -12,9 +12,9 @@ class TokenManager { constructor() { /** @type {number} Refresh token expiry in seconds */ - this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 30 * 24 * 60 * 60 // 30 days + this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 3 // 3 seconds for testing /** @type {number} Access token expiry in seconds */ - this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 1 * 60 * 60 // 1 hour + this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 60 // 60 seconds for testing if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) { Logger.info(`[TokenManager] Refresh token expiry set from ENV variable to ${this.RefreshTokenExpiry} seconds`) @@ -193,6 +193,11 @@ class TokenManager { // Calculate new expiration time const newExpiresAt = new Date(Date.now() + this.RefreshTokenExpiry * 1000) + // Set grace period of old refresh token in case of race condition in token rotation. + // This grace period may need to be longer if fetching the user data takes longer due to large progress objects + session.lastRefreshToken = session.refreshToken + session.lastRefreshTokenExpiresAt = new Date(Date.now() + 10 * 1000) // 10 seconds grace period + // Update the session with the new refresh token and expiration session.refreshToken = newRefreshToken session.expiresAt = newExpiresAt @@ -287,8 +292,10 @@ class TokenManager { } } - const session = await Database.sessionModel.findOne({ - where: { refreshToken: refreshToken } + let session = await Database.sessionModel.findOne({ + where: { + [Op.or]: [{ refreshToken: refreshToken }, { lastRefreshToken: refreshToken }] + } }) if (!session) { @@ -298,12 +305,27 @@ class TokenManager { } } - // Check if session is expired in database - if (session.expiresAt < new Date()) { - Logger.info(`[TokenManager] Session expired in database, cleaning up`) - await session.destroy() - return { - error: 'Refresh token expired' + let isGracePeriod = false + if (session.refreshToken !== refreshToken) { + // Token matched lastRefreshToken + if (session.lastRefreshTokenExpiresAt && session.lastRefreshTokenExpiresAt > new Date()) { + isGracePeriod = true + Logger.debug(`[TokenManager] Grace period hit for user ${session.userId}`) + } else { + Logger.debug(`[TokenManager] Grace period expired for user ${session.userId}`) + return { + error: 'Invalid refresh token' + } + } + } else { + // Token matched current refreshToken + // Check if session is expired in database + if (session.expiresAt < new Date()) { + Logger.info(`[TokenManager] Session expired in database, cleaning up`) + await session.destroy() + return { + error: 'Refresh token expired' + } } } @@ -315,6 +337,20 @@ class TokenManager { } } + if (isGracePeriod) { + // Return the already rotated refresh token store in the database, + // and generate a new access token without changing the refresh token + // again + const accessToken = this.generateTempAccessToken(user) + this.setRefreshTokenCookie(req, res, session.refreshToken) + + return { + accessToken, + refreshToken: session.refreshToken, + user + } + } + const newTokens = await this.rotateTokensForSession(session, user, req, res) return { accessToken: newTokens.accessToken, diff --git a/server/migrations/v2.33.0-add-last-refresh-token.js b/server/migrations/v2.33.0-add-last-refresh-token.js new file mode 100644 index 000000000..5b53c749a --- /dev/null +++ b/server/migrations/v2.33.0-add-last-refresh-token.js @@ -0,0 +1,84 @@ +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface - a Sequelize QueryInterface object. + * @property {import('../Logger')} logger - a Logger object. + * + * @typedef MigrationOptions + * @property {MigrationContext} context - an object containing the migration context. + */ + +const migrationVersion = '2.33.0' +const migrationName = `${migrationVersion}-add-last-refresh-token` +const loggerPrefix = `[${migrationVersion} migration]` + +/** + * This migration script adds lastRefreshToken and lastRefreshTokenExpiresAt columns to the sessions table. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function up({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) + + if (await queryInterface.tableExists('sessions')) { + const tableDescription = await queryInterface.describeTable('sessions') + + if (!tableDescription.lastRefreshToken) { + logger.info(`${loggerPrefix} Adding lastRefreshToken column to sessions table`) + await queryInterface.addColumn('sessions', 'lastRefreshToken', { + type: queryInterface.sequelize.Sequelize.DataTypes.STRING, + allowNull: true + }) + } else { + logger.info(`${loggerPrefix} lastRefreshToken column already exists in sessions table`) + } + + if (!tableDescription.lastRefreshTokenExpiresAt) { + logger.info(`${loggerPrefix} Adding lastRefreshTokenExpiresAt column to sessions table`) + await queryInterface.addColumn('sessions', 'lastRefreshTokenExpiresAt', { + type: queryInterface.sequelize.Sequelize.DataTypes.DATE, + allowNull: true + }) + } else { + logger.info(`${loggerPrefix} lastRefreshTokenExpiresAt column already exists in sessions table`) + } + } else { + logger.info(`${loggerPrefix} sessions table does not exist`) + } + + logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) +} + +/** + * This migration script removes the lastRefreshToken and lastRefreshTokenExpiresAt columns from the sessions table. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function down({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) + + if (await queryInterface.tableExists('sessions')) { + const tableDescription = await queryInterface.describeTable('sessions') + + if (tableDescription.lastRefreshToken) { + logger.info(`${loggerPrefix} Removing lastRefreshToken column from sessions table`) + await queryInterface.removeColumn('sessions', 'lastRefreshToken') + } else { + logger.info(`${loggerPrefix} lastRefreshToken column does not exist in sessions table`) + } + + if (tableDescription.lastRefreshTokenExpiresAt) { + logger.info(`${loggerPrefix} Removing lastRefreshTokenExpiresAt column from sessions table`) + await queryInterface.removeColumn('sessions', 'lastRefreshTokenExpiresAt') + } else { + logger.info(`${loggerPrefix} lastRefreshTokenExpiresAt column does not exist in sessions table`) + } + } else { + logger.info(`${loggerPrefix} sessions table does not exist`) + } + + logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) +} + +module.exports = { up, down } diff --git a/server/models/Session.js b/server/models/Session.js index fe9dd5425..3b85bd46a 100644 --- a/server/models/Session.js +++ b/server/models/Session.js @@ -18,6 +18,10 @@ class Session extends Model { this.userId /** @type {Date} */ this.expiresAt + /** @type {string} */ + this.lastRefreshToken + /** @type {Date} */ + this.lastRefreshTokenExpiresAt // Expanded properties @@ -66,6 +70,14 @@ class Session extends Model { expiresAt: { type: DataTypes.DATE, allowNull: false + }, + lastRefreshToken: { + type: DataTypes.STRING, + allowNull: true + }, + lastRefreshTokenExpiresAt: { + type: DataTypes.DATE, + allowNull: true } }, { From 7aa2f84daad13fc9f4b7646797385e25cf2f9872 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 24 Jan 2026 17:00:07 -0700 Subject: [PATCH 130/285] Revert default token expiry --- server/auth/TokenManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 5d8683553..27c7a518b 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -12,9 +12,9 @@ class TokenManager { constructor() { /** @type {number} Refresh token expiry in seconds */ - this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 3 // 3 seconds for testing + this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 30 * 24 * 60 * 60 // 30 days /** @type {number} Access token expiry in seconds */ - this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 60 // 60 seconds for testing + this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 1 * 60 * 60 // 1 hour if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) { Logger.info(`[TokenManager] Refresh token expiry set from ENV variable to ${this.RefreshTokenExpiry} seconds`) From e1ae4f2d315d153e206edebb8857883af896a5c7 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 24 Jan 2026 18:10:38 -0700 Subject: [PATCH 131/285] Fix: race condition in rotation --- server/auth/TokenManager.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 27c7a518b..bf987bb67 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -188,20 +188,32 @@ class TokenManager { async rotateTokensForSession(session, user, req, res) { // Generate new tokens const newAccessToken = this.generateTempAccessToken(user) - const newRefreshToken = this.generateRefreshToken(user) - - // Calculate new expiration time - const newExpiresAt = new Date(Date.now() + this.RefreshTokenExpiry * 1000) + let newRefreshToken = this.generateRefreshToken(user) // Set grace period of old refresh token in case of race condition in token rotation. // This grace period may need to be longer if fetching the user data takes longer due to large progress objects session.lastRefreshToken = session.refreshToken - session.lastRefreshTokenExpiresAt = new Date(Date.now() + 10 * 1000) // 10 seconds grace period + session.lastRefreshTokenExpiresAt = new Date(Date.now() + 60 * 1000) // 1 minute grace period // Update the session with the new refresh token and expiration session.refreshToken = newRefreshToken - session.expiresAt = newExpiresAt - await session.save() + session.expiresAt = new Date(Date.now() + this.RefreshTokenExpiry * 1000) + + // Only update the session if the refresh token hasn't changed since we originally read it + const [numUpdated] = await Database.sessionModel.update(session, { + where: { + id: session.id, + refreshToken: session.lastRefreshToken + } + }) + + if (numUpdated === 0) { + Logger.debug(`[TokenManager] Race condition in rotateTokensForSession for user ${user.id}, getting new token`) + + const updatedSession = await Database.sessionModel.findOne({ where: { id: session.id } }) + + newRefreshToken = updatedSession.refreshToken + } // Set new refresh token cookie this.setRefreshTokenCookie(req, res, newRefreshToken) From b8a2d113f05b180623f6f944f69ea746c9c03a26 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 24 Jan 2026 18:26:11 -0700 Subject: [PATCH 132/285] Allow rotation without grace period for invalidating all user sessions --- server/auth/TokenManager.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index bf987bb67..c36aada08 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -183,17 +183,23 @@ class TokenManager { * @param {import('../models/User')} user * @param {import('express').Request} req * @param {import('express').Response} res - * @returns {Promise<{ accessToken:string, refreshToken:string }>} + * @param {boolean} noGracePeriod - whether to skip the grace period */ - async rotateTokensForSession(session, user, req, res) { + async rotateTokensForSession(session, user, req, res, noGracePeriod = false) { // Generate new tokens const newAccessToken = this.generateTempAccessToken(user) let newRefreshToken = this.generateRefreshToken(user) - // Set grace period of old refresh token in case of race condition in token rotation. - // This grace period may need to be longer if fetching the user data takes longer due to large progress objects - session.lastRefreshToken = session.refreshToken - session.lastRefreshTokenExpiresAt = new Date(Date.now() + 60 * 1000) // 1 minute grace period + if (noGracePeriod) { + // Set grace period of old refresh token in case of race condition in token rotation. + // This grace period may need to be longer if fetching the user data takes longer due to large progress objects + session.lastRefreshToken = session.refreshToken + session.lastRefreshTokenExpiresAt = new Date(Date.now() + 60 * 1000) // 1 minute grace period + } else { + // Do not set grace period of old refresh token, such as when specifically invalidating sessions for a user + session.lastRefreshToken = null + session.lastRefreshTokenExpiresAt = null + } // Update the session with the new refresh token and expiration session.refreshToken = newRefreshToken @@ -416,7 +422,7 @@ class TokenManager { // So rotate token for current session const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } }) if (currentSession) { - const newTokens = await this.rotateTokensForSession(currentSession, user, req, res) + const newTokens = await this.rotateTokensForSession(currentSession, user, req, res, true) // Invalidate all sessions for the user except the current one await Database.sessionModel.destroy({ From 077b523bd66fce70639aa8f88d92e2c442115fca Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 24 Jan 2026 18:42:50 -0700 Subject: [PATCH 133/285] Fix JS Doc deletion --- server/auth/TokenManager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index c36aada08..63463c4b5 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -184,6 +184,7 @@ class TokenManager { * @param {import('express').Request} req * @param {import('express').Response} res * @param {boolean} noGracePeriod - whether to skip the grace period + * @returns {Promise<{ accessToken:string, refreshToken:string }>} */ async rotateTokensForSession(session, user, req, res, noGracePeriod = false) { // Generate new tokens From cfeb6bd502a0f69483db35c37217b8f738c5d029 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 24 Jan 2026 18:57:40 -0700 Subject: [PATCH 134/285] Fix: grace period enable statement --- server/auth/TokenManager.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 63463c4b5..12a92903c 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -183,15 +183,15 @@ class TokenManager { * @param {import('../models/User')} user * @param {import('express').Request} req * @param {import('express').Response} res - * @param {boolean} noGracePeriod - whether to skip the grace period + * @param {boolean} gracePeriod - whether to use the grace period * @returns {Promise<{ accessToken:string, refreshToken:string }>} */ - async rotateTokensForSession(session, user, req, res, noGracePeriod = false) { + async rotateTokensForSession(session, user, req, res, gracePeriod = true) { // Generate new tokens const newAccessToken = this.generateTempAccessToken(user) let newRefreshToken = this.generateRefreshToken(user) - if (noGracePeriod) { + if (gracePeriod) { // Set grace period of old refresh token in case of race condition in token rotation. // This grace period may need to be longer if fetching the user data takes longer due to large progress objects session.lastRefreshToken = session.refreshToken @@ -423,7 +423,7 @@ class TokenManager { // So rotate token for current session const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } }) if (currentSession) { - const newTokens = await this.rotateTokensForSession(currentSession, user, req, res, true) + const newTokens = await this.rotateTokensForSession(currentSession, user, req, res, false) // Invalidate all sessions for the user except the current one await Database.sessionModel.destroy({ From 5107b0307c7a39c3344c3e380d41271631fcfca5 Mon Sep 17 00:00:00 2001 From: Charlie Date: Wed, 24 Dec 2025 20:22:40 +0100 Subject: [PATCH 135/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index 4841ec9f6..4ab98d805 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -962,7 +962,7 @@ "PlaceholderNewFolderPath": "Nouveau chemin de dossier", "PlaceholderNewPlaylist": "Nouveau nom de liste de lecture", "PlaceholderSearch": "Recherche…", - "PlaceholderSearchEpisode": "Rechercher un épisode..", + "PlaceholderSearchEpisode": "Rechercher un épisode…", "StatsAuthorsAdded": "auteurs ajoutés", "StatsBooksAdded": "livres ajoutés", "StatsBooksAdditional": "Les ajouts comprennent…", From c089336e416857e5d04030a9b423f45fb412ac22 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Thu, 1 Jan 2026 23:00:03 +0100 Subject: [PATCH 136/285] Translated using Weblate (Italian) 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/it/ --- client/strings/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/it.json b/client/strings/it.json index 46ecb0f53..d2178cbf6 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Libri", "LabelButtonText": "Buttone Testo", - "LabelByAuthor": "da {0}", + "LabelByAuthor": "di {0}", "LabelChangePassword": "Cambia Password", "LabelChannels": "Canali", "LabelChapterCount": "{0} Capitoli", From b921a08809b23bd7e591fef7cdef37e5ef1714c0 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Thu, 1 Jan 2026 22:59:35 +0100 Subject: [PATCH 137/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index 4ab98d805..6ff72c7ef 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Livres", "LabelButtonText": "Texte du bouton", - "LabelByAuthor": "par {0}", + "LabelByAuthor": "de {0}", "LabelChangePassword": "Modifier le mot de passe", "LabelChannels": "Canaux", "LabelChapterCount": "{0} Chapitres", From 32276aacd94b207ecb116c6145e10b91e3a218a3 Mon Sep 17 00:00:00 2001 From: xxzp3 Date: Wed, 7 Jan 2026 18:48:31 +0100 Subject: [PATCH 138/285] Translated using Weblate (Danish) Currently translated at 99.8% (1161 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/ --- client/strings/da.json | 45 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/client/strings/da.json b/client/strings/da.json index ab1444970..0f4c24ec9 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -127,6 +127,7 @@ "HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer", "HeaderAuthentication": "Autentificering", "HeaderBackups": "Sikkerhedskopier", + "HeaderBulkChapterModal": "Tilføj flere kapitler", "HeaderChangePassword": "Skift Adgangskode", "HeaderChapters": "Kapitler", "HeaderChooseAFolder": "Vælg en Mappe", @@ -199,6 +200,7 @@ "HeaderSettingsExperimental": "Eksperimentelle Funktioner", "HeaderSettingsGeneral": "Generelt", "HeaderSettingsScanner": "Scanner", + "HeaderSettingsSecurity": "Sikkerhed", "HeaderSettingsWebClient": "Webklient", "HeaderSleepTimer": "Søvntimer", "HeaderStatsLargestItems": "Største Elementer", @@ -293,6 +295,7 @@ "LabelContinueListening": "Fortsæt med at lytte", "LabelContinueReading": "Fortsæt med at læse", "LabelContinueSeries": "Fortsæt Serien", + "LabelCorsAllowed": "Tilladte CORS-oprindelser", "LabelCover": "Omslag", "LabelCoverImageURL": "Omslagsbillede URL", "LabelCoverProvider": "Cover billede udbyder", @@ -306,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "Slet fra filsystem (afmarker kun for at fjerne fra databasen)", "LabelDescription": "Beskrivelse", "LabelDeselectAll": "Fravælg Alle", + "LabelDetectedPattern": "Identificeret mønster:", "LabelDevice": "Enheds", "LabelDeviceInfo": "Enhedsinformation", "LabelDeviceIsAvailableTo": "Enhed er tilgængelig for...", @@ -374,11 +378,12 @@ "LabelFilterByUser": "Filtrér efter bruger", "LabelFindEpisodes": "Find episoder", "LabelFinished": "Færdig", + "LabelFinishedDate": "Færdig {0}", "LabelFolder": "Mappe", "LabelFolders": "Mapper", "LabelFontBold": "Fed", "LabelFontBoldness": "Skrift tykkelse", - "LabelFontFamily": "Fontfamilie", + "LabelFontFamily": "Skrifttypefamilie", "LabelFontItalic": "Kursiv", "LabelFontScale": "Skriftstørrelse", "LabelFontStrikethrough": "Gennemstreget", @@ -418,6 +423,7 @@ "LabelLanguages": "Sprog", "LabelLastBookAdded": "Senest tilføjede bog", "LabelLastBookUpdated": "Senest opdaterede bog", + "LabelLastProgressDate": "Sidste fremgang: {0}", "LabelLastSeen": "Sidst set", "LabelLastTime": "Sidste gang", "LabelLastUpdate": "Seneste opdatering", @@ -430,6 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Nej {0}", "LabelLibraryItem": "Bibliotekselement", "LabelLibraryName": "Biblioteksnavn", + "LabelLibrarySortByProgress": "Fremgang: Sidst opdateret", + "LabelLibrarySortByProgressFinished": "Fremgang: Afsluttet", + "LabelLibrarySortByProgressStarted": "Fremgang: Startet", "LabelLimit": "Grænse", "LabelLineSpacing": "Linjeafstand", "LabelListenAgain": "Lyt igen", @@ -467,6 +476,7 @@ "LabelNewestAuthors": "Nyeste forfattere", "LabelNewestEpisodes": "Nyeste episoder", "LabelNextBackupDate": "Næste sikkerhedskopi dato", + "LabelNextChapters": "Næste kapitler vil være:", "LabelNextScheduledRun": "Næste planlagte kørsel", "LabelNoApiKeys": "Ingen API-nøgler", "LabelNoCustomMetadataProviders": "Ingen brugerdefinerede metadata udbydere", @@ -484,6 +494,7 @@ "LabelNotificationsMaxQueueSize": "Maksimal køstørrelse for meddelelseshændelser", "LabelNotificationsMaxQueueSizeHelp": "Hændelser begrænses til at udløse en gang pr. sekund. Hændelser ignoreres, hvis køen er fyldt. Dette forhindrer meddelelsesspam.", "LabelNumberOfBooks": "Antal bøger", + "LabelNumberOfChapters": "Antal kapitler:", "LabelNumberOfEpisodes": "# afsnit", "LabelOpenIDAdvancedPermsClaimDescription": "Navnet af OpenID claimet som indeholder avancerede brugerhandlinger inden i applikationen som vil gælde for ikke administrative roller (hvis konfigureret). Hvis et claim mangler fra svaret vil adgang til ABS blive nægtet. Hvis en enkelt indstilling/option mangler, vil det bliver behandlet som false. Sørg for at identity provider's claim matcher den forventede struktur:", "LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avanceret gruppe og adgangsindstilling, ved automatisk at assigne 'Bruger' grupper.", @@ -576,8 +587,8 @@ "LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder", "LabelSettingsChromecastSupport": "Chromecast-understøttelse", "LabelSettingsDateFormat": "Datoformat", - "LabelSettingsEnableWatcher": "Scan automatisk bibliotek for ændringer", - "LabelSettingsEnableWatcherForLibrary": "Scan automatisk bibliotek for ændringer", + "LabelSettingsEnableWatcher": "Automatisk biblioteksovervåger", + "LabelSettingsEnableWatcherForLibrary": "Automatisk biblioteksovervåger", "LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart", "LabelSettingsEpubsAllowScriptedContent": "Tillad scriptet indhold i epub", "LabelSettingsEpubsAllowScriptedContentHelp": "Tillad epub filer at køre scripts. Det anbefales at holde denne indstilling deaktiveret med mindre du stoler på kilderne af epub filerne.", @@ -626,6 +637,7 @@ "LabelStartTime": "Starttid", "LabelStarted": "Startet", "LabelStartedAt": "Startet klokken", + "LabelStartedDate": "Startet {0}", "LabelStatsAudioTracks": "Lydspor", "LabelStatsAuthors": "Forfattere", "LabelStatsBestDay": "Bedste dag", @@ -655,6 +667,7 @@ "LabelTheme": "Tema", "LabelThemeDark": "Mørk", "LabelThemeLight": "Lys", + "LabelThemeSepia": "Sepia", "LabelTimeBase": "Tidsbase", "LabelTimeDurationXHours": "{0} timer", "LabelTimeDurationXMinutes": "{0} minutter", @@ -739,6 +752,7 @@ "MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Intet resultat for query", "MessageBookshelfNoSeries": "Du har ingen serier", + "MessageBulkChapterPattern": "Hvor mange kapitler vil du tilføje med dette nummereringsmønster?", "MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog", "MessageChapterErrorFirstNotZero": "Første kapitel skal starte ved 0", "MessageChapterErrorStartGteDuration": "Ugyldig starttid skal være mindre end lydbogens varighed", @@ -775,6 +789,7 @@ "MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?", "MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?", "MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Obs: Dette sletter ikke lydfilen medmindre \"Permanent sletning af fil\" er aktiveret", "MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?", "MessageConfirmRemoveListeningSessions": "Er du sikker på at du vil fjerne {0} lytte sessioner?", "MessageConfirmRemoveMetadataFiles": "Er du sikker på at du vil fjerne alle metadata.{0} filer i dine biblioteksfoldere?", @@ -800,6 +815,8 @@ "MessageFeedURLWillBe": "Feed-URL vil være {0}", "MessageFetching": "Henter...", "MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.", + "MessageHeatmapListeningTimeTooltip": "{0} lytter på {1}", + "MessageHeatmapNoListeningSessions": "Ingen lyttesessioner på {0}", "MessageImportantNotice": "Vigtig besked!", "MessageInsertChapterBelow": "Indsæt kapitel nedenfor", "MessageInvalidAsin": "Ugyldig ASIN", @@ -870,7 +887,7 @@ "MessageResetChaptersConfirm": "Er du sikker på, at du vil nulstille kapitler og annullere ændringerne, du har foretaget?", "MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den", "MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.

Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.

Alle klienter, der bruger din server, opdateres automatisk.", - "MessageScheduleLibraryScanNote": "For de fleste brugere, er det anbefalet at efterlade denne funktion deaktiveret for at holde mappe lurer indstilling aktiveret. Mappe lureren vil automatisk opdage ændringer i biblioteksmapper. Mappe lureren virker ikke for alle filsystemer (så som NFS) så schedulerede biblioteksscans vil blive anvendt.", + "MessageScheduleLibraryScanNote": "For de fleste brugere er det anbefalet, at efterlade denne funktion deaktiveret, og lade biblioteksovervågeren være aktiveret - den vil automatisk opdage ændringer i dine biblioteksmapper. Aktiver denne funktion, hvis biblioteksovervågeren ikke virker med dit filsystem (f. eks. NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Kør hvert {0} af {1}", "MessageSearchResultsFor": "Søgeresultater for", "MessageSelected": "{0} valgt", @@ -939,6 +956,7 @@ "NotificationOnRSSFeedDisabledDescription": "Aktiveret når automatiske episode-downloads er slået fra, på grund af for mange forsøg", "NotificationOnRSSFeedFailedDescription": "Aktiveret når anmodning om RSS-feedet fejler for en automatisk episode-download", "NotificationOnTestDescription": "Event for test af notifikationssystemet", + "PlaceholderBulkChapterInput": "Indtast kapiteltitel eller brug nummerering (f.eks. 'Episode 1', 'Kapitel 10', '1.')", "PlaceholderNewCollection": "Nyt samlingnavn", "PlaceholderNewFolderPath": "Ny mappes sti", "PlaceholderNewPlaylist": "Nyt afspilningslistnavn", @@ -992,9 +1010,15 @@ "ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke", "ToastBookmarkCreateSuccess": "Bogmærke tilføjet", "ToastBookmarkRemoveSuccess": "Bogmærke fjernet", + "ToastBulkChapterInvalidCount": "Indtast et tal mellem 1 og 150", "ToastCachePurgeFailed": "Fejlede at opryde cache", "ToastCachePurgeSuccess": "Cache ryddet op i succesfuldt", + "ToastChapterLocked": "Kapitel er låst.", + "ToastChapterStartTimeAdjusted": "Kapitelstarttid justeret med {0} sekunder", + "ToastChaptersAllLocked": "Alle kapitler er låst. Lås op for nogle kapitler for at ændre deres tider.", "ToastChaptersHaveErrors": "Kapitler har fejl", + "ToastChaptersInvalidShiftAmountLast": "Ugyldig ændring. Det sidste kapitels starttid ville fortsætte længere end varigheden på denne lydbog.", + "ToastChaptersInvalidShiftAmountStart": "Ugyldig ændring. Første kapitel ville have en længde på nul eller negativt og ville blive overskrevet af andet kapitel. Udvid startvarigheden på andet kapitel.", "ToastChaptersMustHaveTitles": "Kapitler skal have titler", "ToastChaptersRemoved": "Kapitler fjernet", "ToastChaptersUpdated": "Kapitler opdateret", @@ -1002,6 +1026,7 @@ "ToastCollectionRemoveSuccess": "Samling fjernet", "ToastCollectionUpdateSuccess": "Samling opdateret", "ToastConnectionNotAvailable": "Forbindelse mislykkedes. Prøv igen senere", + "ToastCoverSearchFailed": "Cover-søgning mislykkedes", "ToastCoverUpdateFailed": "Cover opdatering fejlede", "ToastDateTimeInvalidOrIncomplete": "Dato og tid er ugyldig eller ufærdig", "ToastDeleteFileFailed": "Sletning af fil fejlede", @@ -1051,6 +1076,7 @@ "ToastMustHaveAtLeastOnePath": "Skal have mindst en sti", "ToastNameEmailRequired": "Navn og email påkrævet", "ToastNameRequired": "Navn påkrævet", + "ToastNewApiKeyUserError": "En bruger skal vælges", "ToastNewEpisodesFound": "{0} nye afsnit fundet", "ToastNewUserCreatedFailed": "Fejlede at oprette konto: \"{0}\"", "ToastNewUserCreatedSuccess": "Ny konto oprettet", @@ -1075,6 +1101,7 @@ "ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret", "ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast", "ToastPodcastCreateSuccess": "Podcast oprettet med succes", + "ToastPodcastEpisodeUpdated": "Episode opdateret", "ToastPodcastGetFeedFailed": "Fejlede at hente podcast feed", "ToastPodcastNoEpisodesInFeed": "Ingen nye afsnit fundet i RSS feed", "ToastPodcastNoRssFeed": "Podcast har ingen RSS feed", @@ -1125,5 +1152,13 @@ "ToastUserPasswordChangeSuccess": "Password ændret", "ToastUserPasswordMismatch": "Passwords passer ikke sammen", "ToastUserPasswordMustChange": "Nyt password må ikke være det gamle", - "ToastUserRootRequireName": "Skal indholde et root brugernavn" + "ToastUserRootRequireName": "Skal indholde et root brugernavn", + "TooltipAddChapters": "Tilføj kapitler", + "TooltipAddOneSecond": "Tilføj 1 sekund", + "TooltipAdjustChapterStart": "Klik for at ændre starttiden", + "TooltipLockAllChapters": "Lås alle kapitler", + "TooltipLockChapter": "Lås kapitel (Shift+click for at markere flere)", + "TooltipSubtractOneSecond": "Fratag 1 sekund", + "TooltipUnlockAllChapters": "Lås alle kapitaler op", + "TooltipUnlockChapter": "Lås kapitel op (Shift+click for at markere flere)" } From dcbeecff7a14282f35a109ed156ef2620cf76c52 Mon Sep 17 00:00:00 2001 From: herny ucet Date: Sun, 11 Jan 2026 15:25:45 +0100 Subject: [PATCH 139/285] Translated using Weblate (Slovak) Currently translated at 99.7% (1160 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/ --- client/strings/sk.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/strings/sk.json b/client/strings/sk.json index e0d02898c..e9d1d4c60 100644 --- a/client/strings/sk.json +++ b/client/strings/sk.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Knihy", "LabelButtonText": "Text tlačidla", - "LabelByAuthor": "od {0}", + "LabelByAuthor": "od", "LabelChangePassword": "Zmeniť heslo", "LabelChannels": "Kanály", "LabelChapterCount": "{0} kapitol", @@ -383,7 +383,7 @@ "LabelFolders": "Priečinky", "LabelFontBold": "Tučné", "LabelFontBoldness": "Hrúbka písma", - "LabelFontFamily": "Rodina písiem", + "LabelFontFamily": "Písmo", "LabelFontItalic": "Kurzíva", "LabelFontScale": "Veľkosť písma", "LabelFontStrikethrough": "Preškrtnuté", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeuomorfný dizajn s drevenými poličkami", "LabelSettingsChromecastSupport": "Podpora chromecastu", "LabelSettingsDateFormat": "Formát dátumu", - "LabelSettingsEnableWatcher": "Automatické skenovanie knižníc pre zmeny", - "LabelSettingsEnableWatcherForLibrary": "Automaticky skenovať knižnicu pre zmeny", + "LabelSettingsEnableWatcher": "Automatické sledovanie zmien v knižniciach", + "LabelSettingsEnableWatcherForLibrary": "Automatické sledovanie zmien v knižnici", "LabelSettingsEnableWatcherHelp": "Povoliť automatické pridávanie/aktualizácie položiek pri zmene súborov. *Vyžaduje reštart servera", "LabelSettingsEpubsAllowScriptedContent": "Povoliť v e-knihách skriptovaný obsah", "LabelSettingsEpubsAllowScriptedContentHelp": "Povoliť e-knihám spúšťanie skriptov. Odporúča sa túto voľbu nepovolovať, pokiaľ plne nedôverujete zdrojom súborov e-kníh.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Ste si istý, že chcete resetnúť kapitoly a zahodiť zmeny, ktoré ste vykonali?", "MessageRestoreBackupConfirm": "Ste si istí, že chcete obnoviť zálohu vytvorenú", "MessageRestoreBackupWarning": "Obnovenie zálohy spôsobí kompletný prepis databázy umiestnenej v /config a obrázkov prebalov a autorov v /metadata/items a /metadata/authors.

Zálohy nemenia žiadne súbory v priečinkoch vašej knižnice. Ak ste povolili v nastaveniach servera ukladanie obrázkov prebalov a metadát v priečinkoch knižnice, tieto nie sú zálohované a teda ani prepisované.

Všetky klienti používajúci váš server budú automaticky obnovené.", - "MessageScheduleLibraryScanNote": "Pre väčšinu používateľov sa odporúča ponechať túto funkciu vypnutú a povoliť nastavenia funkcie sledovania obsahu priečinku. Funkcia sledovania priečinku bude automaticky detekovať zmeny v priečinkoch knižnice. Táto funkcia však nefunguje pre všetky súborové systémy (ako napr. NFS), v tom prípade využite funkciu plánovaného skenovania knižnice.", + "MessageScheduleLibraryScanNote": "Pre väčšinu používateľov sa odporúča nechať túto funkciu vypnutú a ponechať zapnuté nastavenie „Automatické sledovanie zmien v knižnici“ – táto funkcia automaticky zistí zmeny vo vašich priečinkoch knižnice. Túto funkciu zapnite, ak „Automatické sledovanie zmien v knižnici“ nefunguje vo vašom súborovom systéme (napr. NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Spustiť každú {0} o {1}", "MessageSearchResultsFor": "Výsledky vyhľadávania pre", "MessageSelected": "{0} vybrané", @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Pridanie položky/-iek do zbierky zlyhalo", "ToastCollectionRemoveSuccess": "Zbierka odstránená", "ToastCollectionUpdateSuccess": "Zbierka aktualizovaná", + "ToastConnectionNotAvailable": "Pripojenie je nedostupné. Skúste to neskôr.", + "ToastCoverSearchFailed": "Vyhľadanie obalu sa nepodarilo", "ToastCoverUpdateFailed": "Aktualizácia prebalu zlyhala", "ToastDateTimeInvalidOrIncomplete": "Dátum a čas sú neplatné alebo neúplné", "ToastDeleteFileFailed": "Odstránenie súboru zlyhalo", From e55fed4a33f42d9c91043fcb4ac1510789a2d8d8 Mon Sep 17 00:00:00 2001 From: Henrik Lynge Date: Sun, 11 Jan 2026 05:32:43 +0100 Subject: [PATCH 140/285] Translated using Weblate (Danish) Currently translated at 99.9% (1162 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/ --- client/strings/da.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/strings/da.json b/client/strings/da.json index 0f4c24ec9..6cf517fa3 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Bøger", "LabelButtonText": "Knap tekst", - "LabelByAuthor": "af {0}", + "LabelByAuthor": "Efter Forfatter", "LabelChangePassword": "Ændre Adgangskode", "LabelChannels": "Kanaler", "LabelChapterCount": "{0} Kapitler", @@ -447,6 +447,7 @@ "LabelLogLevelWarn": "Advarsel", "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", "LabelLowestPriority": "Laveste prioritet", + "LabelMatchConfidence": "Confidens", "LabelMatchExistingUsersBy": "Match eksisterende brugere ved", "LabelMatchExistingUsersByDescription": "Anvendt for at forbinde brugere. Når forbundet, brugere vil blive matchet ved unikt id fra din SSO udbyder", "LabelMaxEpisodesToDownload": "Max # afsnit for at downloade. Anvend 0 for ubegrænset.", From 4f8fbbc9793124c9d9e9a23a42ab00683934be5d Mon Sep 17 00:00:00 2001 From: N Visi Date: Thu, 15 Jan 2026 12:43:53 +0100 Subject: [PATCH 141/285] Translated using Weblate (Japanese) Currently translated at 22.8% (266 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/ --- client/strings/ja.json | 87 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/client/strings/ja.json b/client/strings/ja.json index 7c2acb889..28f6de6a6 100644 --- a/client/strings/ja.json +++ b/client/strings/ja.json @@ -72,6 +72,7 @@ "ButtonQueueRemoveItem": "次に再生から削除", "ButtonQuickEmbed": "クイック埋め込み", "ButtonQuickEmbedMetadata": "メタデータの埋め込み", + "ButtonQuickMatch": "クイックマッチ", "ButtonReScan": "再スキャン", "ButtonRead": "読む", "ButtonReadLess": "閉じる", @@ -80,71 +81,157 @@ "ButtonRemove": "削除", "ButtonRemoveAll": "全て削除", "ButtonRemoveAllLibraryItems": "ライブラリーの項目を全て削除", + "ButtonRemoveFromContinueListening": "「続きを聴く」から削除", + "ButtonRemoveFromContinueReading": "「続きを読む」から削除", + "ButtonRemoveSeriesFromContinueSeries": "「シリーズを続く」からシリーズを削除", "ButtonReset": "元に戻す", "ButtonResetToDefault": "デフォルトに戻す", "ButtonRestore": "復元", "ButtonSave": "保存", "ButtonSaveAndClose": "保存して閉じる", + "ButtonSaveTracklist": "トラックリストを保存", "ButtonScan": "スキャン", "ButtonScanLibrary": "ライブラリーをスキャン", "ButtonScrollLeft": "左にスクロール", "ButtonScrollRight": "右にスクロール", "ButtonSearch": "検索", + "ButtonSelectFolderPath": "保存先フォルダを選択", "ButtonSeries": "シリーズ", + "ButtonSetChaptersFromTracks": "トラックからチャプターを設定する", + "ButtonShare": "共有", + "ButtonShiftTimes": "再生時間の移動", + "ButtonShow": "表示", + "ButtonStartM4BEncode": "M4Bエンコード開始", + "ButtonStartMetadataEmbed": "メタデータ埋め込み開始", + "ButtonStats": "統計", "ButtonSubmit": "送信", + "ButtonTest": "テスト", + "ButtonUnlinkOpenId": "OpenID 連携解除", + "ButtonUpload": "アップロード", + "ButtonUploadBackup": "バックアップのアップロード", + "ButtonUploadCover": "カバー画像をアップロード", + "ButtonUploadOPMLFile": "OPMLファイルをアップロード", + "ButtonUserDelete": "ユーザーを削除 {0}", + "ButtonUserEdit": "ユーザを編集 {0}", + "ButtonViewAll": "すべて表示", "ButtonYes": "はい", + "ErrorUploadFetchMetadataAPI": "メタデータの取得中にエラーが発生しました", + "ErrorUploadFetchMetadataNoResults": "メタデータ取得に失敗しました。タイトルや著者名を更新してください", + "ErrorUploadLacksTitle": "タイトルは必須です", "HeaderAccount": "アカウント", + "HeaderAddCustomMetadataProvider": "カスタムメタデータプロバイダーを追加", "HeaderAdvanced": "上級者向け", + "HeaderApiKeys": "APIキー", + "HeaderAppriseNotificationSettings": "Apprise 通知設定", "HeaderAudioTracks": "オーディオトラック", + "HeaderAudiobookTools": "オーディオブックのファイル管理ツール", + "HeaderAuthentication": "認証", + "HeaderBackups": "バックアップ", + "HeaderBulkChapterModal": "チャプターをまとめて追加", + "HeaderChangePassword": "パスワードを変更", "HeaderChapters": "チャプター", + "HeaderChooseAFolder": "フォルダを選択", "HeaderCollection": "コレクション", "HeaderCollectionItems": "コレクションの項目", + "HeaderCover": "カバー", + "HeaderCurrentDownloads": "現在のダウンロード", + "HeaderCustomMessageOnLogin": "ログイン時のカスタムメッセージ", + "HeaderCustomMetadataProviders": "カスタムメタデータプロバイダー", "HeaderDetails": "詳細", + "HeaderDownloadQueue": "ダウンロード待ち", "HeaderEbookFiles": "電子書籍ファイル", + "HeaderEmail": "メール", + "HeaderEmailSettings": "メール設定", "HeaderEpisodes": "エピソード", + "HeaderEreaderDevices": "電子書籍リーダー端末", "HeaderEreaderSettings": "電子書籍リーダーの設定", + "HeaderFiles": "ファイル", + "HeaderFindChapters": "チャプターを検索", + "HeaderIgnoredFiles": "無視されたファイル", + "HeaderItemFiles": "アイテムファイル", + "HeaderItemMetadataUtils": "アイテムメタデータユーティリティ", + "HeaderLastListeningSession": "直近の再生セッション", "HeaderLatestEpisodes": "最新のエピソード", "HeaderLibraries": "ライブラリー", + "HeaderLibraryFiles": "ライブラリファイル", + "HeaderLibraryStats": "ライブラリ統計", + "HeaderListeningSessions": "再生セッション", + "HeaderListeningStats": "再生統計", + "HeaderLogin": "ログイン", + "HeaderLogs": "ログ", + "HeaderManageGenres": "ジャンルを管理", + "HeaderManageTags": "タグを管理", + "HeaderMapDetails": "マップの詳細", + "HeaderMatch": "マッチ", + "HeaderMetadataOrderOfPrecedence": "メタデータの優先順", + "HeaderMetadataToEmbed": "埋め込むメタデータ", + "HeaderNewAccount": "新規アカウント", + "HeaderNewApiKey": "新規APIキー", + "HeaderNewLibrary": "新規ライブラリー", + "HeaderNotificationCreate": "通知を作成", + "HeaderNotificationUpdate": "通知を更新", + "HeaderNotifications": "通知", + "HeaderOpenIDConnectAuthentication": "OpenID Connect 認証", "HeaderOpenRSSFeed": "RSS Feedを開く", "HeaderPlayerSettings": "プレーヤーの設定", "HeaderPlaylist": "プレイリスト", "HeaderPlaylistItems": "プレイリストアイテム", "HeaderRSSFeedGeneral": "RSS 詳細", + "HeaderRSSFeedIsOpen": "RSSフィードが開いています", "HeaderSettings": "設定", "HeaderSettingsGeneral": "一般", "HeaderSettingsScanner": "スキャナー", "HeaderSleepTimer": "スリープタイマー", "HeaderStatsMinutesListeningChart": "過去7日間の視聴時間(分)", + "HeaderStatsRecentSessions": "最近の再生履歴", + "HeaderTableOfContents": "目次", + "HeaderYourStats": "再生統計", "LabelAddToPlaylist": "プレイリストの追加", + "LabelAddedAt": "追加日時", + "LabelAddedDate": "追加日時 ­­{0}", + "LabelAll": "すべて", "LabelAuthor": "著者", "LabelAuthorFirstLast": "著者(名 氏)", "LabelAuthorLastFirst": "著者(氏 名)", "LabelAuthors": "著者", "LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード", "LabelBooks": "ほん", + "LabelByAuthor": "著 {0}", "LabelChapters": "チャプター", "LabelClosePlayer": "プレイヤーを閉じる", + "LabelCollapseSeries": "シリーズを折りたたむ", "LabelComplete": "完了", "LabelContinueListening": "続きから聞く", + "LabelContinueReading": "続きを読む", + "LabelContinueSeries": "シリーズを続く", "LabelDescription": "説明", + "LabelDiscover": "おすすめ", "LabelDownload": "ダウンロード", "LabelDuration": "長さ", "LabelEbook": "Eブック", "LabelEbooks": "Eブック", "LabelEnable": "有効", + "LabelEnd": "終了", "LabelEndOfChapter": "チャプターの最後", "LabelEpisode": "エピソード", + "LabelExplicit": "", "LabelFeedURL": "Feed URL", "LabelFile": "ファイル", + "LabelFileBirthtime": "ファイル作成日時", + "LabelFileModified": "ファイル更新日時", "LabelFilename": "ファイル名", "LabelFinished": "完了", "LabelFolder": "フォルダ", "LabelFontBoldness": "フォントの太さ", + "LabelFontFamily": "フォントファミリー", "LabelFontScale": "フォントサイズ", "LabelGenre": "ジャンル", "LabelGenres": "ジャンル", + "LabelHasEbook": "eBookあり", + "LabelHasSupplementaryEbook": "付属eBookあり", "LabelHost": "ホスト", "LabelInProgress": "進行中", + "LabelIncomplete": "未完了", "LabelLanguage": "言語", "LabelLanguages": "言語", "LabelLayout": "レイアウト", From 7201cced4287e07dc595d01fe4176f397f7cb755 Mon Sep 17 00:00:00 2001 From: Kabika82 Date: Mon, 19 Jan 2026 13:42:10 +0100 Subject: [PATCH 142/285] 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/ --- client/strings/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/hu.json b/client/strings/hu.json index 574fd1c9b..1c177a09c 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -499,7 +499,7 @@ "LabelNumberOfEpisodes": "Epizódok száma", "LabelOpenIDAdvancedPermsClaimDescription": "Az OpenID-igény neve, amely a felhasználói műveletekre vonatkozó haladó jogosultságokat tartalmazza az alkalmazáson belül, és amely a nem adminisztrátori szerepkörökre vonatkozik (ha konfigurálva van). Ha az igény hiányzik a válaszból, az ABS-hez való hozzáférés megtagadásra kerül. Ha egyetlen opció hiányzik, azt false-ként fogja kezelni. Győződj meg arról, hogy az identitásszolgáltató igénye megfelel a várt struktúrának:", "LabelOpenIDClaims": "Hagyd üresen a következő opciókat, hogy letiltsd a haladó csoport- és jogosultság-hozzárendelést, ekkor automatikusan a ‘Felhasználó’ csoport kerül hozzárendelésre.", - "LabelOpenIDGroupClaimDescription": "Az OpenID-igény neve, amely a felhasználó csoportjainak listáját tartalmazza. Általában groups néven hivatkoznak rá. Ha konfigurálva van, az alkalmazás automatikusan hozzárendeli a szerepköröket a felhasználó csoporttagságai alapján, feltéve, hogy ezek a csoportok az igényben kis- és nagybetűkre érzéketlenül ‘admin’, ‘user’ vagy ‘guest’ néven szerepelnek. Az igénynek egy listát kell tartalmaznia, és ha egy felhasználó több csoport tagja, az alkalmazás a legmagasabb szintű hozzáféréssel rendelkező szerepkört rendeli hozzá. Ha egyetlen csoport sem felel meg, a hozzáférés megtagadásra kerül.", + "LabelOpenIDGroupClaimDescription": "Az OpenID-igény neve, amely a felhasználó csoportjainak listáját tartalmazza. Általában groups néven hivatkoznak rá. Ha konfigurálva van, az alkalmazás automatikusan hozzárendeli a szerepköröket a felhasználó csoporttagságai alapján, feltéve, hogy ezek a csoportok az igényben kis- és nagybetűkre érzéketlenül ‘admin’, ‘user’ vagy ‘guest’ néven szerepelnek. Az igénynek egy listát kell tartalmaznia, és ha egy felhasználó több csoport tagja, az alkalmazás a legmagasabb szintű hozzáféréssel rendelkező szerepkört rendeli hozzá. Ha egyetlen csoport sem felel meg, a hozzáférés megtagadásra kerül.", "LabelOpenRSSFeed": "RSS hírcsatorna megnyitása", "LabelOverwrite": "Felülírás", "LabelPaginationPageXOfY": "{0} oldal {1}-ból/ből", From d08cef11edd448fe60212b975599a0fe6184c9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej?= <20731216+Jarsey45@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:30:11 +0100 Subject: [PATCH 143/285] Translated using Weblate (Polish) Currently translated at 94.5% (1100 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 68 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 8b70a1341..c50ebe85e 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -915,10 +915,16 @@ "MessageTaskNoFilesToScan": "Brak plików do skanowania", "MessageTaskOpmlImport": "Importuj OPML", "MessageTaskOpmlImportDescription": "Tworzenie {0} podcastów z kanałów RSS", + "MessageTaskOpmlImportFeedPodcastDescription": "Tworzenie podcastu \"{0}\"", + "MessageTaskOpmlImportFeedPodcastExists": "Podcast już istnieje pod podaną ścieżką", + "MessageTaskOpmlImportFeedPodcastFailed": "Nie udało się utworzyć podcastu", + "MessageTaskOpmlImportFinished": "Dodano {0} podcastów", + "MessageTaskOpmlParseFailed": "Błąd parsowania pliku OPML", "MessageTaskScanItemsAdded": "Dodano {0}", "MessageTaskScanItemsMissing": "Brakuje {0}", "MessageTaskScanItemsUpdated": "Zaktualizowano {0}", "MessageTaskScanNoChangesNeeded": "Brak zmian", + "MessageTaskScanningLibrary": "Skanowanie biblioteki \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "Brak prawa zapisu do folderu docelowego", "MessageThinking": "Myślę...", "MessageUploaderItemFailed": "Nie udało się przesłać", @@ -937,6 +943,7 @@ "NoteUploaderFoldersWithMediaFiles": "Foldery z plikami multimedialnymi będą traktowane jako osobne elementy w bibliotece.", "NoteUploaderOnlyAudioFiles": "Jeśli przesyłasz tylko pliki audio, każdy plik audio będzie traktowany jako osobny audiobook.", "NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.", + "NotificationOnTestDescription": "Zdarzenie używane do testowania systemu powiadomień", "PlaceholderNewCollection": "Nowa nazwa kolekcji", "PlaceholderNewFolderPath": "Nowa ścieżka folderu", "PlaceholderNewPlaylist": "Nowa nazwa playlisty", @@ -957,26 +964,50 @@ "StatsTopMonth": "TOPOWY MIESIĄC", "StatsTopNarrator": "TOPOWY NARRATOR", "StatsTopNarrators": "TOPOWI NARRATORZY", + "StatsTotalDuration": "O sumarycznej długości…", "StatsYearInReview": "PRZEGLĄD ROKU", "ToastAccountUpdateSuccess": "Zaktualizowano konto", + "ToastAsinRequired": "ASIN jest wymagany", "ToastAuthorImageRemoveSuccess": "Zdjęcie autora usunięte", + "ToastAuthorNotFound": "Autor \"{0}\" nie został znaleziony", + "ToastAuthorRemoveSuccess": "Autor usunięty", + "ToastAuthorSearchNotFound": "Autor nie odnaleziony", "ToastAuthorUpdateMerged": "Autor scalony", "ToastAuthorUpdateSuccess": "Autor zaktualizowany", "ToastAuthorUpdateSuccessNoImageFound": "Autor zaktualizowany (nie znaleziono obrazu)", + "ToastBackupAppliedSuccess": "Kopia zapasowa została przywrócona", "ToastBackupCreateFailed": "Nie udało się utworzyć kopii zapasowej", "ToastBackupCreateSuccess": "Utworzono kopię zapasową", "ToastBackupDeleteFailed": "Nie udało się usunąć kopii zapasowej", "ToastBackupDeleteSuccess": "Udało się usunąć kopie zapasowej", + "ToastBackupInvalidMaxKeep": "Nieprawidłowa ilość kopii zapasowych do przechowania", + "ToastBackupInvalidMaxSize": "Nieprawidłowy rozmiar maksymalny kopii zapasowej", "ToastBackupRestoreFailed": "Nie udało się przywrócić kopii zapasowej", "ToastBackupUploadFailed": "Nie udało się przesłać kopii zapasowej", "ToastBackupUploadSuccess": "Kopia zapasowa została przesłana", - "ToastBatchUpdateFailed": "Aktualizacja wsadowa nie powiodła się", - "ToastBatchUpdateSuccess": "Aktualizacja wsadowa powiodła się", + "ToastBatchDeleteFailed": "Usuwanie zbiorcze nie powiodło się", + "ToastBatchDeleteSuccess": "Usuwanie zbiorcze powiodło się", + "ToastBatchUpdateFailed": "Aktualizacja zbiorcza nie powiodła się", + "ToastBatchUpdateSuccess": "Aktualizacja zbiorcza powiodła się", "ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki", "ToastBookmarkCreateSuccess": "Dodano zakładkę", "ToastBookmarkRemoveSuccess": "Zakładka została usunięta", + "ToastBulkChapterInvalidCount": "Wprowadź liczbę z przedziału od 1 do 150", + "ToastCachePurgeFailed": "Nie udało się wyczyścić pamięci cache", + "ToastCachePurgeSuccess": "Wyczyszczono pamięć cache", + "ToastChapterLocked": "Rozdział jest zablokowany.", + "ToastChapterStartTimeAdjusted": "Czas rozpoczęcia rozdziału przesunięty o \"{0}\" sekund", + "ToastChaptersAllLocked": "Wszystkie rozdziały są zablokowane. Odblokuj edycję, aby użyć przesunięcia czasowego.", + "ToastChaptersHaveErrors": "Rozdziały posiadają błędy", + "ToastChaptersInvalidShiftAmountLast": "Niepoprawna wartość przesunięcia. Czas rozpoczęcia ostatniego rozdziału wykroczyłby poza długość tego audiobooka.", + "ToastChaptersInvalidShiftAmountStart": "Niepoprawna wartość przesunięcia. Pierwszy rozdział miałby długość mniejszą lub równą zeru oraz on zostałby nadpisany przez rozdział drugi. Ustaw późniejszy czas rozpoczęcia drugiego rozdziału.", + "ToastChaptersMustHaveTitles": "Rozdziały muszą posiadać tytuł", + "ToastChaptersRemoved": "Rozdziały usunięte", + "ToastChaptersUpdated": "Rozdziały zaktualizowane", + "ToastCollectionItemsAddFailed": "Dodanie elementów do kolekcji nie powiodło się", "ToastCollectionRemoveSuccess": "Kolekcja usunięta", "ToastCollectionUpdateSuccess": "Zaktualizowano kolekcję", + "ToastConnectionNotAvailable": "Brak połączenia. Spróbuj ponownie później", "ToastCoverSearchFailed": "Nieudane wyszukiwanie okładki", "ToastCoverUpdateFailed": "Nieudana aktualizacja okładki", "ToastDateTimeInvalidOrIncomplete": "Niepoprawna data i czas", @@ -993,6 +1024,8 @@ "ToastInvalidUrl": "Nieprawidłowy URL", "ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe", "ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę", + "ToastItemDeletedFailed": "Nie udało się usunąć elementu", + "ToastItemDeletedSuccess": "Element usunięty", "ToastItemDetailsUpdateSuccess": "Zaktualizowano szczegóły", "ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako ukończone", "ToastItemMarkedAsFinishedSuccess": "Pozycja oznaczona jako ukończona", @@ -1031,12 +1064,41 @@ "ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się", "ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji", "ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji", + "ToastRemoveItemsWithIssuesFailed": "Nie udało się usunąć wadliwych elementów z biblioteki", + "ToastRemoveItemsWithIssuesSuccess": "Usunięto wadliwe elementy z biblioteki", + "ToastRenameFailed": "Nie udało się zmienić nazwy", + "ToastRescanFailed": "Ponowne skanowanie nie powiodło się dla {0}", + "ToastRescanRemoved": "Ponowne skanowanie powiodło się – element został usunięty", + "ToastRescanUpToDate": "Ponowne skanowanie powiodło się – element był aktualny", + "ToastRescanUpdated": "Ponowne skanowanie powiodło się – element został zaktualizowany", + "ToastScanFailed": "Nie powiódł się skan elementu biblioteki", + "ToastSelectAtLeastOneUser": "Zaznacz co najmniej jednego użytkownika", "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device", + "ToastSendEbookToDeviceSuccess": "Ebook wysłany na urządzenie \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Nie można dodać dwóch serii pod tą samą nazwą", + "ToastSeriesUpdateFailed": "Aktualizacja serii nie powiodła się", + "ToastSeriesUpdateSuccess": "Aktualizacja serii powiodła się", + "ToastServerSettingsUpdateSuccess": "Zaktualizowano ustawienia serwera", + "ToastSessionCloseFailed": "Nie udało się zamknąć sesji", "ToastSessionDeleteFailed": "Nie udało się usunąć sesji", "ToastSessionDeleteSuccess": "Sesja usunięta", + "ToastSleepTimerDone": "Słodkich snów... zZzzZz", "ToastSocketConnected": "Nawiązano połączenie z serwerem", "ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte", "ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się", + "ToastTitleRequired": "Tytuł jest wymagany", + "ToastUnknownError": "Nieznany błąd", + "ToastUnlinkOpenIdFailed": "Nie udało się odpiąć użytkownika z OpenID", + "ToastUnlinkOpenIdSuccess": "Użytkownik odpięty z OpenID", + "ToastUploaderFilepathExistsError": "Ścieżka \"{0}\" już istnieje na serwerze", "ToastUserDeleteFailed": "Nie udało się usunąć użytkownika", - "ToastUserDeleteSuccess": "Użytkownik usunięty" + "ToastUserDeleteSuccess": "Użytkownik usunięty", + "TooltipAddChapters": "Dodaj rozdział(y)", + "TooltipAddOneSecond": "Dodaj sekundę", + "TooltipAdjustChapterStart": "Kliknij, aby skorygować czas początkowy", + "TooltipLockAllChapters": "Zablokuj wszystkie rozdziały", + "TooltipLockChapter": "Zablokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)", + "TooltipSubtractOneSecond": "Odejmij sekundę", + "TooltipUnlockAllChapters": "Odblokuj wszystkie rozdziały", + "TooltipUnlockChapter": "Odblokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)" } From 87f376629927b381620bf8eeb484c8592fea1ce2 Mon Sep 17 00:00:00 2001 From: Mantas Date: Fri, 30 Jan 2026 00:08:32 +0100 Subject: [PATCH 144/285] Translated using Weblate (Lithuanian) Currently translated at 59.9% (697 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/lt/ --- client/strings/lt.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/lt.json b/client/strings/lt.json index b5ca66630..2480cdf35 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -1,5 +1,6 @@ { "ButtonAdd": "Pridėti", + "ButtonAddApiKey": "Pridėti API raktą", "ButtonAddChapters": "Pridėti skyrius", "ButtonAddDevice": "Pridėti įrenginį", "ButtonAddLibrary": "Pridėti Biblioteką", From 1f3fa80ddd78a929958cf8714288b92b88d38e30 Mon Sep 17 00:00:00 2001 From: Samuel Guerrero Date: Fri, 30 Jan 2026 22:56:07 +0100 Subject: [PATCH 145/285] Translated using Weblate (Spanish) Currently translated at 97.5% (1134 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/es.json b/client/strings/es.json index 4a611b3de..73ec1cd2d 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Libros", "LabelButtonText": "Texto del botón", - "LabelByAuthor": "por {0}", + "LabelByAuthor": "por", "LabelChangePassword": "Cambiar contraseña", "LabelChannels": "Canales", "LabelChapterCount": "{0} capítulos", From 6c7221d37d781a44d67ba8044e11993e82dfce01 Mon Sep 17 00:00:00 2001 From: dv4yGY2U Date: Fri, 30 Jan 2026 23:31:49 +0100 Subject: [PATCH 146/285] 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/ --- client/strings/tr.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/strings/tr.json b/client/strings/tr.json index af64a618d..770521cb8 100644 --- a/client/strings/tr.json +++ b/client/strings/tr.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Kitaplar", "LabelButtonText": "Buton Metni", - "LabelByAuthor": "Yazar: {0}", + "LabelByAuthor": "{0} tarafından", "LabelChangePassword": "Şifreyi Değiştir", "LabelChannels": "Kanallar", "LabelChapterCount": "{0} Bölüm", @@ -383,7 +383,7 @@ "LabelFolders": "Klasörler", "LabelFontBold": "Kalın", "LabelFontBoldness": "Yazı Tipi Kalınlığı", - "LabelFontFamily": "Yazı Tipi Ailesi", + "LabelFontFamily": "Yazı tipi ailesi", "LabelFontItalic": "İtalik", "LabelFontScale": "Yazı Tipi Ölçeği", "LabelFontStrikethrough": "Üstü Çizili", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Ahşap raflı skeuomorfik tasarım", "LabelSettingsChromecastSupport": "Chromecast desteği", "LabelSettingsDateFormat": "Tarih Formatı", - "LabelSettingsEnableWatcher": "Değişiklikler için kütüphaneleri otomatik olarak tara", - "LabelSettingsEnableWatcherForLibrary": "Değişiklikler için kütüphaneyi otomatik olarak tara", + "LabelSettingsEnableWatcher": "Kütüphanelerdeki değişiklikleri otomatik olarak izle", + "LabelSettingsEnableWatcherForLibrary": "Kütüphanedeki değişiklikleri otomatik olarak izle", "LabelSettingsEnableWatcherHelp": "Dosya değişiklikleri algılandığında öğelerin otomatik olarak eklenmesini/güncellenmesini sağlar. *Sunucunun yeniden başlatılmasını gerektirir", "LabelSettingsEpubsAllowScriptedContent": "Epub'larda betiklenmiş içeriğe izin ver", "LabelSettingsEpubsAllowScriptedContentHelp": "Epub dosyalarının betik çalıştırmasına izin verin. Epub dosyalarının kaynağına güvenmiyorsanız bu ayarı devre dışı bırakmanız önerilir.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Bölümleri sıfırlamak ve yaptığınız değişiklikleri geri almak istediğinizden emin misiniz?", "MessageRestoreBackupConfirm": "Şu tarihte oluşturulan yedeği geri yüklemek istediğinizden emin misiniz", "MessageRestoreBackupWarning": "Bir yedeği geri yüklemek, /config konumundaki tüm veritabanının ve /metadata/items & /metadata/authors içindeki kapak resimlerinin üzerine yazacaktır.

Yedekler, kütüphane klasörlerinizdeki hiçbir dosyayı değiştirmez. Sunucu ayarlarını kütüphane klasörlerinizde kapak resmi ve üst veri saklamak için etkinleştirdiyseniz, bunlar yedeklenmez veya üzerine yazılmaz.

Sunucunuzu kullanan tüm istemciler otomatik olarak yenilenecektir.", - "MessageScheduleLibraryScanNote": "Çoğu kullanıcı için, bu özelliği devre dışı bırakıp klasör izleyici ayarını etkin tutmaları önerilir. Klasör izleyici, kütüphane klasörlerinizdeki değişiklikleri otomatik olarak algılayacaktır. Klasör izleyici her dosya sistemi için (NFS gibi) çalışmaz, bu nedenle bunun yerine zamanlanmış kütüphane taramaları kullanılabilir.", + "MessageScheduleLibraryScanNote": "Çoğu kullanıcı için bu ayarı pasif bırakması ve \"Kütüphanedeki değişiklikleri otomatik olarak izle\" seçeneğini aktif etmesi önerilir. O seçenek kütüphane dizinlerindeki herhangi bir değişikliği otomatik olarak tespit edecektir. Eğer dosya sisteminiz \"Kütüphanedeki değişiklikleri otomatik olarak izle\" yöntemini desteklemiyorsa (örn; NFS dosya sistemi) bu özelliği aktif edebilirsiniz.", "MessageScheduleRunEveryWeekdayAtTime": "Her {0} günü saat {1}'de çalıştır", "MessageSearchResultsFor": "Arama sonuçları", "MessageSelected": "{0} seçildi", From e7cb0466e6678f81ccddd6396623a19251607292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ku=C5=BAnicki?= Date: Mon, 2 Feb 2026 07:04:06 +0100 Subject: [PATCH 147/285] Translated using Weblate (Polish) Currently translated at 94.5% (1100 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index c50ebe85e..3a9593db0 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -437,8 +437,8 @@ "LabelLibraryItem": "Element biblioteki", "LabelLibraryName": "Nazwa biblioteki", "LabelLibrarySortByProgress": "Postęp: Ostatnio zaktualizowane", - "LabelLibrarySortByProgressFinished": "Postęp: Ukończone", - "LabelLibrarySortByProgressStarted": "Postęp: Rozpoczęte", + "LabelLibrarySortByProgressFinished": "Postęp: Ukończony", + "LabelLibrarySortByProgressStarted": "Postęp: Rozpoczęty", "LabelLimit": "Limit", "LabelLineSpacing": "Odstęp między wierszami", "LabelListenAgain": "Słuchaj ponownie", From 6e5feee78ad43a709f93b45d43acdcaf930abd76 Mon Sep 17 00:00:00 2001 From: FiendFEARing Date: Sun, 1 Feb 2026 08:13:41 +0100 Subject: [PATCH 148/285] 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/ --- client/strings/zh-cn.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 64c41619e..14c70cb10 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "没有 {0}", "LabelLibraryItem": "媒体库项目", "LabelLibraryName": "媒体库名称", - "LabelLibrarySortByProgress": "收听进度: 上次收听时间", - "LabelLibrarySortByProgressFinished": "收听进度: 已完成的", - "LabelLibrarySortByProgressStarted": "收听进度: 已开始的", + "LabelLibrarySortByProgress": "进度: 上次更新", + "LabelLibrarySortByProgressFinished": "进度: 已完成", + "LabelLibrarySortByProgressStarted": "进度: 已开始", "LabelLimit": "限制", "LabelLineSpacing": "行间距", "LabelListenAgain": "再次收听", From b8942c59319911a5edffdba1d46464b9e80e636a Mon Sep 17 00:00:00 2001 From: enosh Date: Sun, 1 Feb 2026 15:02:25 +0100 Subject: [PATCH 149/285] Translated using Weblate (Hebrew) Currently translated at 81.4% (947 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/ --- client/strings/he.json | 102 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/client/strings/he.json b/client/strings/he.json index 0efc1ec9c..786683a5d 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -81,7 +81,7 @@ "ButtonRemove": "הסר", "ButtonRemoveAll": "הסר הכל", "ButtonRemoveAllLibraryItems": "הסר את כל פריטי הספרייה", - "ButtonRemoveFromContinueListening": "הסר מ- המשך האזנה", + "ButtonRemoveFromContinueListening": "הסר מ״המשך האזנה״", "ButtonRemoveFromContinueReading": "הסר מ- המשך קריאה", "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מ- המשך סדרה", "ButtonReset": "איפוס", @@ -121,6 +121,7 @@ "HeaderAccount": "חשבון", "HeaderAddCustomMetadataProvider": "הוסף ספק מטא-נתונים מותאם אישית", "HeaderAdvanced": "מתקדם", + "HeaderApiKeys": "מפתחות API", "HeaderAppriseNotificationSettings": "הגדרות התראות של Apprise", "HeaderAudioTracks": "רצועות קול", "HeaderAudiobookTools": "כלים לניהול קבצי ספרים קוליים", @@ -165,6 +166,7 @@ "HeaderMetadataOrderOfPrecedence": "סדר העדפת מטא-נתונים", "HeaderMetadataToEmbed": "מטא-נתונים להטמעה", "HeaderNewAccount": "חשבון חדש", + "HeaderNewApiKey": "מפתח API חדש", "HeaderNewLibrary": "ספרייה חדשה", "HeaderNotificationCreate": "צור התראה", "HeaderNotificationUpdate": "עדכון התראה", @@ -210,6 +212,7 @@ "HeaderTableOfContents": "תוכן עניינים", "HeaderTools": "כלים", "HeaderUpdateAccount": "עדכן חשבון", + "HeaderUpdateApiKey": "עדכן מפתח API", "HeaderUpdateAuthor": "עדכן יוצר", "HeaderUpdateDetails": "עדכן פרטים", "HeaderUpdateLibrary": "עדכן ספרייה", @@ -239,7 +242,10 @@ "LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים", "LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים", "LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך", + "LabelApiKeyCreated": "מפתח API ״{0}״ נוצר בהצלחה.", + "LabelApiKeyCreatedDescription": "אנא העתק את מפתח ה־API כעת, לא ניתן יהיה להציגו שוב.", "LabelApiKeyUser": "פעל בשם המשתמש", + "LabelApiKeyUserDescription": "למפתח ה־API יהיו הרשאות זהות למשתמש שעל שמו הוא פועל. ביומני הרישום (logs), הפעולות יופיעו כאילו בוצעו על ידי המשתמש עצמו.", "LabelApiToken": "טוקן API", "LabelAppend": "הוסף לסוף", "LabelAudioBitrate": "קצב סיביות (לדוגמא 128k)", @@ -289,6 +295,7 @@ "LabelContinueListening": "המשך האזנה", "LabelContinueReading": "המשך קריאה", "LabelContinueSeries": "המשך סדרה", + "LabelCorsAllowed": "מקורות CORS מורשים", "LabelCover": "כריכה", "LabelCoverImageURL": "כתובת התמונה ברשת", "LabelCoverProvider": "ספק כריכה", @@ -302,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק ממסד הנתונים)", "LabelDescription": "תיאור", "LabelDeselectAll": "הסר בחירת כל הפריטים", + "LabelDetectedPattern": "תבנית שזוהתה:", "LabelDevice": "התקן", "LabelDeviceInfo": "מידע על התקן", "LabelDeviceIsAvailableTo": "התקן זמין ל...", @@ -351,6 +359,10 @@ "LabelExample": "דוגמה", "LabelExpandSeries": "הרחב סדרה", "LabelExpandSubSeries": "הרחב תת סדרה", + "LabelExpired": "פג תוקף", + "LabelExpiresAt": "יפוג בתאריך", + "LabelExpiresInSeconds": "יפוג בעוד (שניות)", + "LabelExpiresNever": "ללא הגבלת זמן", "LabelExplicit": "מפורש", "LabelExplicitChecked": "בוטה (מסומן)", "LabelExplicitUnchecked": "לא בוטה (לא מסומן)", @@ -366,6 +378,7 @@ "LabelFilterByUser": "סינון לפי משתמש", "LabelFindEpisodes": "מצא פרקים", "LabelFinished": "הושלם", + "LabelFinishedDate": "הושלם {0}", "LabelFolder": "תיקייה", "LabelFolders": "תיקיות", "LabelFontBold": "מודגש", @@ -410,6 +423,7 @@ "LabelLanguages": "שפות", "LabelLastBookAdded": "הספר האחרון שנוסף", "LabelLastBookUpdated": "הספר האחרון שעודכן", + "LabelLastProgressDate": "התקדמות אחרונה: {0}", "LabelLastSeen": "נראה לאחרונה", "LabelLastTime": "הזמן האחרון", "LabelLastUpdate": "עדכון אחרון", @@ -422,6 +436,9 @@ "LabelLibraryFilterSublistEmpty": "לא {0}", "LabelLibraryItem": "פריט ספרייה", "LabelLibraryName": "שם הספרייה", + "LabelLibrarySortByProgress": "התקדמות: עודכן לאחרונה", + "LabelLibrarySortByProgressFinished": "התקדמות: הושלם", + "LabelLibrarySortByProgressStarted": "התקדמות: הותחל", "LabelLimit": "מגבלה", "LabelLineSpacing": "מרווח שורה", "LabelListenAgain": "האזן שוב", @@ -430,6 +447,7 @@ "LabelLogLevelWarn": "אזהרה", "LabelLookForNewEpisodesAfterDate": "חפש פרקים חדשים לאחר תאריך זה", "LabelLowestPriority": "העדיפות הנמוכה ביותר", + "LabelMatchConfidence": "רמת ודאות", "LabelMatchExistingUsersBy": "התאם משתמשים קיימים לפי", "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יותאמו לפי זיהוי ייחודי מספק ה-SSO שלך", "LabelMaxEpisodesToDownload": "מספר פרקים מקסימלי להורדה. 0 - ללא הגבלה.", @@ -459,7 +477,9 @@ "LabelNewestAuthors": "הסופרים האחרונים", "LabelNewestEpisodes": "הפרקים החדשים ביותר", "LabelNextBackupDate": "תאריך הגיבוי הבא", + "LabelNextChapters": "הפרקים הבא יהיו:", "LabelNextScheduledRun": "הרצה מתוזמנת הבאה", + "LabelNoApiKeys": "אין מפתחות API", "LabelNoCustomMetadataProviders": "אין ספקי מטא-נתונים מותאמים אישית", "LabelNoEpisodesSelected": "לא נבחרו פרקים", "LabelNotFinished": "לא הושלם", @@ -475,16 +495,21 @@ "LabelNotificationsMaxQueueSize": "גודל התור המרבי לאירועי התראה", "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא. הגדרה זו נועדה למנוע ספאם התראות.", "LabelNumberOfBooks": "מספר הספרים", + "LabelNumberOfChapters": "מספר הפרקים:", "LabelNumberOfEpisodes": "# פרקים", "LabelOpenIDAdvancedPermsClaimDescription": "שם OpenID claim המכילה הרשאות מתקדמות לפעולות משתמש בתוך האפליקציה, אשר יחולו על תפקידים שאינם מנהלי מערכת (אם הוגדרה). אם התביעה חסרה בתגובה, הגישה ל-ABS תידחה. אם אפשרות אחת חסרה, היא תטופל כ-false יש לוודא שטענת ספק הזהויות תואמת את המבנה הצפוי:", "LabelOpenIDClaims": "השאר את האפשרויות הבאות ריקות כדי להשבית הקצאת קבוצות והרשאות מתקדמת, ולאחר מכן להקצות אוטומטית את קבוצת 'משתמש'.", + "LabelOpenIDGroupClaimDescription": "שם ה־OpenID claim המכיל את רשימת הקבוצות של המשתמש. בדרך כלל נקרא groups. אם הוגדרה, האפליקציה תקצה תפקידים באופן אוטומטי על סמך השיוך לקבוצות, בתנאי ששמות הקבוצות ב־claim הם 'admin', 'user' או 'guest' (ללא רגישות לרישיות - Case-insensitive). ה־claim צריך להכיל רשימה; אם המשתמש משויך למספר קבוצות, האפליקציה תקצה את התפקיד בעל רמת הגישה הגבוהה ביותר. במידה ולא נמצאה קבוצה תואמת, הגישה תיחסם.", "LabelOpenRSSFeed": "פתח ערוץ RSS", "LabelOverwrite": "לשכפל", + "LabelPaginationPageXOfY": "עמוד {0} מתוך {1}", "LabelPassword": "סיסמה", "LabelPath": "נתיב", + "LabelPermanent": "קבוע", "LabelPermissionsAccessAllLibraries": "ניתן לגשת לכל הספריות", "LabelPermissionsAccessAllTags": "ניתן לגשת לכל התגיות", "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן בוטה", + "LabelPermissionsCreateEreader": "ניתן ליצור קורא ספרים דיגיטלי", "LabelPermissionsDelete": "מותר למחוק", "LabelPermissionsDownload": "מותר להוריד", "LabelPermissionsUpdate": "מותר לעדכן", @@ -492,6 +517,8 @@ "LabelPersonalYearReview": "השנה שלך בסקירה ({0})", "LabelPhotoPathURL": "נתיב/URL לתמונה", "LabelPlayMethod": "שיטת הפעלה", + "LabelPlaybackRateIncrementDecrement": "שיעור הגדלה/הפחתה של מהירות ההשמעה", + "LabelPlayerChapterNumberMarker": "{0} מתוך {1}", "LabelPlaylists": "רשימות השמעה", "LabelPodcast": "פודקאסט", "LabelPodcastSearchRegion": "אזור חיפוש פודקאסט", @@ -503,10 +530,14 @@ "LabelPrimaryEbook": "ספר אלקטרוני ראשי", "LabelProgress": "התקדמות", "LabelProvider": "ספק", + "LabelProviderAuthorizationValue": "ערך כותרת האימות (Authorization Header)", "LabelPubDate": "תאריך פרסום", "LabelPublishYear": "שנת הפרסום", "LabelPublishedDate": "פורסם {0}", + "LabelPublishedDecade": "עשור פרסום", + "LabelPublishedDecades": "עשורי פרסום", "LabelPublisher": "מוציא לאור", + "LabelPublishers": "מוצאים לאור", "LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית", "LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית", "LabelRSSFeedOpen": "ערוץ RSS פתוח", @@ -514,6 +545,7 @@ "LabelRSSFeedSlug": "Slug של ערוץ ה-RSS", "LabelRSSFeedURL": "כתובת ערוץ ה-RSS", "LabelRandomly": "באופן אקראי", + "LabelReAddSeriesToContinueListening": "הוסף סדרה בחזרה אל ״המשך האזנה״", "LabelRead": "קריאה", "LabelReadAgain": "קרא שוב", "LabelReadEbookWithoutProgress": "קרא/י ספר אלקטרוני ללא שמירת התקדמות", @@ -523,29 +555,44 @@ "LabelRedo": "עשה שוב", "LabelRegion": "אזור", "LabelReleaseDate": "תאריך הוצאה לאור", + "LabelRemoveAllMetadataAbs": "הסר את כל קבצי metadata.abs", + "LabelRemoveAllMetadataJson": "הסר את כל קבצי metadata.json", + "LabelRemoveAudibleBranding": "הסר פתיח וסיום של Audible מהפרקים", "LabelRemoveCover": "הסר כריכה", + "LabelRemoveMetadataFile": "הסר קבצי מטא־נתונים מתיקיות הפריטים בספרייה", + "LabelRemoveMetadataFileHelp": "הסר את כל קבצי metadata.json ו־metadata.abs מתיקיות {0}.", "LabelRowsPerPage": "שורות לעמוד", "LabelSearchTerm": "מונח חיפוש", "LabelSearchTitle": "כותרת חיפוש", "LabelSearchTitleOrASIN": "כותרת חיפוש או ASIN", "LabelSeason": "עונה", + "LabelSeasonNumber": "עונה #{0}", + "LabelSelectAll": "בחר הכל", "LabelSelectAllEpisodes": "בחר את כל הפרקים", "LabelSelectEpisodesShowing": "בחר {0} פרקים המוצגים", + "LabelSelectUser": "בחר משתמש", "LabelSelectUsers": "בחר משתמשים", "LabelSendEbookToDevice": "שלח ספר אלקטרוני ל...", "LabelSequence": "רצף", + "LabelSerial": "מספר סידורי", "LabelSeries": "סדרה", "LabelSeriesName": "שם הסדרה", "LabelSeriesProgress": "התקדמות בסדרה", + "LabelServerLogLevel": "רמת פירוט יומני הרישום", "LabelServerYearReview": "השנה בסקירה של השרת ({0})", "LabelSetEbookAsPrimary": "קבע כראשי", "LabelSetEbookAsSupplementary": "קבע כמשלים", + "LabelSettingsAllowIframe": "אפשר הטמעה בתוך iframe", "LabelSettingsAudiobooksOnly": "רק ספרי קול", "LabelSettingsAudiobooksOnlyHelp": "הפעלת ההגדרה הזו תתעלם מקבצי ספרים אלקטרוניים אלא אם כן הם נמצאים בתיקיית ספרי קול, שבמקרה זה יקבעו כספרים אלקטרוניים נלווים", "LabelSettingsBookshelfViewHelp": "עיצוב סקאומורפי עם מדפי עץ", "LabelSettingsChromecastSupport": "תמיכה ב-Chromecast", "LabelSettingsDateFormat": "פורמט תאריך", + "LabelSettingsEnableWatcher": "הפעל מעקב שינויים בספריות", + "LabelSettingsEnableWatcherForLibrary": "הפעל מעקב שינויים בספרייה", "LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", + "LabelSettingsEpubsAllowScriptedContent": "אפשור תוכן הכולל סקריפטים ב־ePubs", + "LabelSettingsEpubsAllowScriptedContentHelp": "אפשר לקובצי EPUB להריץ סקריפטים. מומלץ להשאיר את ההגדרה כבויה, אלא אם כן מקור קובצי ה־ePub מהימן.", "LabelSettingsExperimentalFeatures": "תכונות ניסיוניות", "LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.", "LabelSettingsFindCovers": "מצא כריכות", @@ -554,7 +601,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "סדרות הכוללות ספר אחד יוסתרו מדף הסדרות ומדף הבית.", "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף בדף הבית", "LabelSettingsLibraryBookshelfView": "השתמש בתצוגת מדף בספרייה", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים ב-המשך סדרה", + "LabelSettingsLibraryMarkAsFinishedWhen": "סמן פריט מדיה כהושלם כאשר", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים ב״המשך סדרה״", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא הושמע בסדרה שיש בה לפחות ספר אחד שהושלם ואין ספרים שכבר באמצע שמיעה. הפעלת הגדרה זו תמשיך סדרות מהספר שהושלם הכי מתקדם בסדרה במקום מהספר הראשון שלא הושמע.", "LabelSettingsParseSubtitles": "פענח כתוביות", "LabelSettingsParseSubtitlesHelp": "העתק כותרת משנה משם תיקיית הספר.
כותרת המשנה חייבת להיות מופרדת עם התו ״-״
לדוגמא, כותרת המשנה לספר ״שם הספר - כותרת משנה״, היא ״כותרת משנה״", @@ -571,13 +619,22 @@ "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם הפריט", "LabelSettingsStoreMetadataWithItemHelp": "כברירת מחדל, קבצי מטה-נתונים מאוחסנים ב- /metadata/items, הפעלת ההגדרה תאחסן קבצי מטה-נתונים בתיקיית פריט שלך בספרייה", "LabelSettingsTimeFormat": "פורמט זמן", + "LabelShare": "שתף", + "LabelShareDownloadableHelp": "אפשר למי שיש ברשותו קישור שיתוף להוריד קובץ ZIP של פריט הספרייה.", + "LabelShareURL": "שתף קישור", "LabelShowAll": "הצג הכל", + "LabelShowSeconds": "הצג שניות", + "LabelShowSubtitles": "הצג כתוביות", "LabelSize": "גודל", "LabelSleepTimer": "טיימר שינה", + "LabelSortAscending": "סדר עולה", + "LabelSortDescending": "סדר יורד", + "LabelSortPubDate": "מיין לפי תאריך פרסום", "LabelStart": "התחל", "LabelStartTime": "זמן התחלה", "LabelStarted": "התחיל", "LabelStartedAt": "התחיל ב", + "LabelStartedDate": "הותחל {0}", "LabelStatsAudioTracks": "רצועות שמע", "LabelStatsAuthors": "מחברים", "LabelStatsBestDay": "היום הטוב ביותר", @@ -607,7 +664,13 @@ "LabelTheme": "ערכת נושא", "LabelThemeDark": "כהה", "LabelThemeLight": "בהיר", + "LabelThemeSepia": "ספיה", "LabelTimeBase": "בסיס זמן", + "LabelTimeDurationXHours": "{0} שעות", + "LabelTimeDurationXMinutes": "{0} דקות", + "LabelTimeDurationXSeconds": "{0} שניות", + "LabelTimeInMinutes": "זמן בשניות", + "LabelTimeLeft": "נותרו {0}", "LabelTimeListened": "זמן האזנה", "LabelTimeListenedToday": "זמן האזנה היום", "LabelTimeRemaining": "{0} נותרו", @@ -615,6 +678,7 @@ "LabelTitle": "כותרת", "LabelToolsEmbedMetadata": "הטמעת מטה-נתונים", "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות כריכה ופרקים.", + "LabelToolsM4bEncoder": "מקודד M4B", "LabelToolsMakeM4b": "יצירת קובץ אודיו M4B", "LabelToolsMakeM4bDescription": "יצירת קובץ אודיו .M4B עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", "LabelToolsSplitM4b": "פיצול M4B ל-MP3", @@ -627,29 +691,39 @@ "LabelTracksMultiTrack": "רב-ערוצי", "LabelTracksNone": "אין ערוצים", "LabelTracksSingleTrack": "רצועה יחידה", + "LabelTrailer": "קדימון", "LabelType": "סוג", "LabelUnabridged": "לא מקוצר", "LabelUndo": "בטל", "LabelUnknown": "לא ידוע", + "LabelUnknownPublishDate": "תאריך הוצאה לאור לא ידוע", "LabelUpdateCover": "עדכן כריכה", "LabelUpdateCoverHelp": "אפשר החלפה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", "LabelUpdateDetails": "עדכון פרטים", "LabelUpdateDetailsHelp": "אפשר החלפה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", "LabelUpdatedAt": "עודכן ב-", "LabelUploaderDragAndDrop": "גרור ושחרר קבצים או תיקיות", + "LabelUploaderDragAndDropFilesOnly": "גרור ושחרר קבצים", "LabelUploaderDropFiles": "שחרר קבצים", "LabelUploaderItemFetchMetadataHelp": "משיכת כותרת, סופר וסדרה באופן אוטומטי", + "LabelUseAdvancedOptions": "השתמש באפשרויות מתקדמות", "LabelUseChapterTrack": "השתמש ברצועות הפרקים", "LabelUseFullTrack": "השתמש ברצועה המלאה", + "LabelUseZeroForUnlimited": "השתמש ב־0 מתוך אין־סוף", "LabelUser": "משתמש", "LabelUsername": "שם משתמש", "LabelValue": "ערך", "LabelVersion": "גרסה", "LabelViewBookmarks": "הצג סימניות", "LabelViewChapters": "הצג פרקים", + "LabelViewPlayerSettings": "הצג הגדרות נגן", "LabelViewQueue": "הצג תור נגן", "LabelVolume": "עוצמת קול", + "LabelWebRedirectURLsDescription": "יש לאשר את הכתובות הבאות אצל ספק ה־OAuth כדי לאפשר הפניה חזרה לאפליקציית הדפדפן לאחר ההתחברות:", + "LabelWebRedirectURLsSubfolder": "תיקיית משנה לכתובות הפניה", "LabelWeekdaysToRun": "ימי השבוע להרצה", + "LabelXBooks": "{0} ספרים", + "LabelXItems": "{0} פריטים", "LabelYearReviewHide": "הסתר סקירת שנה", "LabelYearReviewShow": "הצג סקירת שנה", "LabelYourAudiobookDuration": "משך הספר הקולי שלך", @@ -658,31 +732,55 @@ "LabelYourProgress": "ההתקדמות שלך", "MessageAddToPlayerQueue": "הוסף לתור הנגן", "MessageAppriseDescription": "כדי להשתמש בתכונה זו יש לך להריץ מופע של ממשק התכנית האפליקציה או API שיטפל בבקשות אלו.
כתובת URL של ממשק ה-Apprise API צריכה להיות הנתיב המלא לשליחת ההתראה, לדוגמה, אם המופע של ה-API שלך מוצע ב-http://192.168.1.1:8337 אז עליך לשים http://192.168.1.1:8337/notify.", + "MessageAsinCheck": "יש לוודא שימוש ב־ASIN מאזור ה־Audible הנכון, ולא מ־Amazon.", + "MessageAuthenticationLegacyTokenWarning": "אסימוני API ישנים יוסרו בעתיד. יש להשתמש ב מפתחות API במקום.", + "MessageAuthenticationOIDCChangesRestart": "יש להפעיל מחדש את השרת לאחר השמירה כדי להחיל את שינויי ה־OIDC.", + "MessageAuthenticationSecurityMessage": "האימות שופר מטעמי אבטחה. כל המשתמשים נדרשים להתחבר מחדש.", "MessageBackupsDescription": "גיבויים כוללים משתמשים, התקדמות משתמש, פרטי פריטי ספרייה, הגדרות שרת ותמונות השמורות ב-/metadata/items & /metadata/authors. גיבויים לא כוללים קבצים שמורים בתיקיות הספרייה שלך.", + "MessageBackupsLocationEditNote": "הערה: שינוי מיקום הגיבוי לא יגרום להעברה או לשינוי של גיבויים קיימים", + "MessageBackupsLocationNoEditNote": "הערה: מיקום הגיבוי מוגדר באמצעות משתנה סביבה ולא ניתן לשנותו כאן.", + "MessageBackupsLocationPathEmpty": "נתיב מיקום הגיבוי אינו יכול להיות ריק", + "MessageBatchEditPopulateMapDetailsAllHelp": "מלא את השדות הפעילים בנתונים מכל הפריטים. שדות בעלי ערכים מרובים ימוזגו", + "MessageBatchEditPopulateMapDetailsItemHelp": "מלא את שדות פרטי המיפוי הפעילים בנתונים מפריט זה", "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה להחליף כריכות קיימות ו/או מטה-נתונים.", "MessageBookshelfNoCollections": "עדיין לא יצרת אוספים", + "MessageBookshelfNoCollectionsHelp": "האוספים ציבוריים. כל המשתמשים בעלי גישה לספרייה יכולים לראות אותם.", "MessageBookshelfNoRSSFeeds": "אין ערוצי RSS פתוחים", "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור סינון \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "אין תוצאות עבור השאילתה", "MessageBookshelfNoSeries": "אין לך סדרות", + "MessageBulkChapterPattern": "כמה פרקים להוסיף לפי תבנית מספור זו?", "MessageChapterEndIsAfter": "זמן סיום הפרק אחרי סיום הספר הקולי שלך", "MessageChapterErrorFirstNotZero": "הפרק הראשון חייב להתחיל ב-0", "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין, חייב להיות פחות ממשך הספר הקולי", "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין, חייב להיות גדול או שווה לזמן ההתחלה של הפרק הקודם", "MessageChapterStartIsAfter": "התחלת הפרק אחרי סיום הספר הקולי שלך", + "MessageChaptersNotFound": "לא נמצאו פרקים", "MessageCheckingCron": "בודק את תזמון העבודה...", "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הערוץ הזה?", + "MessageConfirmDeleteApiKey": "האם למחוק את מפתח ה־API \"{0}\"?", "MessageConfirmDeleteBackup": "האם אתה בטוח שברצונך למחוק גיבוי עבור {0}?", + "MessageConfirmDeleteDevice": "האם למחוק את הקורא האלקטרוני \"{0}\"?", "MessageConfirmDeleteFile": "הקובץ ימחק לצמיתות מהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteLibrary": "האם אתה בטוח שברצונך למחוק לצמיתות את הספרייה \"{0}\"?", "MessageConfirmDeleteLibraryItem": "פריט הספרייה יימחק לצמיתות ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteLibraryItems": "פריטי הספרייה {0} יימחקו ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteMetadataProvider": "האם למחוק את ספק המטא־נתונים המותאם \"{0}\"?", + "MessageConfirmDeleteNotification": "האם למחוק התראה זו?", "MessageConfirmDeleteSession": "האם אתה בטוח שאתה רוצה למחוק את ההפעלה הזו?", + "MessageConfirmEmbedMetadataInAudioFiles": "האם להטמיע מטא־נתונים ב־{0} קובצי שמע?", "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה להכריח סריקה מחדש?", "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כהסתיימו?", "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא הסתיימו?", + "MessageConfirmMarkItemFinished": "האם לסמן את \"{0}\" כהושלם?", + "MessageConfirmMarkItemNotFinished": "האם לסמן את \"{0}\" כלא הושלם?", "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כהסתיימו?", "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא הסתיימו?", + "MessageConfirmNotificationTestTrigger": "האם להפעיל התראה זו עם נתוני בדיקה?", + "MessageConfirmPurgeCache": "ניקוי המטמון ימחק את כל התיקייה ב־/metadata/cache.

האם למחוק את תיקיית המטמון?", + "MessageConfirmPurgeItemsCache": "ניקוי מטמון הפריטים ימחק את כל התיקייה ב־metadata/cache/items/.
האם למחוק?", "MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך.

האם ברצונך להמשיך?", + "MessageConfirmQuickMatchEpisodes": "התאמה מהירה תדרוס פרטים עבור פרקים תואמים. רק פרקים ללא התאמה יעודכנו. האם להמשיך?", "MessageConfirmReScanLibraryItems": "האם אתה בטוח שברצונך לסרוק מחדש {0} פריטים?", "MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?", "MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?", From cc5244c5966ce1a3041148eea466fe16bda1be7e Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Mon, 2 Feb 2026 08:50:32 +0100 Subject: [PATCH 150/285] Translated using Weblate (Swedish) 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/sv/ --- client/strings/sv.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index d2fb254ed..e1c08efa5 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ingen {0}", "LabelLibraryItem": "Objekt", "LabelLibraryName": "Biblioteksnamn", - "LabelLibrarySortByProgress": "Framsteg: senast uppdaterat", - "LabelLibrarySortByProgressFinished": "Framsteg: avslutad", - "LabelLibrarySortByProgressStarted": "Framsteg: påbörjad", + "LabelLibrarySortByProgress": "Status: Senast uppdaterad", + "LabelLibrarySortByProgressFinished": "Status: Avslutad", + "LabelLibrarySortByProgressStarted": "Status: Startad", "LabelLimit": "Begränsning", "LabelLineSpacing": "Radavstånd", "LabelListenAgain": "Lyssna igen", From edfce4605856f34e68400f072a3dee73f3b10622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Delta=20Umh=C3=B6fer?= Date: Sat, 31 Jan 2026 20:09:59 +0100 Subject: [PATCH 151/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 970c94233..cf36b07fd 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -436,7 +436,7 @@ "LabelLibraryFilterSublistEmpty": "Keine {0}", "LabelLibraryItem": "Bibliothekseintrag", "LabelLibraryName": "Bibliotheksname", - "LabelLibrarySortByProgress": "Fortschritt: Zuletzt aktualisiert", + "LabelLibrarySortByProgress": "Fortschritt: Letzte Aktualisierung", "LabelLibrarySortByProgressFinished": "Fortschritt: Beendet", "LabelLibrarySortByProgressStarted": "Fortschritt: Gestartet", "LabelLimit": "Begrenzung", From 31630f50a59d598478db6692aaba6bf3089435be Mon Sep 17 00:00:00 2001 From: dapitch666 Date: Mon, 2 Feb 2026 12:17:14 +0100 Subject: [PATCH 152/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index 6ff72c7ef..497426d37 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -436,11 +436,11 @@ "LabelLibraryFilterSublistEmpty": "Aucun {0}", "LabelLibraryItem": "Élément de bibliothèque", "LabelLibraryName": "Nom de la bibliothèque", - "LabelLibrarySortByProgress": "Progression : dernière mise à jour", + "LabelLibrarySortByProgress": "Progression : Mise à jour", "LabelLibrarySortByProgressFinished": "Progression : Terminé", - "LabelLibrarySortByProgressStarted": "Progression : Commencé", + "LabelLibrarySortByProgressStarted": "Progression : En cours", "LabelLimit": "Limite", - "LabelLineSpacing": "Espacement des lignes", + "LabelLineSpacing": "Interligne", "LabelListenAgain": "Écouter à nouveau", "LabelLogLevelDebug": "Débogage", "LabelLogLevelInfo": "Info", @@ -961,7 +961,7 @@ "PlaceholderNewCollection": "Nom de la nouvelle collection", "PlaceholderNewFolderPath": "Nouveau chemin de dossier", "PlaceholderNewPlaylist": "Nouveau nom de liste de lecture", - "PlaceholderSearch": "Recherche…", + "PlaceholderSearch": "Recherche...", "PlaceholderSearchEpisode": "Rechercher un épisode…", "StatsAuthorsAdded": "auteurs ajoutés", "StatsBooksAdded": "livres ajoutés", From 2ee893062f1eb829107f7a358ec9ff128949e6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Llu=C3=ADs=20Forns?= Date: Tue, 3 Feb 2026 11:37:12 +0100 Subject: [PATCH 153/285] Translated using Weblate (Catalan) Currently translated at 92.2% (1073 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ca/ --- client/strings/ca.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/strings/ca.json b/client/strings/ca.json index dbda8b8e4..4180b2518 100644 --- a/client/strings/ca.json +++ b/client/strings/ca.json @@ -166,6 +166,7 @@ "HeaderMetadataOrderOfPrecedence": "Ordre de Precedència de Metadades", "HeaderMetadataToEmbed": "Metadades a Inserir", "HeaderNewAccount": "Nou Compte", + "HeaderNewApiKey": "Nova clau API", "HeaderNewLibrary": "Nova Biblioteca", "HeaderNotificationCreate": "Crea Notificació", "HeaderNotificationUpdate": "Actualització de Notificació", @@ -199,6 +200,7 @@ "HeaderSettingsExperimental": "Funcionalitats experimentals", "HeaderSettingsGeneral": "Generals", "HeaderSettingsScanner": "Escàner", + "HeaderSettingsSecurity": "Seguretat", "HeaderSettingsWebClient": "Client web", "HeaderSleepTimer": "Temporitzador de son", "HeaderStatsLargestItems": "Elements més grans", @@ -421,6 +423,9 @@ "LabelLibraryFilterSublistEmpty": "Sense {0}", "LabelLibraryItem": "Element de Biblioteca", "LabelLibraryName": "Nom de Biblioteca", + "LabelLibrarySortByProgress": "Progrés: Última actualització", + "LabelLibrarySortByProgressFinished": "Progrés: Finalitzat", + "LabelLibrarySortByProgressStarted": "Progrés: Començat", "LabelLimit": "Límits", "LabelLineSpacing": "Interlineat", "LabelListenAgain": "Escoltar de nou", @@ -443,7 +448,7 @@ "LabelMetadataProvider": "Proveïdor de metadades", "LabelMinute": "Minut", "LabelMinutes": "Minuts", - "LabelMissing": "Absent", + "LabelMissing": "Falta", "LabelMissingEbook": "No té llibre electrònic", "LabelMissingSupplementaryEbook": "No té ebook complementari", "LabelMobileRedirectURIs": "URI de redirecció mòbil permeses", From c15cb48def796006807ff105945f22829f82bab8 Mon Sep 17 00:00:00 2001 From: Khashayar Toodehfallah Date: Fri, 6 Feb 2026 14:31:09 -0500 Subject: [PATCH 154/285] Improved subtitle parsing to account for bare colon in title --- server/finders/BookFinder.js | 8 ++++---- server/utils/parsers/parseNfoMetadata.js | 2 +- test/server/finders/BookFinder.test.js | 3 +++ test/server/utils/parsers/parseNfoMetadata.test.js | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 6d31b2ab9..90fed951f 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -227,7 +227,7 @@ class BookFinder { title = this.#removeAuthorFromTitle(title) const titleTransformers = [ - [/([,:;_]| by ).*/g, ''], // Remove subtitle + [/(: |[,;_]| by ).*/g, ''], // Remove subtitle [/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate [/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition [/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type @@ -646,11 +646,11 @@ class BookFinder { module.exports = new BookFinder() function hasSubtitle(title) { - return title.includes(':') || title.includes(' - ') + return title.includes(': ') || title.includes(' - ') } function stripSubtitle(title) { - if (title.includes(':')) { - return title.split(':')[0].trim() + if (title.includes(': ')) { + return title.split(': ')[0].trim() } else if (title.includes(' - ')) { return title.split(' - ')[0].trim() } diff --git a/server/utils/parsers/parseNfoMetadata.js b/server/utils/parsers/parseNfoMetadata.js index 6682a0078..4276443d0 100644 --- a/server/utils/parsers/parseNfoMetadata.js +++ b/server/utils/parsers/parseNfoMetadata.js @@ -22,7 +22,7 @@ function parseNfoMetadata(nfoText) { switch (key) { case 'title': { - const titleMatch = value.match(/^(.*?):(.*)$/) + const titleMatch = value.match(/^(.*?): (.*)$/) if (titleMatch) { metadata.title = titleMatch[1].trim() metadata.subtitle = titleMatch[2].trim() diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js index 6578ca82a..cb697b81a 100644 --- a/test/server/finders/BookFinder.test.js +++ b/test/server/finders/BookFinder.test.js @@ -35,7 +35,10 @@ describe('TitleCandidates', () => { ['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']], ['does not add empty candidate after removing author', cleanAuthor, []], ['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']], + ['adds candidate, not stripping subtitle for bare colon in title', '10:04', ['10:04']], + ['adds candidate, not stripping subtitle for colon between words without space', 'making the mission:impossible movies', ['making the mission:impossible movies']], ['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], + ['adds candidate + variant, removing "by ..." when title has bare colon', '10:04 by ben lerner', ['10:04', '10:04 by ben lerner']], ['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], ['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], diff --git a/test/server/utils/parsers/parseNfoMetadata.test.js b/test/server/utils/parsers/parseNfoMetadata.test.js index 9ff51fbe5..ba11ddff2 100644 --- a/test/server/utils/parsers/parseNfoMetadata.test.js +++ b/test/server/utils/parsers/parseNfoMetadata.test.js @@ -21,6 +21,20 @@ describe('parseNfoMetadata', () => { expect(result.subtitle).to.equal('A Novel') }) + it('does not split title on bare colon without space', () => { + const nfoText = 'Title: 10:04' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('10:04') + expect(result.subtitle).to.be.undefined + }) + + it('does not split title on colon between words without space', () => { + const nfoText = 'Title: Making the Mission:Impossible Movies' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('Making the Mission:Impossible Movies') + expect(result.subtitle).to.be.undefined + }) + it('parses authors', () => { const nfoText = 'Author: F. Scott Fitzgerald' const result = parseNfoMetadata(nfoText) From dd4fc09909a0a2862756292b11fd684b2f427c1f Mon Sep 17 00:00:00 2001 From: Michael Tuttle Date: Sun, 8 Feb 2026 00:19:23 -0700 Subject: [PATCH 155/285] Fix OpenAPI spec description --- custom-metadata-provider-specification.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom-metadata-provider-specification.yaml b/custom-metadata-provider-specification.yaml index 71cbba23a..a0ec172ae 100644 --- a/custom-metadata-provider-specification.yaml +++ b/custom-metadata-provider-specification.yaml @@ -127,7 +127,7 @@ components: duration: type: integer format: int64 - description: Duration in seconds + description: Duration in minutes SeriesMetadata: type: object From fa5fa7b7884adad3d020745aa97f5a52ca6e6156 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 14 Feb 2026 17:17:12 -0600 Subject: [PATCH 156/285] Fix server crash on /me/progress/:libraryItemId/:episodeId? when episodeId is not passed in for a podcast library item #5058 --- server/models/User.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/models/User.js b/server/models/User.js index 36b2eca98..936efde13 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -782,7 +782,14 @@ class User extends Model { error: 'Library item not found', statusCode: 404 } + } else if (libraryItem.mediaType !== 'book') { + Logger.error(`[User] createUpdateMediaProgress: library item ${progressPayload.libraryItemId} is not a book`) + return { + error: 'Library item is not a book', + statusCode: 400 + } } + mediaItemId = libraryItem.media.id mediaProgress = libraryItem.media.mediaProgresses?.[0] } From ade1752e979216bcc206cc423a1a7a89978204e1 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Sun, 15 Feb 2026 22:01:36 -0500 Subject: [PATCH 157/285] Fix IDOR bugs --- server/controllers/MeController.js | 48 ++- test/server/MeController.test.js | 638 +++++++++++++++++++++++++++++ 2 files changed, 682 insertions(+), 4 deletions(-) create mode 100644 test/server/MeController.test.js diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 51773a5ad..c5968f521 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -63,7 +63,7 @@ class MeController { * @param {Response} res */ async getItemListeningSessions(req, res) { - const libraryItem = await Database.libraryItemModel.findByPk(req.params.libraryItemId) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.libraryItemId) const episode = await Database.podcastEpisodeModel.findByPk(req.params.episodeId) if (!libraryItem || (libraryItem.isPodcast && !episode)) { @@ -71,6 +71,12 @@ class MeController { return res.sendStatus(404) } + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to access listening sessions for library item "${req.params.libraryItemId}" without access`) + return res.sendStatus(403) + } + const mediaItemId = episode?.id || libraryItem.mediaId let listeningSessions = await this.getUserItemListeningSessionsHelper(req.user.id, mediaItemId) @@ -125,6 +131,13 @@ class MeController { * @param {Response} res */ async removeMediaProgress(req, res) { + // Verify the media progress belongs to the current user + const mediaProgress = req.user.mediaProgresses.find((mp) => mp.id === req.params.id) + if (!mediaProgress) { + Logger.error(`[MeController] Media progress not found or does not belong to user "${req.user.username}"`) + return res.sendStatus(404) + } + await Database.mediaProgressModel.removeById(req.params.id) req.user.mediaProgresses = req.user.mediaProgresses.filter((mp) => mp.id !== req.params.id) @@ -192,7 +205,16 @@ class MeController { * @param {Response} res */ async createBookmark(req, res) { - if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id) + if (!libraryItem) { + return res.sendStatus(404) + } + + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to create bookmark for library item "${req.params.id}" without access`) + return res.sendStatus(403) + } const { time, title } = req.body if (isNullOrNaN(time)) { @@ -216,7 +238,16 @@ class MeController { * @param {Response} res */ async updateBookmark(req, res) { - if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id) + if (!libraryItem) { + return res.sendStatus(404) + } + + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to update bookmark for library item "${req.params.id}" without access`) + return res.sendStatus(403) + } const { time, title } = req.body if (isNullOrNaN(time)) { @@ -245,7 +276,16 @@ class MeController { * @param {Response} res */ async removeBookmark(req, res) { - if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id) + if (!libraryItem) { + return res.sendStatus(404) + } + + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to remove bookmark for library item "${req.params.id}" without access`) + return res.sendStatus(403) + } const time = Number(req.params.time) if (isNaN(time)) { diff --git a/test/server/MeController.test.js b/test/server/MeController.test.js new file mode 100644 index 000000000..2f6b502db --- /dev/null +++ b/test/server/MeController.test.js @@ -0,0 +1,638 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') +const sinon = require('sinon') + +const Database = require('../../../server/Database') +const ApiRouter = require('../../../server/routers/ApiRouter') +const MeController = require('../../../server/controllers/MeController') +const Auth = require('../../../server/Auth') +const Logger = require('../../../server/Logger') +const SocketAuthority = require('../../../server/SocketAuthority') + +describe('MeController - IDOR Security Tests', () => { + /** @type {ApiRouter} */ + let apiRouter + + beforeEach(async () => { + global.ServerSettings = {} + Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + await Database.buildModels() + + // Create mock server object with required dependencies + const mockServer = { + auth: new Auth(), + playbackSessionManager: { sessions: [] }, + abMergeManager: {}, + backupManager: {}, + podcastManager: {}, + audioMetadataManager: {}, + cronManager: {}, + emailManager: {}, + apiCacheManager: { middleware: (req, res, next) => next() } + } + + apiRouter = new ApiRouter(mockServer) + + sinon.stub(Logger, 'info') + sinon.stub(Logger, 'error') + sinon.stub(SocketAuthority, 'clientEmitter') + }) + + afterEach(async () => { + sinon.restore() + + // Clear all tables + await Database.sequelize.sync({ force: true }) + }) + + describe('removeMediaProgress - IDOR Protection', () => { + let user1, user2 + let mediaProgress1, mediaProgress2 + + beforeEach(async () => { + // Create two users + user1 = await Database.userModel.create({ + username: 'user1', + pash: 'hashed_password_1', + type: 'user', + isActive: true + }) + + user2 = await Database.userModel.create({ + username: 'user2', + pash: 'hashed_password_2', + type: 'user', + isActive: true + }) + + // Create library and book + const library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + const libraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: library.id }) + const book = await Database.bookModel.create({ title: 'Test Book', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const libraryItem = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book.id, + mediaType: 'book', + libraryId: library.id, + libraryFolderId: libraryFolder.id + }) + + // Create media progress for each user + mediaProgress1 = await Database.mediaProgressModel.create({ + userId: user1.id, + mediaItemId: book.id, + mediaItemType: 'book', + duration: 1000, + currentTime: 500, + isFinished: false + }) + + mediaProgress2 = await Database.mediaProgressModel.create({ + userId: user2.id, + mediaItemId: book.id, + mediaItemType: 'book', + duration: 1000, + currentTime: 300, + isFinished: false + }) + + // Load media progresses into users + user1.mediaProgresses = await user1.getMediaProgresses() + user2.mediaProgresses = await user2.getMediaProgresses() + }) + + it('should allow user to delete their own media progress', async () => { + const fakeReq = { + user: user1, + params: { id: mediaProgress1.id } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + await MeController.removeMediaProgress(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(200)).to.be.true + + // Verify media progress was deleted + const deletedProgress = await Database.mediaProgressModel.findByPk(mediaProgress1.id) + expect(deletedProgress).to.be.null + }) + + it('should prevent user from deleting another users media progress (IDOR)', async () => { + const fakeReq = { + user: user1, + params: { id: mediaProgress2.id } // Trying to delete user2's progress + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + await MeController.removeMediaProgress(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + + // Verify media progress was NOT deleted + const existingProgress = await Database.mediaProgressModel.findByPk(mediaProgress2.id) + expect(existingProgress).to.not.be.null + expect(existingProgress.userId).to.equal(user2.id) + }) + + it('should return 404 for non-existent media progress', async () => { + const fakeReq = { + user: user1, + params: { id: 'non-existent-id' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + await MeController.removeMediaProgress(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + }) + }) + + describe('Bookmark Operations - Authorization Checks', () => { + let user1, user2 + let library1, library2 + let libraryItem1, libraryItem2 + + beforeEach(async () => { + // Create two users with different library access + user1 = await Database.userModel.create({ + username: 'user1', + pash: 'hashed_password_1', + type: 'user', + isActive: true, + librariesAccessible: null // Access to all libraries + }) + + user2 = await Database.userModel.create({ + username: 'user2', + pash: 'hashed_password_2', + type: 'user', + isActive: true, + librariesAccessible: [] // Will be set to specific library + }) + + // Create two libraries + library1 = await Database.libraryModel.create({ name: 'Library 1', mediaType: 'book' }) + library2 = await Database.libraryModel.create({ name: 'Library 2', mediaType: 'book' }) + + // User2 only has access to library1 + user2.librariesAccessible = [library1.id] + await user2.save() + + const libraryFolder1 = await Database.libraryFolderModel.create({ path: '/test1', libraryId: library1.id }) + const libraryFolder2 = await Database.libraryFolderModel.create({ path: '/test2', libraryId: library2.id }) + + const book1 = await Database.bookModel.create({ title: 'Book 1', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const book2 = await Database.bookModel.create({ title: 'Book 2', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + + libraryItem1 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book1.id, + mediaType: 'book', + libraryId: library1.id, + libraryFolderId: libraryFolder1.id + }) + + libraryItem2 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book2.id, + mediaType: 'book', + libraryId: library2.id, + libraryFolderId: libraryFolder2.id + }) + + // Initialize bookmarks + user1.bookmarks = [] + user2.bookmarks = [] + }) + + describe('createBookmark', () => { + it('should allow user to create bookmark for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const bookmark = { libraryItemId: libraryItem1.id, time: 100, title: 'Test Bookmark', createdAt: Date.now() } + + const fakeReq = { + user: { + ...user2.toJSON(), + id: user2.id, + username: user2.username, + checkCanAccessLibraryItem: () => true, + createBookmark: sinon.stub().resolves(bookmark), + toOldJSONForBrowser: () => ({ id: user2.id, username: user2.username }) + }, + params: { id: libraryItem1.id }, + body: { time: 100, title: 'Test Bookmark' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.json.calledOnce).to.be.true + expect(fakeRes.json.calledWith(bookmark)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should prevent user from creating bookmark for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { id: libraryItem2.id }, + body: { time: 100, title: 'Test Bookmark' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + // Mock getExpandedById + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + expect(fakeRes.json.called).to.be.false + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should return 404 for non-existent library item', async () => { + const fakeReq = { + user: user1, + params: { id: 'non-existent-id' }, + body: { time: 100, title: 'Test Bookmark' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + // Mock getExpandedById to return null + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(null) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should validate bookmark time parameter', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true + }, + params: { id: libraryItem1.id }, + body: { time: null, title: 'Test Bookmark' } // null time is invalid + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.status.calledWith(400)).to.be.true + expect(fakeRes.send.calledWith('Invalid time')).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) + + describe('updateBookmark', () => { + beforeEach(async () => { + // Add existing bookmark to user1 + user1.bookmarks = [{ libraryItemId: libraryItem1.id, time: 100, title: 'Original Title' }] + await user1.save() + }) + + it('should allow user to update bookmark for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const bookmark = { libraryItemId: libraryItem1.id, time: 100, title: 'Updated Title' } + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true, + updateBookmark: sinon.stub().resolves(bookmark), + toOldJSONForBrowser: () => ({ id: user1.id, username: user1.username }) + }, + params: { id: libraryItem1.id }, + body: { time: 100, title: 'Updated Title' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.updateBookmark(fakeReq, fakeRes) + + expect(fakeRes.json.calledOnce).to.be.true + expect(fakeRes.json.calledWith(bookmark)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should prevent user from updating bookmark for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { id: libraryItem2.id }, + body: { time: 100, title: 'Updated Title' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.updateBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) + + describe('removeBookmark', () => { + beforeEach(async () => { + // Add existing bookmark to user1 + user1.bookmarks = [{ libraryItemId: libraryItem1.id, time: 100, title: 'Test Bookmark' }] + await user1.save() + }) + + it('should allow user to remove bookmark for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true, + findBookmark: sinon.stub().returns({ libraryItemId: libraryItem1.id, time: 100, title: 'Test Bookmark' }), + removeBookmark: sinon.stub().resolves(true), + toOldJSONForBrowser: () => ({ id: user1.id, username: user1.username }) + }, + params: { id: libraryItem1.id, time: '100' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.removeBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(200)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should prevent user from removing bookmark for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { id: libraryItem2.id, time: '100' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.removeBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should validate time parameter is a number', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true + }, + params: { id: libraryItem1.id, time: 'not-a-number' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.removeBookmark(fakeReq, fakeRes) + + expect(fakeRes.status.calledWith(400)).to.be.true + expect(fakeRes.send.calledWith('Invalid time')).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) + }) + + describe('getItemListeningSessions - Authorization Check', () => { + let user1, user2 + let library1, library2 + let libraryItem1, libraryItem2 + + beforeEach(async () => { + // Create two users with different library access + user1 = await Database.userModel.create({ + username: 'user1', + pash: 'hashed_password_1', + type: 'user', + isActive: true, + librariesAccessible: null // Access to all libraries + }) + + user2 = await Database.userModel.create({ + username: 'user2', + pash: 'hashed_password_2', + type: 'user', + isActive: true, + librariesAccessible: [] // Will be set to specific library + }) + + // Create two libraries + library1 = await Database.libraryModel.create({ name: 'Library 1', mediaType: 'book' }) + library2 = await Database.libraryModel.create({ name: 'Library 2', mediaType: 'book' }) + + // User2 only has access to library1 + user2.librariesAccessible = [library1.id] + await user2.save() + + const libraryFolder1 = await Database.libraryFolderModel.create({ path: '/test1', libraryId: library1.id }) + const libraryFolder2 = await Database.libraryFolderModel.create({ path: '/test2', libraryId: library2.id }) + + const book1 = await Database.bookModel.create({ title: 'Book 1', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const book2 = await Database.bookModel.create({ title: 'Book 2', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + + libraryItem1 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book1.id, + mediaType: 'book', + libraryId: library1.id, + libraryFolderId: libraryFolder1.id + }) + + libraryItem2 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book2.id, + mediaType: 'book', + libraryId: library2.id, + libraryFolderId: libraryFolder2.id + }) + }) + + it('should allow user to view listening sessions for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + // Create mock context with getUserItemListeningSessionsHelper + const mockContext = { + getUserItemListeningSessionsHelper: sinon.stub().resolves([ + { id: 'session1', timeListening: 300, startedAt: Date.now() } + ]) + } + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true + }, + params: { libraryItemId: libraryItem1.id }, + query: {} + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + sinon.stub(Database.podcastEpisodeModel, 'findByPk').resolves(null) + + await MeController.getItemListeningSessions.bind(mockContext)(fakeReq, fakeRes) + + expect(fakeRes.json.calledOnce).to.be.true + expect(fakeRes.sendStatus.called).to.be.false + + // Verify the payload structure + const payload = fakeRes.json.firstCall.args[0] + expect(payload).to.have.property('total') + expect(payload).to.have.property('sessions') + + Database.libraryItemModel.getExpandedById.restore() + Database.podcastEpisodeModel.findByPk.restore() + }) + + it('should prevent user from viewing listening sessions for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { libraryItemId: libraryItem2.id }, + query: {} + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + sinon.stub(Database.podcastEpisodeModel, 'findByPk').resolves(null) + + await MeController.getItemListeningSessions.bind(apiRouter)(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + expect(fakeRes.json.called).to.be.false + + Database.libraryItemModel.getExpandedById.restore() + Database.podcastEpisodeModel.findByPk.restore() + }) + + it('should return 404 for non-existent library item', async () => { + const fakeReq = { + user: user1, + params: { libraryItemId: 'non-existent-id' }, + query: {} + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(null) + + await MeController.getItemListeningSessions.bind(apiRouter)(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) +}) From e5af2f336bc2b4571e593eec9a6379e65659cec3 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Sun, 15 Feb 2026 22:06:20 -0500 Subject: [PATCH 158/285] Move file to correct folder... --- test/server/{ => controllers}/MeController.test.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/server/{ => controllers}/MeController.test.js (100%) diff --git a/test/server/MeController.test.js b/test/server/controllers/MeController.test.js similarity index 100% rename from test/server/MeController.test.js rename to test/server/controllers/MeController.test.js From 75eed9d09a2ff2255bf9a06667d3c1656765fa0a Mon Sep 17 00:00:00 2001 From: pavel-miniutka Date: Thu, 19 Feb 2026 10:36:28 +0300 Subject: [PATCH 159/285] Add Belarusian language option & Belarus podcast region --- client/plugins/i18n.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 83e748f25..1ab0379c7 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -6,6 +6,7 @@ const defaultCode = 'en-us' const languageCodeMap = { ar: { label: 'عربي', dateFnsLocale: 'ar' }, + be: { label: 'Беларуская', dateFnsLocale: 'be' }, bg: { label: 'Български', dateFnsLocale: 'bg' }, bn: { label: 'বাংলা', dateFnsLocale: 'bn' }, ca: { label: 'Català', dateFnsLocale: 'ca' }, @@ -48,6 +49,7 @@ const podcastSearchRegionMap = { au: { label: 'Australia' }, br: { label: 'Brasil' }, be: { label: 'België / Belgique / Belgien' }, + by: { label: 'Беларусь' }, cz: { label: 'Česko' }, dk: { label: 'Danmark' }, de: { label: 'Deutschland' }, From 05d9ab81f98fffd8becbfca41aca22bf0e05dcf3 Mon Sep 17 00:00:00 2001 From: Kevin Gatera Date: Thu, 19 Feb 2026 20:11:24 -0500 Subject: [PATCH 160/285] Improve API cache invalidation for high-churn models --- server/managers/ApiCacheManager.js | 41 +++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index 2d8eece85..8a67e4898 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -5,6 +5,9 @@ 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 } + highChurnModels = new Set(['session', 'mediaProgress', 'playbackSession', 'device']) + modelsInvalidatingPersonalized = new Set(['mediaProgress']) + modelsInvalidatingMe = new Set(['session', 'mediaProgress', 'playbackSession', 'device']) constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) { this.cache = cache @@ -16,8 +19,44 @@ class ApiCacheManager { hooks.forEach((hook) => database.sequelize.addHook(hook, (model) => this.clear(model, hook))) } + getModelName(model) { + if (typeof model?.name === 'string') return model.name + if (typeof model?.model?.name === 'string') return model.model.name + if (typeof model?.constructor?.name === 'string' && model.constructor.name !== 'Object') return model.constructor.name + return 'unknown' + } + + clearByUrlPattern(urlPattern) { + let removed = 0 + for (const key of this.cache.keys()) { + try { + const parsed = JSON.parse(key) + if (typeof parsed?.url === 'string' && urlPattern.test(parsed.url)) { + if (this.cache.delete(key)) removed++ + } + } catch { + if (this.cache.delete(key)) removed++ + } + } + return removed + } + + clearUserProgressSlices(modelName, hook) { + const removedPersonalized = this.modelsInvalidatingPersonalized.has(modelName) ? this.clearByUrlPattern(/^\/libraries\/[^/]+\/personalized/) : 0 + const removedMe = this.modelsInvalidatingMe.has(modelName) ? this.clearByUrlPattern(/^\/me(\/|\?|$)/) : 0 + Logger.debug( + `[ApiCacheManager] ${modelName}.${hook}: cleared user-progress cache slices (personalized=${removedPersonalized}, me=${removedMe})` + ) + } + clear(model, hook) { - Logger.debug(`[ApiCacheManager] ${model.constructor.name}.${hook}: Clearing cache`) + const modelName = this.getModelName(model) + if (this.highChurnModels.has(modelName)) { + this.clearUserProgressSlices(modelName, hook) + return + } + + Logger.debug(`[ApiCacheManager] ${modelName}.${hook}: Clearing cache`) this.cache.clear() } From d2915e689fb6644fea7af3b216d1cfe7d8c24f0f Mon Sep 17 00:00:00 2001 From: Kevin Gatera Date: Thu, 19 Feb 2026 20:11:38 -0500 Subject: [PATCH 161/285] Speed up personalized shelves and reduce search payload size --- server/models/LibraryItem.js | 202 +++++++++++------- .../utils/queries/libraryItemsBookFilters.js | 113 +++++++--- .../queries/libraryItemsPodcastFilters.js | 4 +- 3 files changed, 211 insertions(+), 108 deletions(-) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 16a521615..b33fa585c 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -340,6 +340,15 @@ class LibraryItem extends Model { const shelves = [] + const timed = async (loader) => { + const start = Date.now() + const payload = await loader() + return { + payload, + elapsedSeconds: ((Date.now() - start) / 1000).toFixed(2) + } + } + // "Continue Listening" shelf const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, false) if (itemsInProgressPayload.items.length) { @@ -371,11 +380,18 @@ class LibraryItem extends Model { } Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) - let start = Date.now() if (library.isBook) { - start = Date.now() + const [continueSeriesResult, mostRecentResult, seriesMostRecentResult, discoverResult, mediaFinishedResult, newestAuthorsResult] = await Promise.all([ + timed(() => libraryFilters.getLibraryItemsContinueSeries(library, user, include, limit)), + timed(() => libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit)), + timed(() => libraryFilters.getSeriesMostRecentlyAdded(library, user, include, 5)), + timed(() => libraryFilters.getLibraryItemsToDiscover(library, user, include, limit)), + timed(() => libraryFilters.getMediaFinished(library, user, include, limit)), + timed(() => libraryFilters.getNewestAuthors(library, user, limit)) + ]) + + const continueSeriesPayload = continueSeriesResult.payload // "Continue Series" shelf - const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, user, include, limit) if (continueSeriesPayload.libraryItems.length) { shelves.push({ id: 'continue-series', @@ -386,42 +402,24 @@ class LibraryItem extends Model { total: continueSeriesPayload.count }) } - Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } else if (library.isPodcast) { - // "Newest Episodes" shelf - const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit) - if (newestEpisodesPayload.libraryItems.length) { + Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${continueSeriesResult.elapsedSeconds}s`) + + const mostRecentPayload = mostRecentResult.payload + // "Recently Added" shelf + if (mostRecentPayload.libraryItems.length) { shelves.push({ - id: 'newest-episodes', - label: 'Newest Episodes', - labelStringKey: 'LabelNewestEpisodes', - type: 'episode', - entities: newestEpisodesPayload.libraryItems, - total: newestEpisodesPayload.count + id: 'recently-added', + label: 'Recently Added', + labelStringKey: 'LabelRecentlyAdded', + type: library.mediaType, + entities: mostRecentPayload.libraryItems, + total: mostRecentPayload.count }) } - Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } + Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${mostRecentResult.elapsedSeconds}s`) - start = Date.now() - // "Recently Added" shelf - const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit) - if (mostRecentPayload.libraryItems.length) { - shelves.push({ - id: 'recently-added', - label: 'Recently Added', - labelStringKey: 'LabelRecentlyAdded', - type: library.mediaType, - entities: mostRecentPayload.libraryItems, - total: mostRecentPayload.count - }) - } - Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - if (library.isBook) { - start = Date.now() + const seriesMostRecentPayload = seriesMostRecentResult.payload // "Recent Series" shelf - const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, user, include, 5) if (seriesMostRecentPayload.series.length) { shelves.push({ id: 'recent-series', @@ -432,11 +430,10 @@ class LibraryItem extends Model { total: seriesMostRecentPayload.count }) } - Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${seriesMostRecentResult.elapsedSeconds}s`) - start = Date.now() + const discoverLibraryItemsPayload = discoverResult.payload // "Discover" shelf - const discoverLibraryItemsPayload = await libraryFilters.getLibraryItemsToDiscover(library, user, include, limit) if (discoverLibraryItemsPayload.libraryItems.length) { shelves.push({ id: 'discover', @@ -447,45 +444,41 @@ class LibraryItem extends Model { total: discoverLibraryItemsPayload.count }) } - Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } + Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${discoverResult.elapsedSeconds}s`) - start = Date.now() - // "Listen Again" shelf - const mediaFinishedPayload = await libraryFilters.getMediaFinished(library, user, include, limit) - if (mediaFinishedPayload.items.length) { - const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) - const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast') + const mediaFinishedPayload = mediaFinishedResult.payload + // "Listen Again" shelf + if (mediaFinishedPayload.items.length) { + const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) + const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast') - if (audioItemsInProgress.length) { - shelves.push({ - id: 'listen-again', - label: 'Listen Again', - labelStringKey: 'LabelListenAgain', - type: library.isPodcast ? 'episode' : 'book', - entities: audioItemsInProgress, - total: mediaFinishedPayload.count - }) + if (audioItemsInProgress.length) { + shelves.push({ + id: 'listen-again', + label: 'Listen Again', + labelStringKey: 'LabelListenAgain', + type: library.isPodcast ? 'episode' : 'book', + entities: audioItemsInProgress, + total: mediaFinishedPayload.count + }) + } + + if (ebookOnlyItemsInProgress.length) { + // "Read Again" shelf + shelves.push({ + id: 'read-again', + label: 'Read Again', + labelStringKey: 'LabelReadAgain', + type: 'book', + entities: ebookOnlyItemsInProgress, + total: mediaFinishedPayload.count + }) + } } + Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${mediaFinishedResult.elapsedSeconds}s`) - // "Read Again" shelf - if (ebookOnlyItemsInProgress.length) { - shelves.push({ - id: 'read-again', - label: 'Read Again', - labelStringKey: 'LabelReadAgain', - type: 'book', - entities: ebookOnlyItemsInProgress, - total: mediaFinishedPayload.count - }) - } - } - Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - if (library.isBook) { - start = Date.now() + const newestAuthorsPayload = newestAuthorsResult.payload // "Newest Authors" shelf - const newestAuthorsPayload = await libraryFilters.getNewestAuthors(library, user, limit) if (newestAuthorsPayload.authors.length) { shelves.push({ id: 'newest-authors', @@ -496,7 +489,72 @@ class LibraryItem extends Model { total: newestAuthorsPayload.count }) } - Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${newestAuthorsResult.elapsedSeconds}s`) + } else if (library.isPodcast) { + const [newestEpisodesResult, mostRecentResult, mediaFinishedResult] = await Promise.all([ + timed(() => libraryFilters.getNewestPodcastEpisodes(library, user, limit)), + timed(() => libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit)), + timed(() => libraryFilters.getMediaFinished(library, user, include, limit)) + ]) + + const newestEpisodesPayload = newestEpisodesResult.payload + // "Newest Episodes" shelf + if (newestEpisodesPayload.libraryItems.length) { + shelves.push({ + id: 'newest-episodes', + label: 'Newest Episodes', + labelStringKey: 'LabelNewestEpisodes', + type: 'episode', + entities: newestEpisodesPayload.libraryItems, + total: newestEpisodesPayload.count + }) + } + Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${newestEpisodesResult.elapsedSeconds}s`) + + const mostRecentPayload = mostRecentResult.payload + // "Recently Added" shelf + if (mostRecentPayload.libraryItems.length) { + shelves.push({ + id: 'recently-added', + label: 'Recently Added', + labelStringKey: 'LabelRecentlyAdded', + type: library.mediaType, + entities: mostRecentPayload.libraryItems, + total: mostRecentPayload.count + }) + } + Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${mostRecentResult.elapsedSeconds}s`) + + const mediaFinishedPayload = mediaFinishedResult.payload + // "Listen Again" shelf + if (mediaFinishedPayload.items.length) { + const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) + const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast') + + if (audioItemsInProgress.length) { + shelves.push({ + id: 'listen-again', + label: 'Listen Again', + labelStringKey: 'LabelListenAgain', + type: 'episode', + entities: audioItemsInProgress, + total: mediaFinishedPayload.count + }) + } + + if (ebookOnlyItemsInProgress.length) { + // "Read Again" shelf + shelves.push({ + id: 'read-again', + label: 'Read Again', + labelStringKey: 'LabelReadAgain', + type: 'book', + entities: ebookOnlyItemsInProgress, + total: mediaFinishedPayload.count + }) + } + } + Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${mediaFinishedResult.elapsedSeconds}s`) } Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 7ae1dc866..407b18a94 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -888,28 +888,80 @@ module.exports = { }) } - // Step 2: Get books not started and not in a series OR is the first book of a series not started (ordered randomly) - const { rows: books, count } = await Database.bookModel.findAndCountAll({ - where: [ - { - '$mediaProgresses.isFinished$': { - [Sequelize.Op.or]: [null, 0] - }, - '$mediaProgresses.currentTime$': { - [Sequelize.Op.or]: [null, 0] - }, - [Sequelize.Op.or]: [ - Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0), - { - id: { - [Sequelize.Op.in]: booksFromSeriesToInclude - } + const discoverWhere = [ + { + [Sequelize.Op.and]: [ + Sequelize.where( + Sequelize.literal( + `(SELECT COUNT(*) FROM mediaProgresses mp WHERE mp.mediaItemId = book.id AND mp.userId = :userId AND (mp.isFinished = 1 OR mp.currentTime > 0))` + ), + 0 + ) + ], + [Sequelize.Op.or]: [ + Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0), + { + id: { + [Sequelize.Op.in]: booksFromSeriesToInclude } - ] - }, - ...userPermissionBookWhere.bookWhere - ], - replacements: userPermissionBookWhere.replacements, + } + ] + }, + ...userPermissionBookWhere.bookWhere + ] + + const baseDiscoverInclude = [ + { + model: Database.libraryItemModel, + where: { + libraryId + } + } + ] + + // Step 2a: Count with lightweight includes only + const count = await Database.bookModel.count({ + where: discoverWhere, + replacements: { + userId: user.id, + ...userPermissionBookWhere.replacements + }, + include: baseDiscoverInclude, + distinct: true, + col: 'id', + subQuery: false + }) + + // Step 2b: Select random IDs with lightweight includes only + const randomSelection = await Database.bookModel.findAll({ + attributes: ['id'], + where: discoverWhere, + replacements: { + userId: user.id, + ...userPermissionBookWhere.replacements + }, + include: baseDiscoverInclude, + subQuery: false, + distinct: true, + limit, + order: Database.sequelize.random() + }) + + const selectedIds = randomSelection.map((b) => b.id).filter(Boolean) + if (!selectedIds.length) { + return { + libraryItems: [], + count + } + } + + // Step 2c: Hydrate selected IDs with full metadata for API response + const books = await Database.bookModel.findAll({ + where: { + id: { + [Sequelize.Op.in]: selectedIds + } + }, include: [ { model: Database.libraryItemModel, @@ -918,13 +970,6 @@ module.exports = { }, include: libraryItemIncludes }, - { - model: Database.mediaProgressModel, - where: { - userId: user.id - }, - required: false - }, { model: Database.bookAuthorModel, attributes: ['authorId'], @@ -942,14 +987,14 @@ module.exports = { separate: true } ], - subQuery: false, - distinct: true, - limit, - order: Database.sequelize.random() + subQuery: false }) + const booksById = new Map(books.map((b) => [b.id, b])) + const orderedBooks = selectedIds.map((id) => booksById.get(id)).filter(Boolean) + // Step 3: Map books to library items - const libraryItems = books.map((bookExpanded) => { + const libraryItems = orderedBooks.map((bookExpanded) => { const libraryItem = bookExpanded.libraryItem const book = bookExpanded delete book.libraryItem @@ -1122,7 +1167,7 @@ module.exports = { libraryItem.media = book itemMatches.push({ - libraryItem: libraryItem.toOldJSONExpanded() + libraryItem: libraryItem.toOldJSONMinified() }) } diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 8bb5dc110..05a7b083f 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -410,7 +410,7 @@ module.exports = { libraryItem.media = podcast libraryItem.media.podcastEpisodes = [] itemMatches.push({ - libraryItem: libraryItem.toOldJSONExpanded() + libraryItem: libraryItem.toOldJSONMinified() }) } @@ -444,7 +444,7 @@ module.exports = { libraryItem.media = episode.podcast libraryItem.media.podcastEpisodes = [] const oldPodcastEpisodeJson = episode.toOldJSONExpanded(libraryItem.id) - const libraryItemJson = libraryItem.toOldJSONExpanded() + const libraryItemJson = libraryItem.toOldJSONMinified() libraryItemJson.recentEpisode = oldPodcastEpisodeJson episodeMatches.push({ libraryItem: libraryItemJson From f1a2e56054ab2c9e9ee9c518ee84b4b607ca5248 Mon Sep 17 00:00:00 2001 From: Kevin Gatera Date: Thu, 19 Feb 2026 20:11:49 -0500 Subject: [PATCH 162/285] Add database indexes for discover query performance --- .../v2.32.2-add-discover-query-indexes.js | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 server/migrations/v2.32.2-add-discover-query-indexes.js diff --git a/server/migrations/v2.32.2-add-discover-query-indexes.js b/server/migrations/v2.32.2-add-discover-query-indexes.js new file mode 100644 index 000000000..26b12383d --- /dev/null +++ b/server/migrations/v2.32.2-add-discover-query-indexes.js @@ -0,0 +1,74 @@ +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface + * @property {import('../Logger')} logger + * + * @typedef MigrationOptions + * @property {MigrationContext} context + */ + +const migrationVersion = '2.32.2' +const migrationName = `${migrationVersion}-add-discover-query-indexes` +const loggerPrefix = `[${migrationVersion} migration]` + +const indexes = [ + { + table: 'mediaProgresses', + name: 'media_progress_user_item_finished_time', + fields: ['userId', 'mediaItemId', 'isFinished', 'currentTime'] + }, + { + table: 'bookSeries', + name: 'book_series_series_book', + fields: ['seriesId', 'bookId'] + } +] + +async function up({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) + + for (const index of indexes) { + await addIndexIfMissing(queryInterface, logger, index) + } + + logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) +} + +async function down({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) + + for (const index of indexes) { + await removeIndexIfExists(queryInterface, logger, index) + } + + logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) +} + +async function addIndexIfMissing(queryInterface, logger, index) { + const existing = await queryInterface.showIndex(index.table) + if (existing.some((i) => i.name === index.name)) { + logger.info(`${loggerPrefix} index ${index.name} already exists on ${index.table}`) + return + } + + logger.info(`${loggerPrefix} adding index ${index.name} on ${index.table}(${index.fields.join(', ')})`) + await queryInterface.addIndex(index.table, { + name: index.name, + fields: index.fields + }) + logger.info(`${loggerPrefix} added index ${index.name}`) +} + +async function removeIndexIfExists(queryInterface, logger, index) { + const existing = await queryInterface.showIndex(index.table) + if (!existing.some((i) => i.name === index.name)) { + logger.info(`${loggerPrefix} index ${index.name} does not exist on ${index.table}`) + return + } + + logger.info(`${loggerPrefix} removing index ${index.name}`) + await queryInterface.removeIndex(index.table, index.name) + logger.info(`${loggerPrefix} removed index ${index.name}`) +} + +module.exports = { up, down } From ee6016f70e33cd4f7782c1966aca906aaa4b47ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Bel=C3=A1k?= Date: Sat, 21 Feb 2026 09:11:50 +0100 Subject: [PATCH 163/285] Add Slovak (sk) language to the language selector The Slovak translation file (client/strings/sk.json) already exists with a complete translation but was missing from the languageCodeMap in i18n.js, making it inaccessible from the language dropdown. Also adds Slovakia to the podcast search region map. Co-Authored-By: Claude Opus 4.6 --- client/plugins/i18n.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 83e748f25..5a672ba06 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -28,6 +28,7 @@ const languageCodeMap = { pl: { label: 'Polski', dateFnsLocale: 'pl' }, 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, ru: { label: 'Русский', dateFnsLocale: 'ru' }, + sk: { label: 'Slovenčina', dateFnsLocale: 'sk' }, sl: { label: 'Slovenščina', dateFnsLocale: 'sl' }, sv: { label: 'Svenska', dateFnsLocale: 'sv' }, tr: { label: 'Türkçe', dateFnsLocale: 'tr' }, @@ -67,6 +68,7 @@ const podcastSearchRegionMap = { pt: { label: 'Portugal' }, ru: { label: 'Россия' }, ch: { label: 'Schweiz / Suisse / Svizzera' }, + sk: { label: 'Slovensko' }, se: { label: 'Sverige' }, vn: { label: 'Việt Nam' }, ua: { label: 'Україна' }, From 6e0da3bf7a7b81893dbcca542519cf594d7d3232 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 21 Feb 2026 16:00:38 -0600 Subject: [PATCH 164/285] Fix updating author name merging with same name authors in a different library #4628 --- server/controllers/AuthorController.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 50eeda31a..82ed3e50a 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -113,7 +113,7 @@ class AuthorController { payload.lastFirst = Database.authorModel.getLastFirst(payload.name) } - // Check if author name matches another author and merge the authors + // Check if author name matches another author in the same library and merge the authors let existingAuthor = null if (authorNameUpdate) { existingAuthor = await Database.authorModel.findOne({ @@ -121,7 +121,8 @@ class AuthorController { id: { [sequelize.Op.not]: req.author.id }, - name: payload.name + name: payload.name, + libraryId: req.author.libraryId } }) } From c0319ebbac6c5ba6f0473453b7ba0528036e2316 Mon Sep 17 00:00:00 2001 From: Kevin Gatera Date: Mon, 23 Feb 2026 19:06:33 -0500 Subject: [PATCH 165/285] Adjust discover/search changes for API compatibility --- .../utils/queries/libraryItemsBookFilters.js | 33 +++++++++---------- .../queries/libraryItemsPodcastFilters.js | 4 +-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 407b18a94..fbe0c4f0d 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -890,14 +890,12 @@ module.exports = { const discoverWhere = [ { - [Sequelize.Op.and]: [ - Sequelize.where( - Sequelize.literal( - `(SELECT COUNT(*) FROM mediaProgresses mp WHERE mp.mediaItemId = book.id AND mp.userId = :userId AND (mp.isFinished = 1 OR mp.currentTime > 0))` - ), - 0 - ) - ], + '$mediaProgresses.isFinished$': { + [Sequelize.Op.or]: [null, 0] + }, + '$mediaProgresses.currentTime$': { + [Sequelize.Op.or]: [null, 0] + }, [Sequelize.Op.or]: [ Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0), { @@ -916,16 +914,20 @@ module.exports = { where: { libraryId } + }, + { + model: Database.mediaProgressModel, + where: { + userId: user.id + }, + required: false } ] // Step 2a: Count with lightweight includes only const count = await Database.bookModel.count({ where: discoverWhere, - replacements: { - userId: user.id, - ...userPermissionBookWhere.replacements - }, + replacements: userPermissionBookWhere.replacements, include: baseDiscoverInclude, distinct: true, col: 'id', @@ -936,10 +938,7 @@ module.exports = { const randomSelection = await Database.bookModel.findAll({ attributes: ['id'], where: discoverWhere, - replacements: { - userId: user.id, - ...userPermissionBookWhere.replacements - }, + replacements: userPermissionBookWhere.replacements, include: baseDiscoverInclude, subQuery: false, distinct: true, @@ -1167,7 +1166,7 @@ module.exports = { libraryItem.media = book itemMatches.push({ - libraryItem: libraryItem.toOldJSONMinified() + libraryItem: libraryItem.toOldJSONExpanded() }) } diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 05a7b083f..8bb5dc110 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -410,7 +410,7 @@ module.exports = { libraryItem.media = podcast libraryItem.media.podcastEpisodes = [] itemMatches.push({ - libraryItem: libraryItem.toOldJSONMinified() + libraryItem: libraryItem.toOldJSONExpanded() }) } @@ -444,7 +444,7 @@ module.exports = { libraryItem.media = episode.podcast libraryItem.media.podcastEpisodes = [] const oldPodcastEpisodeJson = episode.toOldJSONExpanded(libraryItem.id) - const libraryItemJson = libraryItem.toOldJSONMinified() + const libraryItemJson = libraryItem.toOldJSONExpanded() libraryItemJson.recentEpisode = oldPodcastEpisodeJson episodeMatches.push({ libraryItem: libraryItemJson From a9e12657f572d4ac1ef9b27d09ae1137f6f9f7c0 Mon Sep 17 00:00:00 2001 From: meek2100 Date: Thu, 26 Feb 2026 14:29:28 -0800 Subject: [PATCH 166/285] Add autocomplete attributes to login and setup fields for password manager support --- client/components/ui/TextInput.vue | 5 +++-- client/components/ui/TextInputWithLabel.vue | 5 +++-- client/pages/login.vue | 10 +++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index dd0eaa738..cd3bc6d2d 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -1,6 +1,6 @@ @@ -26,7 +26,8 @@ export default { disabled: Boolean, inputClass: String, showCopy: Boolean, - trimWhitespace: Boolean + trimWhitespace: Boolean, + autocomplete: String }, data() { return {} diff --git a/client/pages/login.vue b/client/pages/login.vue index 8e5cde098..cec150e71 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -17,9 +17,9 @@

Create Root User

- - - + + +

Directory Paths

@@ -51,10 +51,10 @@ - + - +
{{ processing ? 'Checking...' : $strings.ButtonSubmit }}
From 6d3404272c2308464bd00aa9668afdc529ac296e Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 28 Feb 2026 11:32:28 -0600 Subject: [PATCH 167/285] Fix Match click to use current label string --- client/components/modals/item/tabs/Match.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 4b92f6cd8..a1fc41091 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -78,7 +78,7 @@
@@ -87,7 +87,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName }}

@@ -96,7 +96,7 @@
@@ -105,7 +105,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.publisher }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.publisher }}

@@ -114,7 +114,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear }}

From e6d49a2d53c90350a31cd418302a5c22d279f9b5 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 4 Mar 2026 16:50:36 -0600 Subject: [PATCH 168/285] Fix home page check current user for hide from continue listening --- client/components/app/BookShelfCategorized.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 4bf8cfbbf..a8e8ae469 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -300,6 +300,8 @@ export default { }) }, userUpdated(user) { + if (user.id !== this.$store.state.user.user.id) return + if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) { this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening) } From 2f2d026b06730aa2233f9aa4a98ccd40ef78a126 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 8 Mar 2026 17:25:52 -0500 Subject: [PATCH 169/285] Auto format --- test/server/controllers/MeController.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/server/controllers/MeController.test.js b/test/server/controllers/MeController.test.js index 2f6b502db..3cc5496d9 100644 --- a/test/server/controllers/MeController.test.js +++ b/test/server/controllers/MeController.test.js @@ -547,9 +547,7 @@ describe('MeController - IDOR Security Tests', () => { // Create mock context with getUserItemListeningSessionsHelper const mockContext = { - getUserItemListeningSessionsHelper: sinon.stub().resolves([ - { id: 'session1', timeListening: 300, startedAt: Date.now() } - ]) + getUserItemListeningSessionsHelper: sinon.stub().resolves([{ id: 'session1', timeListening: 300, startedAt: Date.now() }]) } const fakeReq = { From d0ba455ed64d372e2fc275c9c90ed3af92f3be52 Mon Sep 17 00:00:00 2001 From: Luis Landeiro Date: Tue, 3 Feb 2026 20:12:10 +0100 Subject: [PATCH 170/285] Translated using Weblate (Portuguese (Brazil)) 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/pt_BR/ --- client/strings/pt-br.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index c6c9781cf..d2189698c 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -436,8 +436,8 @@ "LabelLibraryFilterSublistEmpty": "Sem {0}", "LabelLibraryItem": "Item da Biblioteca", "LabelLibraryName": "Nome da Biblioteca", - "LabelLibrarySortByProgress": "Última Atualização", - "LabelLibrarySortByProgressFinished": "Concluído", + "LabelLibrarySortByProgress": "Progresso: Ultima Atualização", + "LabelLibrarySortByProgressFinished": "Progresso: Terminado", "LabelLibrarySortByProgressStarted": "Progresso: Iniciado", "LabelLimit": "Limite", "LabelLineSpacing": "Espaçamento entre linhas", From cd8640f00ed2da178199b68ee17be7fb46f95188 Mon Sep 17 00:00:00 2001 From: Yusuke Sakai Date: Thu, 5 Feb 2026 14:59:07 +0100 Subject: [PATCH 171/285] Translated using Weblate (Japanese) Currently translated at 23.1% (269 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/ --- client/strings/ja.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/strings/ja.json b/client/strings/ja.json index 28f6de6a6..0621614f4 100644 --- a/client/strings/ja.json +++ b/client/strings/ja.json @@ -214,7 +214,9 @@ "LabelEnd": "終了", "LabelEndOfChapter": "チャプターの最後", "LabelEpisode": "エピソード", - "LabelExplicit": "", + "LabelEpisodes": "エピソード", + "LabelEpisodic": "エピソード", + "LabelExplicit": "露骨な表現", "LabelFeedURL": "Feed URL", "LabelFile": "ファイル", "LabelFileBirthtime": "ファイル作成日時", From 31120ad1110a9a610ef0f45372844fef68c5a685 Mon Sep 17 00:00:00 2001 From: ugyes Date: Sat, 14 Feb 2026 17:45:24 +0100 Subject: [PATCH 172/285] 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/ --- client/strings/hu.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/hu.json b/client/strings/hu.json index 1c177a09c..90f58343a 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -205,7 +205,7 @@ "HeaderSleepTimer": "Alvásidőzítő", "HeaderStatsLargestItems": "Legnagyobb elemek", "HeaderStatsLongestItems": "Leghosszabb elemek (órában)", - "HeaderStatsMinutesListeningChart": "Hallgatási grafikon percekben (az elmúlt 7 napból)", + "HeaderStatsMinutesListeningChart": "Hallgatási grafikon percben (az elmúlt 7 napból)", "HeaderStatsRecentSessions": "Legutóbbi munkamenetek", "HeaderStatsTop10Authors": "Top 10 szerző", "HeaderStatsTop5Genres": "Top 5 műfaj", @@ -643,8 +643,8 @@ "LabelStatsAuthors": "Szerző", "LabelStatsBestDay": "Legjobb nap", "LabelStatsDailyAverage": "Napi átlag", - "LabelStatsDays": "Napok", - "LabelStatsDaysListened": "Hallgatással töltött napok", + "LabelStatsDays": "Nap", + "LabelStatsDaysListened": "Hallgatással töltött nap", "LabelStatsHours": "Órák", "LabelStatsInARow": "egymás után", "LabelStatsItemsFinished": "Befejezett elem", From cf4b9e938d1be0085484b780b25fcb6ead25d0f8 Mon Sep 17 00:00:00 2001 From: Maxklos Date: Fri, 13 Feb 2026 12:55:15 +0100 Subject: [PATCH 173/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index cf36b07fd..754f9a3a4 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -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", @@ -710,7 +710,7 @@ "LabelUploaderDragAndDropFilesOnly": "Dateien per Drag & Drop hierher ziehen", "LabelUploaderDropFiles": "Dateien löschen", "LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie", - "LabelUseAdvancedOptions": "Nutze Erweiterte Optionen", + "LabelUseAdvancedOptions": "Erweiterte Optionen nutzen", "LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUseZeroForUnlimited": "0 für unbegrenzt", @@ -737,7 +737,7 @@ "MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen", "MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von Apprise API laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann.
Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter http://192.168.1.1:8337 läuft, würdest du http://192.168.1.1:8337/notify eingeben.", "MessageAsinCheck": "Stelle sicher, dass die ASIN aus der richtigen Audible Region verwendet wird, nicht Amazon.", - "MessageAuthenticationLegacyTokenWarning": "Alte API tokens werden in Zukunft entfernt. Benutze stattdessen API Keys.", + "MessageAuthenticationLegacyTokenWarning": "Alte API-Token werden in Zukunft entfernt. Benutze stattdessen API Keys.", "MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.", "MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.", "MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in /metadata/items & /metadata/authors gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.", @@ -1103,11 +1103,11 @@ "ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden", "ToastPodcastCreateSuccess": "Podcast erstellt", "ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert", - "ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds", + "ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast-Feeds", "ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden", "ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed", "ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet", - "ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters", + "ToastProviderCreatedFailed": "Fehler beim Hinzufügen des Anbieters", "ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt", "ToastProviderNameAndUrlRequired": "Name und URL notwendig", "ToastProviderRemoveSuccess": "Anbieter entfernt", From 40f42b2ab6c870e1658e1d72145d444bc5e05278 Mon Sep 17 00:00:00 2001 From: B0rax Date: Fri, 13 Feb 2026 12:58:05 +0100 Subject: [PATCH 174/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 754f9a3a4..276348656 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -710,7 +710,7 @@ "LabelUploaderDragAndDropFilesOnly": "Dateien per Drag & Drop hierher ziehen", "LabelUploaderDropFiles": "Dateien löschen", "LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie", - "LabelUseAdvancedOptions": "Erweiterte Optionen nutzen", + "LabelUseAdvancedOptions": "Erweiterte Optionen verwenden", "LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUseZeroForUnlimited": "0 für unbegrenzt", From a35ba05600be1542f5d3e755c6c31c7486601a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Dobra=C4=8Da?= Date: Mon, 16 Feb 2026 04:12:03 +0100 Subject: [PATCH 175/285] 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/ --- client/strings/hr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/hr.json b/client/strings/hr.json index eccd12563..65150409f 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Br {0}", "LabelLibraryItem": "Stavka knjižnice", "LabelLibraryName": "Ime knjižnice", - "LabelLibrarySortByProgress": "Napredak: zadnje ažurirano", - "LabelLibrarySortByProgressFinished": "Napredak: završeno", - "LabelLibrarySortByProgressStarted": "Napredak: započeto", + "LabelLibrarySortByProgress": "Napredak: Zadnje ažuriranje", + "LabelLibrarySortByProgressFinished": "Napredak: Završeno", + "LabelLibrarySortByProgressStarted": "Napredak: Započeto", "LabelLimit": "Ograničenje", "LabelLineSpacing": "Razmak između redaka", "LabelListenAgain": "Ponovno poslušaj", From 69d7c399b8a8110b29fdbaafef489bc44677577b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Bernst=C3=A5l?= Date: Sun, 15 Feb 2026 13:54:38 +0100 Subject: [PATCH 176/285] Translated using Weblate (Swedish) 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/sv/ --- client/strings/sv.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index e1c08efa5..fa0267e82 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -1,6 +1,6 @@ { "ButtonAdd": "Lägg till", - "ButtonAddApiKey": "Addera API-nyckel", + "ButtonAddApiKey": "Lägg till API-nyckel", "ButtonAddChapters": "Lägg till kapitel", "ButtonAddDevice": "Lägg till enhet", "ButtonAddLibrary": "Lägg till bibliotek", @@ -48,7 +48,7 @@ "ButtonLogout": "Logga ut", "ButtonLookup": "Sök", "ButtonManageTracks": "Hantera spår", - "ButtonMapChapterTitles": "Karta kapitelrubriker", + "ButtonMapChapterTitles": "Mappa kapitelrubriker", "ButtonMatchAllAuthors": "Matcha alla författare", "ButtonMatchBooks": "Matcha böcker", "ButtonNevermind": "Glöm det", @@ -104,7 +104,7 @@ "ButtonStartM4BEncode": "Starta M4B-omkodning", "ButtonStartMetadataEmbed": "Infoga metadata", "ButtonStats": "Statistik", - "ButtonSubmit": "Spara", + "ButtonSubmit": "Skicka", "ButtonTest": "Testa", "ButtonUnlinkOpenId": "Koppla ifrån OpenID", "ButtonUpload": "Ladda upp", @@ -123,7 +123,7 @@ "HeaderAdvanced": "Avancerad", "HeaderApiKeys": "API-nyckel", "HeaderAppriseNotificationSettings": "Inställningar av meddelanden med Apprise", - "HeaderAudioTracks": "Ljudfiler", + "HeaderAudioTracks": "Ljudspår", "HeaderAudiobookTools": "Hantering av ljudboksfiler", "HeaderAuthentication": "Autentisering", "HeaderBackups": "Säkerhetskopior", @@ -202,7 +202,7 @@ "HeaderSettingsScanner": "Skanner", "HeaderSettingsSecurity": "Säkerhet", "HeaderSettingsWebClient": "Webklient", - "HeaderSleepTimer": "Timer för att sova", + "HeaderSleepTimer": "Insomningstimer", "HeaderStatsLargestItems": "Största objekten", "HeaderStatsLongestItems": "Längsta objekten (timmar)", "HeaderStatsMinutesListeningChart": "Minuters lyssning (senaste 7 dagarna)", @@ -231,10 +231,10 @@ "LabelActivity": "Aktivitet", "LabelAddToCollection": "Lägg till i en samling", "LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen", - "LabelAddToPlaylist": "Lägg till i en spellista", + "LabelAddToPlaylist": "Lägg till i spellista", "LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan", "LabelAddedAt": "Datum adderad", - "LabelAddedDate": "Adderad {0}", + "LabelAddedDate": "Tillagd {0}", "LabelAdminUsersOnly": "Endast administratörer", "LabelAll": "Alla", "LabelAllEpisodesDownloaded": "Alla avsnitt är nedladdade", @@ -363,14 +363,14 @@ "LabelExpiresAt": "Gäller till och med", "LabelExpiresInSeconds": "Upphör om (sekunder)", "LabelExpiresNever": "Aldrig", - "LabelExplicit": "Bestämd", + "LabelExplicit": "Vuxeninnehåll", "LabelExplicitChecked": "Explicit version (markerad)", "LabelExplicitUnchecked": "Ej Explicit version (ej markerad)", "LabelExportOPML": "Exportera OPML-information", "LabelFeedURL": "URL-adress för flödet", "LabelFetchingMetadata": "Hämtar metadata", "LabelFile": "Fil", - "LabelFileBirthtime": "Tidpunkt, adderad", + "LabelFileBirthtime": "Tidpunkt, tillagd", "LabelFileBornDate": "Skapad {0}", "LabelFileModified": "Tidpunkt, ändrad", "LabelFileModifiedDate": "Ändrad {0}", @@ -629,7 +629,7 @@ "LabelShowSeconds": "Visa i sekunder", "LabelShowSubtitles": "Visa underrubriker", "LabelSize": "Storlek", - "LabelSleepTimer": "Sovtimer", + "LabelSleepTimer": "Insomningstimer", "LabelSlug": "Kortnamn", "LabelSortAscending": "Stigande", "LabelSortDescending": "Fallande", From fb8ca043ad5d34086ef8ac2b431ee1664fd69861 Mon Sep 17 00:00:00 2001 From: Silviu Bajenaru Date: Wed, 18 Feb 2026 18:45:55 +0100 Subject: [PATCH 177/285] Translated using Weblate (Romanian) Currently translated at 55.1% (641 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/ --- client/strings/ro.json | 447 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 444 insertions(+), 3 deletions(-) diff --git a/client/strings/ro.json b/client/strings/ro.json index f6e8977ef..441ccc400 100644 --- a/client/strings/ro.json +++ b/client/strings/ro.json @@ -11,140 +11,581 @@ "ButtonApplyChapters": "Aplică Capitole", "ButtonAuthors": "Autori", "ButtonBack": "Înapoi", + "ButtonBatchEditPopulateFromExisting": "Populează din existente", + "ButtonBatchEditPopulateMapDetails": "Populează detaliile hărții", + "ButtonBrowseForFolder": "Caută un dosar", "ButtonCancel": "Anulează", + "ButtonCancelEncode": "Anulare codificare", + "ButtonChangeRootPassword": "Schimbare parolă de root", + "ButtonCheckAndDownloadNewEpisodes": "Verifică și descarcă episoade noi", + "ButtonChooseAFolder": "Alege un dosar", + "ButtonChooseFiles": "Alege fișiere", "ButtonClearFilter": "Șterge filtrul", "ButtonClose": "Închide", "ButtonCloseFeed": "Închide sursa", "ButtonCloseSession": "Închide Sesiunea Curentă", "ButtonCollections": "Colecții", + "ButtonConfigureScanner": "Configurare scaner", "ButtonCreate": "Creează", + "ButtonCreateBackup": "Creează backup", "ButtonDelete": "Șterge", + "ButtonDownloadQueue": "Coadă", + "ButtonEdit": "Editare", + "ButtonEditChapters": "Editare capitole", + "ButtonEditPodcast": "Editare podcast", + "ButtonEnable": "Activează", + "ButtonForceReScan": "Forțează rescanare", + "ButtonFullPath": "Calea completă", "ButtonHide": "Ascunde", "ButtonHome": "Acasă", - "ButtonIssues": "Erori", + "ButtonIssues": "Probleme", + "ButtonJumpBackward": "Sari înapoi", + "ButtonJumpForward": "Sari înainte", "ButtonLatest": "Noutăți", "ButtonLibrary": "Bibliotecă", + "ButtonLogout": "Deconectare", + "ButtonLookup": "Căutare", + "ButtonManageTracks": "Gestionează pista", + "ButtonMapChapterTitles": "Maparea titlurilor capitolelor", + "ButtonMatchAllAuthors": "Potriviește toți autorii", + "ButtonMatchBooks": "Potrivește Cărți", + "ButtonNevermind": "Anulează", + "ButtonNext": "Următorul", + "ButtonNextChapter": "Următorul Capitol", + "ButtonNextItemInQueue": "Următorul Articol în Coadă", "ButtonOk": "OK", "ButtonOpenFeed": "Vezi noutățile", + "ButtonOpenManager": "Deschide Managerul", "ButtonPause": "Pauză", "ButtonPlay": "Redă", + "ButtonPlayAll": "Redă tot", + "ButtonPlaying": "Redare", "ButtonPlaylists": "Liste", + "ButtonPrevious": "Anterior", + "ButtonPreviousChapter": "Capitolul Anterior", + "ButtonProbeAudioFile": "Analizare Fișier Audio", + "ButtonPurgeAllCache": "Golire Cache Completă", + "ButtonPurgeItemsCache": "Golire Cache Articole", + "ButtonQueueAddItem": "Adaugă la Coadă", + "ButtonQueueRemoveItem": "Sterge din Coadă", + "ButtonQuickEmbed": "Încorporare Rapidă", + "ButtonQuickEmbedMetadata": "Metadate pentru Încorporare Rapidă", + "ButtonQuickMatch": "Potrivire Rapidă", + "ButtonReScan": "Rescanare", "ButtonRead": "Citește", - "ButtonReadLess": "Mai puțin", + "ButtonReadLess": "Citește Mai Puțin", "ButtonReadMore": "Afișează mai mult", + "ButtonRefresh": "Reîmprospătare", "ButtonRemove": "Elimină", + "ButtonRemoveAll": "Eliminați Tot", + "ButtonRemoveAllLibraryItems": "Ștergerea tuturor Articolelor din Librărie", + "ButtonRemoveFromContinueListening": "Ștergere din \"Continuă să Asculți\"", + "ButtonRemoveFromContinueReading": "Ștergere din \"Continuă să citești\"", + "ButtonRemoveSeriesFromContinueSeries": "Ștergere Serie din \"Continuă Seria\"", + "ButtonReset": "Resetează", + "ButtonResetToDefault": "Resetează la valorile implicite", + "ButtonRestore": "Restaurare", "ButtonSave": "Salvează", + "ButtonSaveAndClose": "Salvează și Închide", + "ButtonSaveTracklist": "Salvare Pistă", + "ButtonScan": "Scanează", + "ButtonScanLibrary": "Scanează Librăria", + "ButtonScrollLeft": "Derulează spre stânga", + "ButtonScrollRight": "Derulează spre Dreapta", "ButtonSearch": "Caută", + "ButtonSelectFolderPath": "Selectează Calea către Dosar", "ButtonSeries": "Serii", + "ButtonSetChaptersFromTracks": "Setează capitole din piste", + "ButtonShare": "Distribuie", + "ButtonShiftTimes": "Aliniează timpi", + "ButtonShow": "Arată", + "ButtonStartM4BEncode": "Începe Codarea M4B", + "ButtonStartMetadataEmbed": "Începe Încorporarea Metadatelor", + "ButtonStats": "Statistici", "ButtonSubmit": "Trimite", + "ButtonTest": "Testează", + "ButtonUnlinkOpenId": "Deconectare OpenID", + "ButtonUpload": "Încarcă", + "ButtonUploadBackup": "Încarcă Backup", + "ButtonUploadCover": "Încarcă Copertă", + "ButtonUploadOPMLFile": "Încarcă Fișier OPML", + "ButtonUserDelete": "Șterge userul {0}", + "ButtonUserEdit": "Editează userul {0}", + "ButtonViewAll": "Vizualizează tot", "ButtonYes": "Da", + "ErrorUploadFetchMetadataAPI": "Eroare în descărcarea metadatelor", + "ErrorUploadFetchMetadataNoResults": "Nu s-au putut prelua metadatele - încearcă să editezi titlul și/sau autorul", + "ErrorUploadLacksTitle": "Trebuie să aibă un titlu", "HeaderAccount": "Cont", + "HeaderAddCustomMetadataProvider": "Adaugă Furnizor de Metadate Personalizat", "HeaderAdvanced": "Avansat", + "HeaderApiKeys": "Chei API", + "HeaderAppriseNotificationSettings": "Setări Notificări Apprise", "HeaderAudioTracks": "Înregistrări audio", + "HeaderAudiobookTools": "Instrumente pentru Gestionarea Fișierelor Audiobook", + "HeaderAuthentication": "Autentificare", + "HeaderBackups": "Copii de siguranță", + "HeaderBulkChapterModal": "Adaugă Multiple Capitole", + "HeaderChangePassword": "Schimbă Parola", "HeaderChapters": "Capitole", + "HeaderChooseAFolder": "Alege Dosar", "HeaderCollection": "Colecție", "HeaderCollectionItems": "Conținutul colecției", + "HeaderCover": "Copertă", + "HeaderCurrentDownloads": "Descărcări Curente", + "HeaderCustomMessageOnLogin": "Mesaj Personalizat la Autentificare", + "HeaderCustomMetadataProviders": "Furnizor de Metadate Personalizat", "HeaderDetails": "Detalii", + "HeaderDownloadQueue": "Coadă de Descărcare", "HeaderEbookFiles": "Ebook-uri", + "HeaderEmail": "Email", + "HeaderEmailSettings": "Setări Email", "HeaderEpisodes": "Episoade", + "HeaderEreaderDevices": "Dispozitive eReader", "HeaderEreaderSettings": "Setări eReader", + "HeaderFiles": "Fișiere", + "HeaderFindChapters": "Caută Capitol", + "HeaderIgnoredFiles": "Fișiere Ignorate", + "HeaderItemFiles": "Fișiere Articol", + "HeaderLastListeningSession": "Ultima Sesiune de Ascultare", "HeaderLatestEpisodes": "Episoade recente", "HeaderLibraries": "Biblioteci", + "HeaderLibraryFiles": "Fișiere in Librărie", + "HeaderLibraryStats": "Statistici Librărie", + "HeaderListeningSessions": "Sesiuni de Ascultare", + "HeaderListeningStats": "Statistici Ascultare", + "HeaderLogin": "Autentifică", + "HeaderLogs": "Loguri", + "HeaderManageGenres": "Gestionează Genuri", + "HeaderManageTags": "Gestionează Etichete", + "HeaderMapDetails": "Detaliile Hărții", + "HeaderMatch": "Potrivește", + "HeaderMetadataOrderOfPrecedence": "Prioritatea Metadatelor", + "HeaderMetadataToEmbed": "Metadate pentru Încorporare", + "HeaderNewAccount": "Cont nou", + "HeaderNewApiKey": "Cheie API Nouă", + "HeaderNewLibrary": "Librărie Nouă", + "HeaderNotificationCreate": "Creează Notificare", + "HeaderNotificationUpdate": "Actualizare Notificare", + "HeaderNotifications": "Notificări", + "HeaderOpenIDConnectAuthentication": "Autentificare prin OpenID", + "HeaderOpenListeningSessions": "Deschide Sesiuni de Ascultare", "HeaderOpenRSSFeed": "Deschide flux RSS", + "HeaderOtherFiles": "Alte Fișiere", + "HeaderPasswordAuthentication": "Autentificare cu Parolă", + "HeaderPermissions": "Permisiuni", + "HeaderPlayerQueue": "Coadă Player", + "HeaderPlayerSettings": "Setări Player", "HeaderPlaylist": "Listă de redare", "HeaderPlaylistItems": "Conținut listă", + "HeaderPodcastsToAdd": "Podcast de Adăugat", + "HeaderPresets": "Presetări", + "HeaderPreviewCover": "Previzualizare Copertă", "HeaderRSSFeedGeneral": "Date RSS", "HeaderRSSFeedIsOpen": "RSS activ", + "HeaderRSSFeeds": "Fluxuri RSS", + "HeaderRemoveEpisode": "Elimină Episod", + "HeaderRemoveEpisodes": "Elimină {0} Episoade", + "HeaderSavedMediaProgress": "Progres Media Salvat", + "HeaderSchedule": "Planifică", + "HeaderScheduleEpisodeDownloads": "Planifică Descărcare Automată a Episoadelor", + "HeaderScheduleLibraryScans": "Planifică Scanarea Automată a Librăriei", + "HeaderSession": "Sesiuni", + "HeaderSetBackupSchedule": "Planifică Backup", "HeaderSettings": "Setări", + "HeaderSettingsDisplay": "Afișaj", + "HeaderSettingsExperimental": "Caracteristici Experimentale", + "HeaderSettingsGeneral": "General", + "HeaderSettingsScanner": "Scaner", + "HeaderSettingsSecurity": "Securitate", + "HeaderSettingsWebClient": "Client Web", "HeaderSleepTimer": "Timer de somn", + "HeaderStatsLargestItems": "Cele mai mari articole", + "HeaderStatsLongestItems": "Cele mai lungi articole (ore)", "HeaderStatsMinutesListeningChart": "Minute ascultate (ultimele 7 zile)", "HeaderStatsRecentSessions": "Sesiuni recente", + "HeaderStatsTop10Authors": "Top 10 Autori", + "HeaderStatsTop5Genres": "Top 5 Genuri", "HeaderTableOfContents": "Cuprins", + "HeaderTools": "Unelte", + "HeaderUpdateAccount": "Actualizare Cont", + "HeaderUpdateApiKey": "Actualizare Cheie API", + "HeaderUpdateAuthor": "Actualizare Autor", + "HeaderUpdateDetails": "Actualizare Detalii", + "HeaderUpdateLibrary": "Actualizare Librărie", + "HeaderUsers": "Utilizatori", + "HeaderYearReview": "Trecere în revistă a anului {0}", "HeaderYourStats": "Progresul tău", + "LabelAbridged": "Abreviat", + "LabelAbridgedChecked": "Abreviat (verificat)", + "LabelAbridgedUnchecked": "Neprescurtat (neverificat)", + "LabelAccessibleBy": "Accesibil prin", + "LabelAccountType": "Tip de Cont", + "LabelAccountTypeAdmin": "Administrator", + "LabelAccountTypeGuest": "Oaspete", + "LabelAccountTypeUser": "Utilizator", + "LabelActivities": "Activități", + "LabelActivity": "Activitate", + "LabelAddToCollection": "Adaugă la Colecție", + "LabelAddToCollectionBatch": "Adaugare {0} Cărți la Colecție", "LabelAddToPlaylist": "Adaugă în listă", + "LabelAddToPlaylistBatch": "Adaugare {0} Articole la Listă", "LabelAddedAt": "Adăugat la", "LabelAddedDate": "Adăugat {0}", + "LabelAdminUsersOnly": "Doar Administratori", "LabelAll": "Toate", + "LabelAllEpisodesDownloaded": "Toate episoadele descărcate", + "LabelAllUsers": "Toți Utilizatorii", + "LabelAllUsersExcludingGuests": "Toți utilizatorii cu excepția oaspeților", + "LabelAllUsersIncludingGuests": "Toți utilizatorii inclusiv oaspeții", + "LabelAlreadyInYourLibrary": "Deja în bibliotecă", + "LabelApiKeyCreated": "Cheia API \"{0}\" creată cu succes.", + "LabelApiKeyCreatedDescription": "Copiază cheia API acum deoarece nu va mai fi disponibilă pentru vizualizare.", + "LabelApiKeyUser": "Acționează în numele utilizatorului", + "LabelApiKeyUserDescription": "Această cheie API va avea aceleași permisiuni ca utilizatorul în numele căruia acționează. In loguri va părea că utilizatorul lansa cererile.", + "LabelApiToken": "Token API", + "LabelAppend": "Atașează", + "LabelAudioBitrate": "Rata de Biți Audio (e.g. 128k)", + "LabelAudioChannels": "Canale Audio (1 sau 2)", + "LabelAudioCodec": "Codec Audio", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (Prenume Nume)", "LabelAuthorLastFirst": "Autor (Nume, Prenume)", "LabelAuthors": "Autori", "LabelAutoDownloadEpisodes": "Descarcă automat episoadele", + "LabelAutoFetchMetadata": "Descarcă Automat Metadate", + "LabelAutoFetchMetadataHelp": "Descarcă metadate pentru titlu, autor si serii pentru eficientizarea încărcării. Metadatele suplimentare s-ar putea să trebuiască potrivite după încărcare.", + "LabelAutoLaunch": "Lansare automată", + "LabelAutoLaunchDescription": "Redirecționează automat către furnizorul de autentificare când navighez la pagina de autentificare (cale de suprascriere manuală /login?autoLaunch=0)", + "LabelAutoRegister": "Înregistrare Automată", + "LabelAutoRegisterDescription": "Creează utilizatori automat dupa autentificare", + "LabelBackToUser": "Înapoi la Utilizator", + "LabelBackupAudioFiles": "Copii de rezervă a Fișierelor Audio", + "LabelBackupLocation": "Locația Copiilor de Rezervă", + "LabelBackupsEnableAutomaticBackups": "Copii de Rezervă Automate", + "LabelBackupsEnableAutomaticBackupsHelp": "Copiile de Rezervă au fost salvate în /metadata/backups", + "LabelBackupsMaxBackupSize": "Dimensiunea maximă a copiilor de rezervă (în GB) (0 pentru nelimitat)", + "LabelBackupsMaxBackupSizeHelp": "Ca protecție împotriva configurațiilor greșite, backup-ul va eșua dacă trece de limita de dimensiune configurată.", + "LabelBackupsNumberToKeep": "Numărul copiilor de siguranță de păstrat", + "LabelBackupsNumberToKeepHelp": "Doar 1 copie de siguranță va fi ștearsă odata deci dacă există mai multe copii de siguranță vor trebui șterse manual.", + "LabelBitrate": "Rată de biți", + "LabelBonus": "Bonus", "LabelBooks": "Cărți", + "LabelButtonText": "Textul Butonului", + "LabelByAuthor": "de {0}", + "LabelChangePassword": "Schimbare Parolă", + "LabelChannels": "Canale", + "LabelChapterCount": "{0} Capitole", + "LabelChapterTitle": "Titlul Capitolului", "LabelChapters": "Capitole", + "LabelChaptersFound": "capitole găsite", + "LabelClickForMoreInfo": "Click pentru mai multe informații", + "LabelClickToUseCurrentValue": "Click pentru a folosi valoarea curentă", "LabelClosePlayer": "Închide playerul", + "LabelCodec": "Codec", "LabelCollapseSeries": "Restrânge seriile", + "LabelCollapseSubSeries": "Restrânge Sub-Seriile", + "LabelCollection": "Colecție", + "LabelCollections": "Colecții", "LabelComplete": "Finalizat", + "LabelConfirmPassword": "Confirmare Parolă", "LabelContinueListening": "Ascultă în continuare", "LabelContinueReading": "Continuă lectura", "LabelContinueSeries": "Continuă seria", + "LabelCorsAllowed": "Origini CORS Permise", + "LabelCover": "Copertă", + "LabelCoverImageURL": "URL-ul Imaginii de Copertă", + "LabelCoverProvider": "Furnizor Copertă", + "LabelCreatedAt": "Creat la", + "LabelCronExpression": "Expresie Cron", + "LabelCurrent": "Curent", + "LabelCurrently": "Acum:", + "LabelCustomCronExpression": "Expresie Cron Personalizată:", + "LabelDatetime": "Data și ora", + "LabelDays": "Zile", + "LabelDeleteFromFileSystemCheckbox": "Șterge fișierele din sistem (debifeaza pentru a șterge doar din baza de date)", "LabelDescription": "Descriere", + "LabelDeselectAll": "Deselectați Tot", + "LabelDetectedPattern": "Tipar Identificat:", + "LabelDevice": "Dispozitiv", + "LabelDeviceInfo": "Informații Dispozitiv", + "LabelDeviceIsAvailableTo": "Dispozitiv accesibil lui...", + "LabelDirectory": "Dosar", + "LabelDiscFromFilename": "Disc din Numele Fișierului", + "LabelDiscFromMetadata": "Disc din Metadate", "LabelDiscover": "Descoperă", "LabelDownload": "Descarcă", + "LabelDownloadNEpisodes": "Descarcă {0} episoade", + "LabelDownloadable": "Descărcabil", "LabelDuration": "Durată", + "LabelDurationComparisonExactMatch": "(potrivire exactă)", + "LabelDurationComparisonLonger": "({0} mai lung)", + "LabelDurationComparisonShorter": "({0} mai scurt)", + "LabelDurationFound": "Durată identificată:", "LabelEbook": "Carte electronică", "LabelEbooks": "Cărți electronice", + "LabelEdit": "Editare", + "LabelEmail": "Email", + "LabelEmailSettingsFromAddress": "De la Adresa", + "LabelEmailSettingsRejectUnauthorized": "Respingere certificate neautorizate", + "LabelEmailSettingsRejectUnauthorizedHelp": "Dezactivarea verificării certificatelor SSL vă poate expune conexiunea la riscuri de securitate, cum ar fi atacuri de tip man-in-the-middle. Dezactivați această opțiune dacă înțelegeti implicațiile și aveți încredere în serverul de mail la care vă conectați.", + "LabelEmailSettingsSecure": "Sigur", + "LabelEmailSettingsSecureHelp": "Dacă e adevărat, conexiunea se va realiza prin TLS către server. Dacă e fals, TLS este folosit dacă serverul suporta extensia STARTTLS. În majoritatea cazurilor setati adevărat dacă folosiți portul 465. Pentru portul 587 sau 25 setati fals. (referinta nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Adresă de Test", + "LabelEmbeddedCover": "Încorporează Copertă", "LabelEnable": "Activează", + "LabelEncodingBackupLocation": "O copie de siguranță a fișierului audio original va fi salvată în:", + "LabelEncodingChaptersNotEmbedded": "Capitolele nu sunt încorporate în cărțile audio cu mai multe track-uri.", + "LabelEncodingClearItemCache": "Asigurați-vă că ștergeți articolele din cache periodic.", + "LabelEncodingFinishedM4B": "Fișierul M4B va fi adaugat în dosarul dvs. de cărți audio când codificarea e terminată:", + "LabelEncodingInfoEmbedded": "Metadatele vor fi încorporate în fișierele audio din interiorul dosarului dvs. cu cărți audio.", + "LabelEncodingStartedNavigation": "Odată pornită sarcina poti naviga din această pagină.", + "LabelEncodingTimeWarning": "Codificarea poate dura până la 30 de minute.", + "LabelEncodingWarningAdvancedSettings": "Avertizare: Nu modificați aceste setări dacă nu sunteți familiar cu opțiunile de codare ffmpeg .", + "LabelEncodingWatcherDisabled": "Dacă ați dezactivat funcția de urmările va trebui sa rescanați acestă carte audio la ulterior.", "LabelEnd": "Sfârșit", "LabelEndOfChapter": "Sfârșitul capitolului", "LabelEpisode": "Episod", + "LabelEpisodeNotLinkedToRssFeed": "Episoade nelegate de un flux RSS", + "LabelEpisodeNumber": "Episodul #{0}", + "LabelEpisodeTitle": "Titlul Episodului", + "LabelEpisodeType": "Tipul Episodului", + "LabelEpisodeUrlFromRssFeed": "URL-ul Episodului din Fluxul RSS", + "LabelEpisodes": "Episoade", + "LabelEpisodic": "Episodic", + "LabelExample": "Exemplu", + "LabelExpandSeries": "Extinde Seriile", + "LabelExpandSubSeries": "Extinde Sub-Seriile", + "LabelExpired": "Expirat", + "LabelExpiresAt": "Expiră La", + "LabelExpiresInSeconds": "Expiră în (secunde)", + "LabelExpiresNever": "Niciodată", "LabelExplicit": "Explicit", - "LabelFeedURL": "URL flux", + "LabelExplicitChecked": "Explicit (verificat)", + "LabelExplicitUnchecked": "Neexplicit (neverificat)", + "LabelExportOPML": "Exportă OPML", + "LabelFeedURL": "Flux URL", + "LabelFetchingMetadata": "Aducere Metadate", "LabelFile": "Fișier", "LabelFileBirthtime": "Data creării fișierului", + "LabelFileBornDate": "Creat {0}", "LabelFileModified": "Fișier modificat", + "LabelFileModifiedDate": "Modificat {0}", "LabelFilename": "Nume fișier", + "LabelFilterByUser": "Filtrare după Utilizator", + "LabelFindEpisodes": "Găsire Episoade", "LabelFinished": "Finalizat", + "LabelFinishedDate": "Finalizat {0}", "LabelFolder": "Dosar", + "LabelFolders": "Dosare", + "LabelFontBold": "Îngroșat", "LabelFontBoldness": "Grosimea fontului", + "LabelFontFamily": "Familia Fontului", + "LabelFontItalic": "Cursiv", "LabelFontScale": "Mărimea fontului", + "LabelFontStrikethrough": "Tăiat cu o linie", + "LabelFormat": "Format", + "LabelFull": "Întreg", "LabelGenre": "Gen", "LabelGenres": "Genuri", + "LabelHardDeleteFile": "Ștergere definitivă a fișierului", "LabelHasEbook": "Are carte electronică", "LabelHasSupplementaryEbook": "Are carte electronică suplimentară", + "LabelHideSubtitles": "Ascunde Subtitrări", + "LabelHighestPriority": "Prioritatea cea mai ridicată", "LabelHost": "Gazdă", + "LabelHour": "Ora", + "LabelHours": "Ore", + "LabelIcon": "Pictogramă", + "LabelImageURLFromTheWeb": "URL-ul imaginii de pe web", "LabelInProgress": "În desfășurare", + "LabelIncludeInTracklist": "Include în Lista de Melodii", "LabelIncomplete": "Incomplet", + "LabelInterval": "Interval", + "LabelIntervalCustomDailyWeekly": "Personalizat zilnic/saptămânal", + "LabelIntervalEvery12Hours": "La fiecare 12 ore", + "LabelIntervalEvery15Minutes": "La fiecare 15 minute", + "LabelIntervalEvery2Hours": "La fiecare 2 ore", + "LabelIntervalEvery30Minutes": "La fiecare 30 minute", + "LabelIntervalEvery6Hours": "La fiecare 6 ore", + "LabelIntervalEveryDay": "În fiecare zi", + "LabelIntervalEveryHour": "În fiecare oră", + "LabelIntervalEveryMinute": "La fiecare minut", + "LabelInvert": "Inversează", + "LabelItem": "Articol", + "LabelJumpBackwardAmount": "Sari înapoi cu", + "LabelJumpForwardAmount": "Sari înainte cu", "LabelLanguage": "Limbă", + "LabelLanguageDefaultServer": "Limba Prestabilită a Serverului", + "LabelLanguages": "Limbi", + "LabelLastBookAdded": "Ultima Carte Adăugată", + "LabelLastBookUpdated": "Ultima Carte Actualizată", + "LabelLastProgressDate": "Ultimul progres: {0}", + "LabelLastSeen": "Ultima dată văzut", + "LabelLastTime": "Ultima dată", + "LabelLastUpdate": "Ultima actualizare", "LabelLayout": "Aspect", "LabelLayoutSinglePage": "Pagină unică", + "LabelLayoutSplitPage": "Pagină împărțită", + "LabelLess": "Mai puțin", + "LabelLibrariesAccessibleToUser": "Biblioteci Accesibile Utilizatorului", + "LabelLibrary": "Bibliotecă", + "LabelLibraryFilterSublistEmpty": "Numărul {0}", + "LabelLibraryItem": "Articol din Bibliotecă", + "LabelLibraryName": "Numele Bibliotecii", + "LabelLibrarySortByProgress": "Progres: Ultima Actualizare", + "LabelLibrarySortByProgressFinished": "Progres: Finalizat", + "LabelLibrarySortByProgressStarted": "Progres: Început", + "LabelLimit": "Limită", "LabelLineSpacing": "Spațiere între rânduri", "LabelListenAgain": "Ascultă din nou", + "LabelLogLevelDebug": "Depanare", + "LabelLogLevelInfo": "Informații", + "LabelLogLevelWarn": "Avertizare", + "LabelLookForNewEpisodesAfterDate": "Caută episoade noi după această dată", + "LabelLowestPriority": "Cea Mai Scăzută Prioritate", + "LabelMatchConfidence": "Încredere", + "LabelMatchExistingUsersBy": "Potrivire utilizatori existenți prin", + "LabelMatchExistingUsersByDescription": "Folosit pentru a conecta utilizatorii existenți. Odata conectați, utilizatorii vor fi potriviți după un ID unic trimis de furnizorul SSO", + "LabelMaxEpisodesToDownload": "Numarul maxim # de episoade de descărcat. Folosiți 0 pentru nelimitat.", + "LabelMaxEpisodesToDownloadPerCheck": "Numărul maxim # de episoade de descărcat per verificare", + "LabelMaxEpisodesToKeep": "Numarul maxim # de episoade păstrate", + "LabelMaxEpisodesToKeepHelp": "Valorea 0 nu stabilește o limită maximă. După ce un episod nou a fost descărcat automat, cel mai vechi episod va fi șters dacă aveți mai mult de X episoade. Se va șterge câte un episod vechi pentru fiecare episod nou descărcat.", + "LabelMediaPlayer": "Player Media", "LabelMediaType": "Tip media", + "LabelMetaTag": "Etichetă Meta", + "LabelMetaTags": "Etichete Meta", + "LabelMetadataOrderOfPrecedenceDescription": "Sursele de metadate cu prioritate mai mare o să suprascrie sursele de metadate cu prioritate mai mică", + "LabelMetadataProvider": "Furnizor Metadate", + "LabelMinute": "Minut", + "LabelMinutes": "Minute", "LabelMissing": "Lipsă", + "LabelMissingEbook": "Nu are carte electronică", + "LabelMissingSupplementaryEbook": "Nu are carte electronică adițională", + "LabelMobileRedirectURIs": "URL-uri de redirecționare Mobile Permise", + "LabelMobileRedirectURIsDescription": "Aceasta este o listă cu URI-uri valide pentru redirectionare a aplicațiilor mobile. URI-ul predefinit este audiobookshelf://oauth,, care poate fi sters sau suplimentat cu URI-uri adiționale pentru integrarea cu alte aplicații. Folosirea unui asterisc (*) ca singur element permite orice URI.", "LabelMore": "Mai multe", "LabelMoreInfo": "Mai multe informații", "LabelName": "Nume", "LabelNarrator": "Narator", "LabelNarrators": "Naratori", + "LabelNew": "Nou", + "LabelNewPassword": "Parolă Nouă", "LabelNewestAuthors": "Autori noi", "LabelNewestEpisodes": "Episoade noi", + "LabelNextBackupDate": "Următoarea dată a copiilor de siguranță", + "LabelNextChapters": "Următoarele capitole vor fi:", + "LabelNextScheduledRun": "Urmatoarea rulare programată", + "LabelNoApiKeys": "Nu exista chei API", + "LabelNoCustomMetadataProviders": "Nu există furnizori de metadate personalizați", + "LabelNoEpisodesSelected": "Nici un episod selectat", "LabelNotFinished": "Nefinalizat", "LabelNotStarted": "Neînceput", + "LabelNotes": "Note", + "LabelNotificationAppriseURL": "URL-ul(urile) Apprise", + "LabelNotificationAvailableVariables": "Variabile disponibile", + "LabelNotificationBodyTemplate": "Corpul Șablonului", + "LabelNotificationEvent": "Eveniment de notificare", + "LabelNotificationTitleTemplate": "Titlul Șablonului", + "LabelNotificationsMaxFailedAttempts": "Număr de încercări eșuate maxim", + "LabelNotificationsMaxFailedAttemptsHelp": "Notificările sunt dezactivate dacă nu reușesc să fie trimise de acest număr de ori", + "LabelNotificationsMaxQueueSize": "Dimensiunea maximă a cozii pentru evenimentele de notificare", + "LabelNotificationsMaxQueueSizeHelp": "Evenimentele sunt limitate la 1 per secunda. Evenimentele vor fi ignorate dacă coada este plină. Previne spamarea cu notificări.", + "LabelNumberOfBooks": "Numărul de Cărți", + "LabelNumberOfChapters": "Număr de capitole:", "LabelNumberOfEpisodes": "# de Episoade", + "LabelOpenIDAdvancedPermsClaimDescription": "Numele revendicării OpenID care conține permisiuni avansate pentru acțiunile utilizatorului în interiorul aplicației care vor fi aplicate rolurilor non-administrator (dacă e configurat). Dacă revendicarea nu e prezentă în răspunsul primit, accesul către ABS e refuzat. Dacă o singură opțiune lipsește, va fi tratată ca falsă. Asigurați-vă că revendicările furnizorului de securitate corespund structurii așteptate:", + "LabelOpenIDClaims": "Lăsați urmatoarele opțiuni goale pentru a dezactiva atribuirea avansată de grupuri și permisiuni, asignând grupul \"Utilizatori\" automat.", + "LabelOpenRSSFeed": "Flux Open RSS", + "LabelOverwrite": "Suprascrie", + "LabelPaginationPageXOfY": "Pagina {0} din {1}", "LabelPassword": "Parolă", "LabelPath": "Cale", + "LabelPermanent": "Permanent", + "LabelPermissionsAccessAllLibraries": "Poate accesa toate bibliotecile", + "LabelPermissionsAccessAllTags": "Poate accesa toate etichetele", + "LabelPermissionsAccessExplicitContent": "Poate Accesa Conținut Explicit", + "LabelPermissionsCreateEreader": "Poate Crea Cititoare Electronice", + "LabelPermissionsDelete": "Poate Șterge", + "LabelPermissionsDownload": "Poate Descărca", + "LabelPermissionsUpdate": "Poate Actualiza", + "LabelPermissionsUpload": "Poate Încărca", + "LabelPersonalYearReview": "Recapitularea Anului tău ({0})", + "LabelPhotoPathURL": "Calea/URL-ul Fotografiei", + "LabelPlayMethod": "Metoda de Redare", + "LabelPlaybackRateIncrementDecrement": "Incrementare/Decrementare a Ratei de Redare cu", + "LabelPlayerChapterNumberMarker": "{0} din {1}", + "LabelPlaylists": "Liste de redare", "LabelPodcast": "Podcast", + "LabelPodcastSearchRegion": "Regiunea căutării podcastului", + "LabelPodcastType": "Tipul Podcastului", "LabelPodcasts": "Podcasturi", + "LabelPort": "Portul", + "LabelPrefixesToIgnore": "Prefix de ignorat (fără a ține cont de majuscule)", "LabelPreventIndexing": "Împiedică indexarea fluxului în directoarele iTunes și Google Podcasts", + "LabelPrimaryEbook": "eCarte Principală", "LabelProgress": "Progres", + "LabelProvider": "Furnizor", + "LabelProviderAuthorizationValue": "Valoarea Antetului de Autorizare", "LabelPubDate": "Data publicării", "LabelPublishYear": "Anul publicării", "LabelPublishedDate": "Publicat la {0}", + "LabelPublishedDecade": "Deceniul Publicării", + "LabelPublishedDecades": "Deceniile Publicării", + "LabelPublisher": "Editor", + "LabelPublishers": "Editori", "LabelRSSFeedCustomOwnerEmail": "Email personalizat al proprietarului", "LabelRSSFeedCustomOwnerName": "Nume personalizat al proprietarului", "LabelRSSFeedOpen": "Flux RSS deschis", "LabelRSSFeedPreventIndexing": "Previne indexarea", "LabelRSSFeedSlug": "Identificator flux RSS", + "LabelRSSFeedURL": "URL-ul Fluxului RSS", "LabelRandomly": "Aleatoriu", + "LabelReAddSeriesToContinueListening": "Readăugare serie la \"Continuă să asculți\"", "LabelRead": "Citește", "LabelReadAgain": "Citește din nou", + "LabelReadEbookWithoutProgress": "Citire eCarte fără a memora progresul", "LabelRecentSeries": "Serii recente", "LabelRecentlyAdded": "Adăugate recent", + "LabelRecommended": "Recomandat", + "LabelRedo": "Refă", + "LabelRegion": "Regiune", + "LabelReleaseDate": "Data Lansării", + "LabelRemoveAllMetadataAbs": "Ștergerea tuturor fișierelor metadata.abs", + "LabelRemoveAllMetadataJson": "Ștergerea tuturor fișierelor metadata.json", + "LabelRemoveAudibleBranding": "Ștergerea Audible intro și outro din capitole", + "LabelRemoveCover": "Șterge coperta", + "LabelRemoveMetadataFile": "Șterge fisierele metadate din dosarele bibliotecii", + "LabelRemoveMetadataFileHelp": "Șterge toate fișierele metadata.json și metadata.abs din {0} dosare.", + "LabelRowsPerPage": "Rânduri pe pagină", + "LabelSearchTerm": "Termen de căutat", + "LabelSearchTitle": "Titlu de căutat", + "LabelSearchTitleOrASIN": "Titlu de căutat sau ASN", "LabelSeason": "Sezon", + "LabelSeasonNumber": "Sezonul #{0}", + "LabelSelectAll": "Selectează tot", + "LabelSelectAllEpisodes": "Selectează toate episoadele", + "LabelSelectEpisodesShowing": "Selectează {0} episoade dintre cele afișate", + "LabelSelectUser": "Selectare utilizator", + "LabelSelectUsers": "Selectare utilizatori", + "LabelSendEbookToDevice": "Trimite eCarte către...", + "LabelSequence": "Secvență", + "LabelSerial": "Serie", "LabelSeries": "Serii", + "LabelSeriesName": "Numele Seriilor", + "LabelSeriesProgress": "Progresul Seriilor", + "LabelServerLogLevel": "Nivelul de Jurnal al Serverului", + "LabelServerYearReview": "Anul Serverului în Retrospectivă ({0})", "LabelSetEbookAsPrimary": "Setează ca principală", "LabelSetEbookAsSupplementary": "Setează ca suplimentară", + "LabelSettingsAllowIframe": "Permite încorporarea intr-un iframe", + "LabelSettingsAudiobooksOnly": "Doar cărți audio", + "LabelSettingsAudiobooksOnlyHelp": "Activarea acestei set[ri va ignora fișierele eBook daca acestea nu se află într-un dosar al unei cărți audio, caz în care vor fi setate ca eBook suplimentar", + "LabelSettingsBookshelfViewHelp": "Design scheumorf cu rafturi de lemn", + "LabelSettingsChromecastSupport": "Suport Chromecast", + "LabelSettingsDateFormat": "Formatul Datei", + "LabelSettingsEnableWatcher": "Urmărește în mod automat bibliotecile pentru schimbări", + "LabelSettingsEnableWatcherForLibrary": "Urmărește în mod automat biblioteca pentru schimbări", "LabelShowAll": "Afișează tot", "LabelSize": "Dimensiune", "LabelSleepTimer": "Timer de somn", From 210fa55b6a62c3b6b3881dd7d0eb61dd5e313481 Mon Sep 17 00:00:00 2001 From: Plazec Date: Tue, 17 Feb 2026 18:44:48 +0100 Subject: [PATCH 178/285] Translated using Weblate (Czech) 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/cs/ --- client/strings/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index f70d6693c..49b0d2bd7 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -385,7 +385,7 @@ "LabelFontBoldness": "Výraznost písma", "LabelFontFamily": "Rodina písem", "LabelFontItalic": "Kurzíva", - "LabelFontScale": "Měřítko písma", + "LabelFontScale": "Velikost písma", "LabelFontStrikethrough": "Přeškrtnutí", "LabelFormat": "Formát", "LabelFull": "Plné", From 6c2e13fb4e50556a4f6ec0ab005ea577521adbe8 Mon Sep 17 00:00:00 2001 From: Gneb Date: Tue, 17 Feb 2026 18:46:03 +0100 Subject: [PATCH 179/285] Translated using Weblate (Slovak) 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/sk/ --- client/strings/sk.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/strings/sk.json b/client/strings/sk.json index e9d1d4c60..3ea1e4b4a 100644 --- a/client/strings/sk.json +++ b/client/strings/sk.json @@ -23,7 +23,7 @@ "ButtonClearFilter": "Zrušiť filter", "ButtonClose": "Uzavrieť", "ButtonCloseFeed": "Zatvoriť zdroj", - "ButtonCloseSession": "Ukončiť otvorené pripojenie", + "ButtonCloseSession": "Ukončiť aktívne relácie", "ButtonCollections": "Kolekcie", "ButtonConfigureScanner": "Nastaviť skener", "ButtonCreate": "Vytvoriť", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Žiadne {0}", "LabelLibraryItem": "Položka knižnice", "LabelLibraryName": "Názov knižnice", - "LabelLibrarySortByProgress": "Pokrok: Aktualizované", - "LabelLibrarySortByProgressFinished": "Pokrok: Dokončené", - "LabelLibrarySortByProgressStarted": "Pokrok: Začiatok", + "LabelLibrarySortByProgress": "Stav: Naposledy aktualizované", + "LabelLibrarySortByProgressFinished": "Stav: Dokončené", + "LabelLibrarySortByProgressStarted": "Stav: Začal", "LabelLimit": "Limit", "LabelLineSpacing": "Riadkovanie", "LabelListenAgain": "Počúvať znova", @@ -1015,7 +1015,7 @@ "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.", + "ToastChapterStartTimeAdjusted": "Čas začiatku kapitoly upravený o {0} sekúnd", "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.", @@ -1026,7 +1026,7 @@ "ToastCollectionItemsAddFailed": "Pridanie položky/-iek do zbierky zlyhalo", "ToastCollectionRemoveSuccess": "Zbierka odstránená", "ToastCollectionUpdateSuccess": "Zbierka aktualizovaná", - "ToastConnectionNotAvailable": "Pripojenie je nedostupné. Skúste to neskôr.", + "ToastConnectionNotAvailable": "Pripojenie je nedostupné. Skúste to neskôr", "ToastCoverSearchFailed": "Vyhľadanie obalu sa nepodarilo", "ToastCoverUpdateFailed": "Aktualizácia prebalu zlyhala", "ToastDateTimeInvalidOrIncomplete": "Dátum a čas sú neplatné alebo neúplné", From c1a6b51d78ac8d5c198868c41027e54b7ef5b213 Mon Sep 17 00:00:00 2001 From: herny ucet Date: Tue, 17 Feb 2026 18:45:51 +0100 Subject: [PATCH 180/285] Translated using Weblate (Slovak) 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/sk/ --- client/strings/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/sk.json b/client/strings/sk.json index 3ea1e4b4a..f0d3c487b 100644 --- a/client/strings/sk.json +++ b/client/strings/sk.json @@ -383,7 +383,7 @@ "LabelFolders": "Priečinky", "LabelFontBold": "Tučné", "LabelFontBoldness": "Hrúbka písma", - "LabelFontFamily": "Písmo", + "LabelFontFamily": "písmo", "LabelFontItalic": "Kurzíva", "LabelFontScale": "Veľkosť písma", "LabelFontStrikethrough": "Preškrtnuté", From 2f326739914157535d965ef0a8ebfdbcee9836a9 Mon Sep 17 00:00:00 2001 From: peter cerny Date: Tue, 17 Feb 2026 18:48:31 +0100 Subject: [PATCH 181/285] Translated using Weblate (Slovak) 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/sk/ --- client/strings/sk.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/strings/sk.json b/client/strings/sk.json index f0d3c487b..6101eba7d 100644 --- a/client/strings/sk.json +++ b/client/strings/sk.json @@ -150,12 +150,12 @@ "HeaderIgnoredFiles": "Ignorované súbory", "HeaderItemFiles": "Položka Súbory", "HeaderItemMetadataUtils": "Položka Nástroje metadát", - "HeaderLastListeningSession": "Posledné pripojenie", + "HeaderLastListeningSession": "Posledná relácia", "HeaderLatestEpisodes": "Posledné epizódy", "HeaderLibraries": "Knižnice", "HeaderLibraryFiles": "Súbory knižnice", "HeaderLibraryStats": "Štatistiky knižnice", - "HeaderListeningSessions": "Pripojenia", + "HeaderListeningSessions": "Relácie", "HeaderListeningStats": "Štatistiky počúvania", "HeaderLogin": "Prihlásenie", "HeaderLogs": "Záznamy udalostí", @@ -172,7 +172,7 @@ "HeaderNotificationUpdate": "Aktualizovať notifikáciu", "HeaderNotifications": "Notifikácie", "HeaderOpenIDConnectAuthentication": "Overenie pripojenia OpenID", - "HeaderOpenListeningSessions": "Aktívne pripojenia", + "HeaderOpenListeningSessions": "Aktívne relácie", "HeaderOpenRSSFeed": "Otvoriť RSS zdroj", "HeaderOtherFiles": "Ostatné súbory", "HeaderPasswordAuthentication": "Overenie heslom", @@ -189,7 +189,7 @@ "HeaderRSSFeeds": "RSS zdroje", "HeaderRemoveEpisode": "Odstrániť epizódu", "HeaderRemoveEpisodes": "Odstrániť {0} epizód", - "HeaderSavedMediaProgress": "Priebeh uložených médií", + "HeaderSavedMediaProgress": "Stav uložených médií", "HeaderSchedule": "Plán", "HeaderScheduleEpisodeDownloads": "Naplánovať automatické sťahovanie epizód", "HeaderScheduleLibraryScans": "Naplánovanovať automatické skenovanie knižnice", @@ -222,7 +222,7 @@ "LabelAbridged": "Skrátená verzia", "LabelAbridgedChecked": "Skrátená verzia (zaškrtnuté)", "LabelAbridgedUnchecked": "Neskrátená verzia (nezaškrtnuté)", - "LabelAccessibleBy": "Prístupné pre", + "LabelAccessibleBy": "Dostupné pre", "LabelAccountType": "Typ účtu", "LabelAccountTypeAdmin": "Administrátor", "LabelAccountTypeGuest": "Hosť", @@ -230,7 +230,7 @@ "LabelActivities": "Aktivity", "LabelActivity": "Aktivita", "LabelAddToCollection": "Pridať do zbierky", - "LabelAddToCollectionBatch": "Pridať {0} kníh do kolekcie", + "LabelAddToCollectionBatch": "Pridať {0} kníh do zbierky", "LabelAddToPlaylist": "Pridať do playlistu", "LabelAddToPlaylistBatch": "Pridať {0} položie do playlistu", "LabelAddedAt": "Pridané", @@ -288,8 +288,8 @@ "LabelCodec": "Kodek", "LabelCollapseSeries": "Zbaliť série", "LabelCollapseSubSeries": "Zbaliť podsérie", - "LabelCollection": "Kolekcia", - "LabelCollections": "Kolekcie", + "LabelCollection": "Zbierka", + "LabelCollections": "Zbierky", "LabelComplete": "Hotovo", "LabelConfirmPassword": "Potvrdiť heslo", "LabelContinueListening": "Pokračovať v počúvaní", @@ -528,7 +528,7 @@ "LabelPrefixesToIgnore": "Ignorované predpony (bez ohľadu na veľkosť písmen)", "LabelPreventIndexing": "Zabráni indexácii vašich zdrojov službami iTunes a Google podcast directories", "LabelPrimaryEbook": "Primárny e-book", - "LabelProgress": "Stav", + "LabelProgress": "Aktuálny stav", "LabelProvider": "Poskytovateľ", "LabelProviderAuthorizationValue": "Obsah hlavičky autorizácie", "LabelPubDate": "Dátum publikovania", @@ -548,7 +548,7 @@ "LabelReAddSeriesToContinueListening": "Znova pridať série do pokračujúceho počúvania", "LabelRead": "Načítať", "LabelReadAgain": "Čítať znova", - "LabelReadEbookWithoutProgress": "Čítať e-knihu bez sledovania pokroku", + "LabelReadEbookWithoutProgress": "Čítať e-knihu bez zmeny stavu", "LabelRecentSeries": "Posledné série", "LabelRecentlyAdded": "Posledné pridané", "LabelRecommended": "Odporúčané", @@ -577,7 +577,7 @@ "LabelSerial": "Na pokračovanie", "LabelSeries": "Série", "LabelSeriesName": "Názov série", - "LabelSeriesProgress": "Pokrok série", + "LabelSeriesProgress": "Aktuálny stav série", "LabelServerLogLevel": "Úroveň logovania servera", "LabelServerYearReview": "Rok servera v prehľade ({0})", "LabelSetEbookAsPrimary": "Nastaviť ako primárny", From a1d439b8d53ad456ae0315dd0f04de2ba8538a4a Mon Sep 17 00:00:00 2001 From: lambolighting Date: Thu, 19 Feb 2026 12:29:55 +0100 Subject: [PATCH 182/285] Translated using Weblate (Greek) Currently translated at 27.6% (322 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/ --- client/strings/el.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/strings/el.json b/client/strings/el.json index 881bd971b..2160b6e14 100644 --- a/client/strings/el.json +++ b/client/strings/el.json @@ -100,9 +100,11 @@ "ButtonShiftTimes": "Χρόνοι Μετακίνησης", "ButtonShow": "Εμφάνιση", "ButtonStartM4BEncode": "Έναρξη Κωδικοποίησης M4B", + "ButtonStartMetadataEmbed": "Έναρξη Ενσωμάτωσης Μεταδεδομένων", "ButtonStats": "Στατιστικά", "ButtonSubmit": "Υποβολή", "ButtonTest": "Δοκιμή", + "ButtonUnlinkOpenId": "Αποσύνδεση OpenID", "ButtonUpload": "Μεταφόρτωση", "ButtonUploadBackup": "Μεταφόρτωση Αντιγράφου Ασφαλείας", "ButtonUploadCover": "Μεταφόρτωση Εξωφύλλου", @@ -111,11 +113,17 @@ "ButtonUserEdit": "Επεξεργασίας χρήστη {0}", "ButtonViewAll": "Εμφάνιση Όλων", "ButtonYes": "Ναι", + "ErrorUploadFetchMetadataAPI": "Σφάλμα κατά την ανάκτηση μεταδεδομένων", + "ErrorUploadFetchMetadataNoResults": "Δεν ήταν δυνατή η ανάκτηση των μεταδεδομένων - δοκιμάστε να ενημερώσετε τον τίτλο και/ή τον συγγραφέα", "ErrorUploadLacksTitle": "Πρέπει να έχει τίτλο", "HeaderAccount": "Λογαριασμός", + "HeaderAddCustomMetadataProvider": "Προσθήκη Προσαρμοσμένου Παρόχου Μεταδεδομένων", "HeaderAdvanced": "Για Προχωρημένους", "HeaderApiKeys": "Κλειδιά API", + "HeaderAppriseNotificationSettings": "Ρυθμίσεις Ειδοποιήσεων Apprise", "HeaderAudioTracks": "Κομμάτια Ήχου", + "HeaderAudiobookTools": "Εργαλεία Διαχείρισης Αρχείων Audiobooks", + "HeaderAuthentication": "Αυθεντικοποίηση", "HeaderBackups": "Αντίγραφα Ασφαλείας", "HeaderBulkChapterModal": "Προσθήκη Πολλαπλών Κεφαλαίων", "HeaderChangePassword": "Αλλαγή Κωδικού Πρόσβασης", @@ -216,6 +224,7 @@ "LabelChapters": "Κεφάλαια", "LabelChaptersFound": "κεφάλαια βρέθηκαν", "LabelClosePlayer": "Κλείσιμο αναπαραγωγής", + "LabelCollapseSeries": "Σύμπτυξη Σειράς", "LabelCollection": "Συλλογή", "LabelCollections": "Συλλογές", "LabelComplete": "Ολοκλήρωση", From fa6dae1a53638e3d7e49e9e6d48e9578c8fcc4e7 Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Tue, 24 Feb 2026 20:09:01 +0100 Subject: [PATCH 183/285] Translated using Weblate (Belarusian) Currently translated at 79.8% (929 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/ --- client/strings/be.json | 446 ++++++++++++++++++++++++++++------------- 1 file changed, 309 insertions(+), 137 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index f812a13c9..799511d4c 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -7,18 +7,18 @@ "ButtonAddPodcasts": "Дадаць падкасты", "ButtonAddUser": "Дадаць карыстальніка", "ButtonAddYourFirstLibrary": "Дадайце сваю першую бібліятэку", - "ButtonApply": "Ужыць", - "ButtonApplyChapters": "Ужыць раздзелы", + "ButtonApply": "Прымяніць", + "ButtonApplyChapters": "Прымяніць раздзелы", "ButtonAuthors": "Аўтары", "ButtonBack": "Назад", "ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага", "ButtonBatchEditPopulateMapDetails": "Запоўніць падрабязнасці карты", - "ButtonBrowseForFolder": "Знайсці тэчку", + "ButtonBrowseForFolder": "Агляд папак", "ButtonCancel": "Скасаваць", "ButtonCancelEncode": "Скасаваць кадзіраванне", "ButtonChangeRootPassword": "Зменіце Root пароль", "ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды", - "ButtonChooseAFolder": "Выбраць тэчку", + "ButtonChooseAFolder": "Выбраць папку", "ButtonChooseFiles": "Выбраць файлы", "ButtonClearFilter": "Ачысціць фільтр", "ButtonClose": "Закрыць", @@ -26,7 +26,7 @@ "ButtonCloseSession": "Закрыць адкрыты сеанс", "ButtonCollections": "Калекцыі", "ButtonConfigureScanner": "Наладзіць сканер", - "ButtonCreate": "Ствараць", + "ButtonCreate": "Стварыць", "ButtonCreateBackup": "Стварыць рэзервовую копію", "ButtonDelete": "Выдаліць", "ButtonDownloadQueue": "Чарга", @@ -47,22 +47,22 @@ "ButtonLibrary": "Бібліятэка", "ButtonLogout": "Выйсці", "ButtonLookup": "Пошук", - "ButtonManageTracks": "Кіраванне дарожкамі", - "ButtonMapChapterTitles": "Супаставіць назвы раздзелаў", + "ButtonManageTracks": "Кіраванне трэкамі", + "ButtonMapChapterTitles": "Супаставіць загалоўкі раздзелаў", "ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў", "ButtonMatchBooks": "Падбор кніг", "ButtonNevermind": "Няважна", "ButtonNext": "Далей", "ButtonNextChapter": "Наступны раздзел", "ButtonNextItemInQueue": "Наступны элемент у чарзе", - "ButtonOk": "Добра", + "ButtonOk": "ОК", "ButtonOpenFeed": "Адкрыць стужку", "ButtonOpenManager": "Адкрыць менеджар", - "ButtonPause": "Паўза", + "ButtonPause": "Прыпыніць", "ButtonPlay": "Прайграць", "ButtonPlayAll": "Прайграць усё", "ButtonPlaying": "Прайграваецца", - "ButtonPlaylists": "Спісы прайгравання", + "ButtonPlaylists": "Плэй-лісты", "ButtonPrevious": "Папярэдні", "ButtonPreviousChapter": "Папярэдні раздзел", "ButtonProbeAudioFile": "Праверыць аўдыяфайл", @@ -71,7 +71,7 @@ "ButtonQueueAddItem": "Дадаць у чаргу", "ButtonQueueRemoveItem": "Выдаліць з чаргі", "ButtonQuickEmbed": "Хуткае ўбудаванне", - "ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метададзеных", + "ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метаданых", "ButtonQuickMatch": "Хуткі пошук", "ButtonReScan": "Паўторнае сканаванне", "ButtonRead": "Чытаць", @@ -85,7 +85,7 @@ "ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне", "ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю", "ButtonReset": "Скінуць", - "ButtonResetToDefault": "Скінуць па змаўчанні", + "ButtonResetToDefault": "Скінуць да прадвызначаных", "ButtonRestore": "Аднавіць", "ButtonSave": "Захаваць", "ButtonSaveAndClose": "Захаваць і зачыніць", @@ -95,14 +95,14 @@ "ButtonScrollLeft": "Пракруціць улева", "ButtonScrollRight": "Пракруціць направа", "ButtonSearch": "Пошук", - "ButtonSelectFolderPath": "Выбраць шлях да тэчкі", + "ButtonSelectFolderPath": "Выбраць шлях да папкі", "ButtonSeries": "Серыі", - "ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў", + "ButtonSetChaptersFromTracks": "Задаць раздзелы з трэкаў", "ButtonShare": "Падзяліцца", "ButtonShiftTimes": "Карэкцыя часу", "ButtonShow": "Паказаць", "ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B", - "ButtonStartMetadataEmbed": "Пачаць убудаванне метададзеных", + "ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых", "ButtonStats": "Статыстыка", "ButtonSubmit": "Адправіць", "ButtonTest": "Тэст", @@ -115,27 +115,28 @@ "ButtonUserEdit": "Рэдагаваць карыстальніка {0}", "ButtonViewAll": "Прагледзець усе", "ButtonYes": "Так", - "ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метададзеных", - "ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара", - "ErrorUploadLacksTitle": "Павінна быць назва", + "ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метаданых", + "ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метаданыя – паспрабуйце абнавіць загаловак і/або аўтара", + "ErrorUploadLacksTitle": "Павінен быць загаловак", "HeaderAccount": "Уліковы запіс", - "HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метададзенных", + "HeaderAddCustomMetadataProvider": "Дадаванне карыстальніцкага пастаўшчыка метаданых", "HeaderAdvanced": "Дадаткова", "HeaderApiKeys": "API-ключы", "HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise", - "HeaderAudioTracks": "Аўдыядарожкі", + "HeaderAudioTracks": "Аўдыятрэкі", "HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг", "HeaderAuthentication": "Аўтэнтыфікацыя", "HeaderBackups": "Рэзервовыя копіі", + "HeaderBulkChapterModal": "Дадаць некалькі раздзелаў", "HeaderChangePassword": "Змяніць пароль", "HeaderChapters": "Раздзелы", - "HeaderChooseAFolder": "Выбраць тэчку", + "HeaderChooseAFolder": "Выберыце папку", "HeaderCollection": "Калекцыя", "HeaderCollectionItems": "Элементы калекцыі", "HeaderCover": "Вокладка", "HeaderCurrentDownloads": "Бягучыя спампоўкі", "HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе", - "HeaderCustomMetadataProviders": "Карыстальніцкія крыніцы метададзеных", + "HeaderCustomMetadataProviders": "Карыстальніцкія пастаўшчыкі метаданых", "HeaderDetails": "Падрабязнасці", "HeaderDownloadQueue": "Чарга спамповак", "HeaderEbookFiles": "Файлы электронных кніг", @@ -148,7 +149,7 @@ "HeaderFindChapters": "Знайсці раздзелы", "HeaderIgnoredFiles": "Ігнараваныя файлы", "HeaderItemFiles": "Файлы элементаў", - "HeaderItemMetadataUtils": "Утыліты для метададзеных элементаў", + "HeaderItemMetadataUtils": "Утыліты для метаданых элементаў", "HeaderLastListeningSession": "Апошні сеанс праслухоўвання", "HeaderLatestEpisodes": "Апошнія эпізоды", "HeaderLibraries": "Бібліятэкі", @@ -162,8 +163,8 @@ "HeaderManageTags": "Кіраванне тэгамі", "HeaderMapDetails": "Падрабязнасці адлюстравання", "HeaderMatch": "Супадзенне", - "HeaderMetadataOrderOfPrecedence": "Парадак прыярытэтнасці метададзеных", - "HeaderMetadataToEmbed": "Метададзеныя для ўбудавання", + "HeaderMetadataOrderOfPrecedence": "Парадак прыярытэту метаданых", + "HeaderMetadataToEmbed": "Метаданыя для ўбудавання", "HeaderNewAccount": "Новы ўліковы запіс", "HeaderNewApiKey": "Новы API-ключ", "HeaderNewLibrary": "Новая бібліятэка", @@ -178,8 +179,8 @@ "HeaderPermissions": "Дазволы", "HeaderPlayerQueue": "Чарга прайгравання", "HeaderPlayerSettings": "Налады прайгравальніка", - "HeaderPlaylist": "Спіс прайгравання", - "HeaderPlaylistItems": "Элементы спіса прайгравання", + "HeaderPlaylist": "Плэй-ліст", + "HeaderPlaylistItems": "Элементы плэй-ліста", "HeaderPodcastsToAdd": "Падкасты для дадання", "HeaderPresets": "Прадустаноўкі", "HeaderPreviewCover": "Прадпрагляд вокладкі", @@ -199,6 +200,7 @@ "HeaderSettingsExperimental": "Эксперыментальныя функцыі", "HeaderSettingsGeneral": "Агульныя", "HeaderSettingsScanner": "Сканер", + "HeaderSettingsSecurity": "Бяспека", "HeaderSettingsWebClient": "Вэб-кліент", "HeaderSleepTimer": "Таймер сну", "HeaderStatsLargestItems": "Найбуйнейшыя элементы", @@ -229,8 +231,8 @@ "LabelActivity": "Дзеянне", "LabelAddToCollection": "Дадаць у калекцыю", "LabelAddToCollectionBatch": "Дадаць {0} кніг у калекцыю", - "LabelAddToPlaylist": "Дадаць у спіс прайгравання", - "LabelAddToPlaylistBatch": "Дадаць {0} элементаў у спіс прайгравання", + "LabelAddToPlaylist": "Дадаць у плэй-ліст", + "LabelAddToPlaylistBatch": "Дадаць {0} элементаў у плэй-ліст", "LabelAddedAt": "Дата дабаўлення", "LabelAddedDate": "Дададзена {0}", "LabelAdminUsersOnly": "Толькі для адміністратараў", @@ -246,22 +248,22 @@ "LabelApiKeyUserDescription": "Гэты API-ключ будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.", "LabelApiToken": "Токен API", "LabelAppend": "Дадаць", - "LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)", - "LabelAudioChannels": "Аўдыёканалы (1 або 2)", - "LabelAudioCodec": "Аўдыёкодэк", + "LabelAudioBitrate": "Бітрэйт аўдыя (напрыклад, 128к)", + "LabelAudioChannels": "Аўдыяканалы (1 або 2)", + "LabelAudioCodec": "Аўдыякодэк", "LabelAuthor": "Аўтар", "LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)", "LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)", "LabelAuthors": "Аўтары", "LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", - "LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метададзеных", - "LabelAutoFetchMetadataHelp": "Атрыманне звестак пра назву, аўтара і серыю для падыходнага фарматавання перад загрузкай. Далей можа быць неабходна дапоўніць метададзеныя.", + "LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых", + "LabelAutoFetchMetadataHelp": "Атрыманне звестак пра загаловак, аўтара і серыю для спрашчэння запампоўвання. Пасля запампоўвання, магчыма, спатрэбіцца супаставіць дадатковыя метаданыя.", "LabelAutoLaunch": "Аўтазапуск", - "LabelAutoLaunchDescription": "Аўтаматычна перанакіроўваць да пастаўшчыка аўтэнтыфікацыі пры переходзе на старонку ўваходу (ручное пераключэнне праз шлях /login?autoLaunch=0)", + "LabelAutoLaunchDescription": "Аўтаматычна перанакіроўваць да пастаўшчыка аўтэнтыфікацыі пры пераходзе на старонку ўваходу (ручное пераключэнне праз шлях /login?autoLaunch=0)", "LabelAutoRegister": "Аўтарэгістрацыя", "LabelAutoRegisterDescription": "Аўтаматычна ствараць новых карыстальнікаў пасля ўваходу ў сістэму", "LabelBackToUser": "Вярнуцца да карыстальніка", - "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў", + "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыяфайлаў", "LabelBackupLocation": "Месцазнаходжанне рэзервовых копій", "LabelBackupsEnableAutomaticBackups": "Аўтаматычнае рэзервовае капіраванне", "LabelBackupsEnableAutomaticBackupsHelp": "Рэзервовыя копіі захаваныя ў /metadata/backups", @@ -273,11 +275,11 @@ "LabelBonus": "Бонус", "LabelBooks": "Кнігі", "LabelButtonText": "Тэкст кнопкі", - "LabelByAuthor": "ад {0}", + "LabelByAuthor": "– {0}", "LabelChangePassword": "Змяніць пароль", "LabelChannels": "Каналы", "LabelChapterCount": "{0} раздзелаў", - "LabelChapterTitle": "Назва раздзела", + "LabelChapterTitle": "Загаловак раздзела", "LabelChapters": "Раздзелы", "LabelChaptersFound": "раздзелаў знойдзена", "LabelClickForMoreInfo": "Націсніце для больш падрабязнай інфармацыі", @@ -294,8 +296,8 @@ "LabelContinueReading": "Працягнуць чытанне", "LabelContinueSeries": "Працягнуць серыі", "LabelCover": "Вокладка", - "LabelCoverImageURL": "URL выявы вокладкі", - "LabelCoverProvider": "Крыніца вокладак", + "LabelCoverImageURL": "URL-адрас відарыса вокладкі", + "LabelCoverProvider": "Пастаўшчык вокладак", "LabelCreatedAt": "Дата стварэння", "LabelCronExpression": "Запіс Cron", "LabelCurrent": "Бягучы", @@ -306,12 +308,13 @@ "LabelDeleteFromFileSystemCheckbox": "Выдаліць з файлавай сістэмы (зніміце галачку, каб выдаліць толькі з базы даных)", "LabelDescription": "Апісанне", "LabelDeselectAll": "Скасаваць выбар усяго", + "LabelDetectedPattern": "Выяўлены ўзор:", "LabelDevice": "Прылада", "LabelDeviceInfo": "Інфармацыя пра прыладу", "LabelDeviceIsAvailableTo": "Прылада даступная для...", "LabelDirectory": "Каталог", - "LabelDiscFromFilename": "Дыск з імя файла", - "LabelDiscFromMetadata": "Дыск па метададзеных", + "LabelDiscFromFilename": "Дыск з назвы файла", + "LabelDiscFromMetadata": "Дыск з метаданых", "LabelDiscover": "Знайсці", "LabelDownload": "Спампаваць", "LabelDownloadNEpisodes": "Спампована {0} эпізодаў", @@ -333,17 +336,19 @@ "LabelEmailSettingsTestAddress": "Тэставы адрас", "LabelEmbeddedCover": "Убудаваная вокладка", "LabelEnable": "Уключыць", - "LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:", - "LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.", + "LabelEncodingBackupLocation": "Рэзервовая копія арыгінальных аўдыяфайлаў будзе захавана ў:", + "LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудоўваюцца ў аўдыякнігі з некалькімі трэкамі.", "LabelEncodingClearItemCache": "Пераканайцеся, што перыядычна ачышчаеце кэш элементаў.", - "LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:", - "LabelEncodingInfoEmbedded": "Метададзеныя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.", + "LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу папку з аўдыякнігамі ў:", + "LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыятрэкі ўнутры папкі з аўдыякнігамі.", "LabelEncodingStartedNavigation": "Пасля запуску задачы вы можаце перайсці на іншую старонку.", "LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.", + "LabelEncodingWarningAdvancedSettings": "Увага: Не абнаўляйце гэтыя налады, калі вы не знаёмыя з параметрамі кадавання ffmpeg.", "LabelEnd": "Канец", "LabelEndOfChapter": "Канец раздзела", "LabelEpisode": "Эпізод", "LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай", + "LabelEpisodeTitle": "Загаловак эпізоду", "LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі", "LabelEpisodic": "Эпізадычны", "LabelExample": "Прыклад", @@ -356,23 +361,36 @@ "LabelExplicit": "Відверты", "LabelExportOPML": "Экспарт OPML", "LabelFeedURL": "URL стужкі", - "LabelFetchingMetadata": "Атрыманне метададзеных", + "LabelFetchingMetadata": "Атрыманне метаданых", "LabelFile": "Файл", "LabelFileBirthtime": "Час стварэння файла", "LabelFileModified": "Час змянення файла", - "LabelFilename": "Імя файла", - "LabelFinished": "Скончана", - "LabelFolder": "Тэчка", + "LabelFileModifiedDate": "Зменены {0}", + "LabelFilename": "Назва файла", + "LabelFinished": "Завершана", + "LabelFinishedDate": "Завершана {0}", + "LabelFolder": "Папка", + "LabelFolders": "Папкі", + "LabelFontBold": "Тоўсты", "LabelFontBoldness": "Таўшчыня шрыфта", + "LabelFontFamily": "Сямейства шрыфтоў", + "LabelFontItalic": "Курсіў", "LabelFontScale": "Памер шрыфту", + "LabelFontStrikethrough": "Перакрэслены", + "LabelFormat": "Фармат", + "LabelFull": "Поўны", "LabelGenre": "Жанр", "LabelGenres": "Жанры", "LabelHasEbook": "Мае электронную кнігу", "LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу", "LabelHideSubtitles": "Схаваць падзагалоўкі", + "LabelHighestPriority": "Найвышэйшы прыярытэт", "LabelHost": "Хост", - "LabelImageURLFromTheWeb": "URL выявы з інтэрнэту", + "LabelHour": "Гадзіна", + "LabelIcon": "Значок", + "LabelImageURLFromTheWeb": "URL-адрас відарыса з інтэрнэту", "LabelInProgress": "У працэсе", + "LabelIncludeInTracklist": "Уключыць у спіс трэкаў", "LabelIncomplete": "Незавершана", "LabelIntervalCustomDailyWeekly": "Карыстальніцкі штодзённы/штотыднёвы", "LabelIntervalEvery12Hours": "Кожныя 12 гадзін", @@ -386,10 +404,11 @@ "LabelInvert": "Інвертаваць", "LabelItem": "Элемент", "LabelLanguage": "Мова", - "LabelLanguageDefaultServer": "Мова сервера па змаўчанні", + "LabelLanguageDefaultServer": "Прадвызначаная мова сервера", "LabelLanguages": "Мовы", "LabelLastBookAdded": "Апошняя дададзеная кніга", "LabelLastBookUpdated": "Апошняя абноўленая кніга", + "LabelLastProgressDate": "Апошні прагрэс: {0}", "LabelLastSeen": "Апошні прагляд", "LabelLastTime": "Апошні раз", "LabelLastUpdate": "Апошняе абнаўленне", @@ -401,40 +420,63 @@ "LabelLibrary": "Бібліятэка", "LabelLibraryFilterSublistEmpty": "Не {0}", "LabelLibraryItem": "Элемент бібліятэкі", - "LabelLibraryName": "Імя бібліятэкі", - "LabelLibrarySortByProgress": "Прагрэс абноўлены", + "LabelLibraryName": "Назва бібліятэкі", + "LabelLibrarySortByProgress": "Прагрэс: апошняе абнаўленне", + "LabelLibrarySortByProgressFinished": "Прагрэс: завершана", + "LabelLibrarySortByProgressStarted": "Прагрэс: пачата", "LabelLimit": "Абмежаванне", "LabelLineSpacing": "Міжрадковы інтэрвал", "LabelListenAgain": "Паслухаць зноў", + "LabelLogLevelDebug": "Debug", + "LabelLogLevelInfo": "Info", + "LabelLogLevelWarn": "Warn", + "LabelLowestPriority": "Найніжэйшы прыярытэт", + "LabelMatchExistingUsersByDescription": "Выкарыстоўваецца для падключэння існуючых карыстальнікаў. Пасля падключэння карыстальнікі будуць супастаўляцца з дапамогай унікальнага ідэнтыфікатара ад пастаўшчыка SSO", "LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.", "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку", "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.", "LabelMediaPlayer": "Медыяпрайгравальнік", "LabelMediaType": "Тып медыя", - "LabelMetadataOrderOfPrecedenceDescription": "Крыніцы метададзеных з вышэйшым прыярытэтам будуць замяняць крыніцы з ніжэйшым прыярытэтам", - "LabelMetadataProvider": "Пастаўшчык метададзеных", + "LabelMetadataOrderOfPrecedenceDescription": "Крыніцы метаданых з вышэйшым прыярытэтам будуць замяняць крыніцы з ніжэйшым прыярытэтам", + "LabelMetadataProvider": "Пастаўшчык метаданых", "LabelMissing": "Адсутнічае", "LabelMore": "Больш", "LabelMoreInfo": "Больш інфармацыі", - "LabelName": "Імя", + "LabelName": "Назва", "LabelNarrator": "Чытальнік", "LabelNarrators": "Чытальнікі", "LabelNewestAuthors": "Новыя аўтары", "LabelNewestEpisodes": "Новыя эпізоды", - "LabelNoCustomMetadataProviders": "Няма карыстацкіх пастаўшчыкоў метададзеных", - "LabelNotFinished": "Не скончана", + "LabelNextChapters": "Наступныя раздзелы:", + "LabelNextScheduledRun": "Наступны запланаваны запуск", + "LabelNoApiKeys": "Няма ключоў API", + "LabelNoCustomMetadataProviders": "Няма карыстальніцкіх пастаўшчыкоў метаданых", + "LabelNotFinished": "Не завершана", "LabelNotStarted": "Не пачата", + "LabelNotificationAvailableVariables": "Даступныя пераменныя", + "LabelNotificationTitleTemplate": "Шаблон загалоўка", "LabelNotificationsMaxFailedAttemptsHelp": "Апавяшчэнні адключаюцца пасля таго, як не ўдаецца іх адправіць гэтулькі разоў", + "LabelNumberOfBooks": "Колькасць кніг", + "LabelNumberOfChapters": "Колькасць раздзелаў:", "LabelNumberOfEpisodes": "# з эпізодаў", "LabelOpenRSSFeed": "Адкрыць RSS-стужку", + "LabelOverwrite": "Перазапісаць", + "LabelPaginationPageXOfY": "Старонка {0} з {1}", "LabelPassword": "Пароль", "LabelPath": "Шлях", "LabelPermissionsDownload": "Можна спампаваць", - "LabelPlaylists": "Cпісs прайгравання", + "LabelPhotoPathURL": "Шлях/URL-адрас фота", + "LabelPlayerChapterNumberMarker": "{0} з {1}", + "LabelPlaylists": "Плэй-лісты", "LabelPodcast": "Падкаст", + "LabelPodcastSearchRegion": "Рэгіён пошуку падкастаў", + "LabelPodcastType": "Тып падкаста", "LabelPodcasts": "Падкасты", + "LabelPort": "Порт", "LabelPreventIndexing": "Прадухіліць індэксацыю вашай стужкі каталогамі падкастаў iTunes і Google", "LabelProgress": "Прагрэс", + "LabelProvider": "Пастаўшчык", + "LabelProviderAuthorizationValue": "Значэнне загалоўка аўтарызацыі", "LabelPubDate": "Дата публікацыі", "LabelPublishYear": "Год публікацыі", "LabelPublishedDate": "Апублікавана {0}", @@ -448,13 +490,17 @@ "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць", "LabelRead": "Чытаць", "LabelReadAgain": "Чытаць зноў", + "LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу", "LabelRecentSeries": "Апошнія серыі", "LabelRecentlyAdded": "Нядаўна дададзеныя", + "LabelRecommended": "Рэкамендаваныя", + "LabelRedo": "Узнавіць", + "LabelRegion": "Рэгіён", "LabelRemoveAllMetadataAbs": "Выдаліць усе файлы metadata.abs", "LabelRemoveAllMetadataJson": "Выдаліць усе файлы metadata.json", "LabelRemoveCover": "Выдаліць вокладку", - "LabelRemoveMetadataFile": "Выдаліць файлы метададзеных у тэчках элементаў бібліятэкі", - "LabelRemoveMetadataFileHelp": "Выдаліць усе файлы metadata.json і metadata.abs у вашых {0} тэчках.", + "LabelRemoveMetadataFile": "Выдаліць файлы метаданых у папках элементаў бібліятэкі", + "LabelRemoveMetadataFileHelp": "Выдаліць усе файлы metadata.json і metadata.abs у {0} папках.", "LabelRowsPerPage": "Радкоў на старонку", "LabelSearchTerm": "Пошукавы запыт", "LabelSearchTitle": "Пошук па загалоўку", @@ -464,6 +510,7 @@ "LabelSelectAll": "Выбраць усё", "LabelSelectAllEpisodes": "Выбраць усе эпізоды", "LabelSelectEpisodesShowing": "Выбраць {0} эпізодаў для паказу", + "LabelSelectUser": "Выберыце карыстальніка", "LabelSelectUsers": "Выбраць карыстальнікаў", "LabelSendEbookToDevice": "Адправіць электронную кнігу на...", "LabelSequence": "Паслядоўнасць", @@ -477,40 +524,48 @@ "LabelSetEbookAsSupplementary": "Зрабіць дадатковым", "LabelSettingsAllowIframe": "Дазволіць убудоўванне ў iframe", "LabelSettingsAudiobooksOnly": "Толькі аўдыякнігі", - "LabelSettingsAudiobooksOnlyHelp": "Уключэнне гэтай налады будзе ігнараваць файлы электронных кніг, калі толькі яны не знаходзяцца ў тэчцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.", + "LabelSettingsAudiobooksOnlyHelp": "Пры ўключэнні гэтай налады файлы электронных кніг будуць ігнаравацца, калі толькі яны не знаходзяцца ў папцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.", "LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі", - "LabelSettingsEnableWatcherForLibrary": "Аўтаматычна правяраць бібліятэку на змены", + "LabelSettingsDateFormat": "Фарматы даты", + "LabelSettingsEnableWatcherForLibrary": "Аўтаматычна сачыць за зменамі ў бібліятэцы", "LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера", "LabelSettingsEpubsAllowScriptedContent": "Дазволіць скрыптавы кантэнт у EPUB", "LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць EPUB-файлам выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы EPUB-файлаў.", "LabelSettingsExperimentalFeatures": "Эксперыментальныя функцыі", "LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.", "LabelSettingsFindCovers": "Знайсці вокладкі", - "LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або выявы вокладкі ў тэчцы, сканер паспрабуе знайсці вокладку.
Заўвага: гэта павялічыць час сканавання", + "LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або відарыса вокладкі ў папцы, сканер паспрабуе знайсці вокладку.
Заўвага: гэта павялічыць час сканіравання", "LabelSettingsHideSingleBookSeries": "Схаваць серыі з адной кнігай", "LabelSettingsHideSingleBookSeriesHelp": "Серыі, якія змяшчаюць толькі адну кнігу, будуць схаваны са старонкі серый і паліц на галоўнай старонцы.", "LabelSettingsHomePageBookshelfView": "Галоўная старонка выкарыстоўвае выгляд кніжнай паліцы", "LabelSettingsLibraryBookshelfView": "Бібліятэка выкарыстоўвае выгляд кніжнай паліцы", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Працэнт завяршэння большы за", "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунды)", "LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як скончаны, калі", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусціць папярэднія кнігі ў \"Працягнуць серыю\"", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Палка \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.", - "LabelSettingsParseSubtitles": "Разабраць падзагалоўкі", - "LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў тэчак аўдыякніг.
Падзагаловак павінен быць аддзелены знакам \" - \".
Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"", - "LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метададзеным", - "LabelSettingsPreferMatchedMetadataHelp": "Супадаючыя дадзеныя будуць замяняць дэталі элемента пры выкарыстанні функцыі Хуткі пошук. Па змаўчанні Хуткі пошук запаўняе толькі адсутныя дэталі.", - "LabelSettingsStoreCoversWithItemHelp": "Па змаўчанні вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у тэчцы элемента вашай бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"", - "LabelSettingsStoreMetadataWithItem": "Захоўваць метададзеныя разам з элементам", - "LabelSettingsStoreMetadataWithItemHelp": "Па змаўчанні метададзеныя захоўваюцца ў /metadata/items. Уключэнне гэтай опцыі забяспечыць захоўванне файлаў метададзеных у тэчках элементаў вашай бібліятэкі", + "LabelSettingsParseSubtitles": "Аналізаваць падзагалоўкі", + "LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў папак аўдыякніг.
Падзагаловак павінен быць аддзелены сімвалам \" - \".
Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"", + "LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метаданым", + "LabelSettingsPreferMatchedMetadataHelp": "Супадаючыя даныя будуць замяняць звесткі элемента пры выкарыстанні функцыі Хуткі пошук. Прадвызначана Хуткі пошук запаўняе толькі адсутныя звесткі.", + "LabelSettingsSortingIgnorePrefixesHelp": "напрыклад, для прэфікса \"the\" загаловак кнігі \"The Book Title\" будзе сартавацца як \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Выкарыстоўваць квадратныя вокладкі кніг", + "LabelSettingsStoreCoversWithItemHelp": "Прадвызначана вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у папцы элемента бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"", + "LabelSettingsStoreMetadataWithItem": "Захоўваць метаданыя разам з элементам", + "LabelSettingsStoreMetadataWithItemHelp": "Прадвызначана метаданыя захоўваюцца ў /metadata/items. Пры ўключэнні гэтай опцыі файлаў метаданых будуць захоўвацца ў папках элементаў бібліятэкі", "LabelSettingsTimeFormat": "Фармат часу", + "LabelShare": "Абагуліць", "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.", "LabelShowAll": "Паказаць усё", + "LabelShowSeconds": "Паказваць секунды", "LabelShowSubtitles": "Паказаць падзагалоўкі", "LabelSize": "Памер", "LabelSleepTimer": "Таймер сну", + "LabelSortAscending": "Па ўзрастанні", + "LabelSortDescending": "Па ўбыванні", "LabelStart": "Пачаць", "LabelStartTime": "Час пачатку", - "LabelStatsAudioTracks": "Аўдыядарожкі", + "LabelStatsAudioTracks": "Аўдыятрэкі", "LabelStatsAuthors": "Аўтары", "LabelStatsBestDay": "Лепшы дзень", "LabelStatsDailyAverage": "У сярэднім за дзень", @@ -518,7 +573,7 @@ "LabelStatsDaysListened": "Дзён праслухана", "LabelStatsHours": "Гадзін", "LabelStatsInARow": "без перапынку", - "LabelStatsItemsFinished": "Скончаныя элементы", + "LabelStatsItemsFinished": "Элементаў завершана", "LabelStatsItemsInLibrary": "Элементаў у бібліятэцы", "LabelStatsMinutes": "хвілін", "LabelStatsMinutesListening": "Хвілін праслухоўвання", @@ -539,6 +594,7 @@ "LabelTheme": "Тэма", "LabelThemeDark": "Цёмная", "LabelThemeLight": "Светлая", + "LabelThemeSepia": "Сепія", "LabelTimeBase": "Часавая база", "LabelTimeDurationXHours": "{0} гадзін", "LabelTimeDurationXMinutes": "{0} хвілін", @@ -549,21 +605,22 @@ "LabelTimeListenedToday": "Час праслухоўвання сёння", "LabelTimeRemaining": "Засталося {0}", "LabelTimeToShift": "Час зрушэння ў секундах", - "LabelTitle": "Назва", - "LabelToolsEmbedMetadata": "Убудаваць метададзеныя", - "LabelToolsEmbedMetadataDescription": "Убудаваць метададзеныя ў аўдыёфайлы, уключаючы выяву вокладкі і раздзелы.", - "LabelToolsMakeM4bDescription": "Стварыць аўдыёкнігу ў фармаце .M4B з убудаванымі метададзенымі, выявай вокладкі і раздзеламі.", - "LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метададзенымі, выявай вокладкі і раздзеламі.", + "LabelTitle": "Загаловак", + "LabelToolsEmbedMetadata": "Убудаваць метаданыя", + "LabelToolsEmbedMetadataDescription": "Убудаваць метаданыя ў аўдыяфайлы, уключаючы відарыс вокладкі і раздзелы.", + "LabelToolsM4bEncoder": "Кадавальнік M4B", + "LabelToolsMakeM4bDescription": "Стварыць аўдыякнігу ў фармаце .M4B з убудаванымі метаданымі, відарысам вокладкі і раздзеламі.", + "LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метаданымі, відарысам вокладкі і раздзеламі.", "LabelTotalDuration": "Агульная працягласць", "LabelTotalTimeListened": "Агульны час праслухоўвання", - "LabelTrackFromFilename": "Дарожка з імя файла", - "LabelTrackFromMetadata": "Дарожка з метададзеных", - "LabelTracks": "Дарожкі", - "LabelTracksMultiTrack": "Шматдарожкавы", - "LabelTracksNone": "Няма дарожак", - "LabelTracksSingleTrack": "Аднадарожкавы", + "LabelTrackFromFilename": "Трэк з назвы файла", + "LabelTrackFromMetadata": "Трэк з метаданых", + "LabelTracks": "Трэкі", + "LabelTracksMultiTrack": "Некалькі трэкаў", + "LabelTracksNone": "Няма трэкаў", + "LabelTracksSingleTrack": "Адзін трэк", "LabelType": "Тып", - "LabelUndo": "Адмяніць", + "LabelUndo": "Адрабіць", "LabelUnknown": "Невядома", "LabelUnknownPublishDate": "Невядомая дата публікацыі", "LabelUpdateCover": "Абнавіць вокладку", @@ -571,13 +628,13 @@ "LabelUpdateDetails": "Абнавіць падрабязнасці", "LabelUpdateDetailsHelp": "Дазволіць замену існуючых падрабязнасцей для выбраных кніг пры выяўленні адпаведнасці", "LabelUpdatedAt": "Абноўлена ў", - "LabelUploaderDragAndDrop": "Перацягвайце і скідайце файлы або тэчкі", + "LabelUploaderDragAndDrop": "Перацягніце файлы або папкі", "LabelUploaderDragAndDropFilesOnly": "Перацягвайце і скідайце файлы", "LabelUploaderDropFiles": "Скідайце файлы", - "LabelUploaderItemFetchMetadataHelp": "Аўтаматычна атрымліваць назву, аўтара і серыю", + "LabelUploaderItemFetchMetadataHelp": "Аўтаматычна атрымліваць загаловак, аўтара і серыю", "LabelUseAdvancedOptions": "Выкарыстоўваць пашыраныя параметры", - "LabelUseChapterTrack": "Выкарыстоўваць дарожку раздзелаў", - "LabelUseFullTrack": "Выкарыстоўваць поўную дарожку", + "LabelUseChapterTrack": "Выкарыстоўваць трэк раздзела", + "LabelUseFullTrack": "Выкарыстоўваць увесь трэк", "LabelUser": "Карыстальнік", "LabelUsername": "Імя карыстальніка", "LabelValue": "Значэнне", @@ -587,8 +644,8 @@ "LabelViewPlayerSettings": "Праглядзець налады прайгравальніка", "LabelViewQueue": "Праглядзець чаргу прайгравальніка", "LabelVolume": "Гучнасць", - "LabelWebRedirectURLsDescription": "Аўтарызуйце гэтыя URL у вашым OAuth-правайдары для перанакіравання ў вэб-дадатак пасля ўваходу:", - "LabelWebRedirectURLsSubfolder": "Падтэчка для URL-перанакіраванняў", + "LabelWebRedirectURLsDescription": "Аўтарызуйце гэтыя URL-адрасы ў вашым пастаўшчыку OAuth для перанакіравання ў вэб-праграму пасля ўваходу:", + "LabelWebRedirectURLsSubfolder": "Падпапка для URL-адрасоў перанакіравання", "LabelWeekdaysToRun": "Дні тыдня для запуску", "LabelXBooks": "{0} кніг", "LabelXItems": "{0} элементаў", @@ -596,80 +653,115 @@ "LabelYearReviewShow": "Азнаёміцца з вынікамі года", "LabelYourAudiobookDuration": "Працягласць вашай аўдыякнігі", "LabelYourBookmarks": "Вашы закладкі", - "LabelYourPlaylists": "Вашы спісы прайгравання", + "LabelYourPlaylists": "Вашы плэй-лісты", "LabelYourProgress": "Ваш прагрэс", "MessageAddToPlayerQueue": "Дадаць у чаргу прайгравальніка", "MessageAppriseDescription": "Каб выкарыстоўваць гэтую функцыю, вам спатрэбіцца запусціць асобнік Apprise API або API, які будзе апрацоўваць тыя ж запыты.
URL Apprise API павінен быць поўным шляхам для адпраўкі апавяшчэння, напрыклад, калі ваш API працуе па адрасе http://192.168.1.1:8337, то вы павінны ўвесці http://192.168.1.1:8337/notify.", - "MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і выявы, якія захоўваюцца ў /metadata/items і /metadata/authors. Рэзервовыя копіі не ўключаюць файлы, якія захоўваюцца ў вашых тэчках бібліятэкі.", + "MessageAuthenticationOIDCChangesRestart": "Перазапусціце сервер пасля захавання, каб прымяніць змены OIDC.", + "MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і відарысы, якія захоўваюцца ў /metadata/items і /metadata/authors. Рэзервовыя копіі не ўключаюць файлы, якія захоўваюцца ў папках бібліятэкі.", "MessageBackupsLocationEditNote": "Заўвага: Абнаўленне месцазнаходжання рэзервовых копій не перамяшчае і не змяняе існуючыя рэзервовыя копіі", "MessageBackupsLocationNoEditNote": "Заўвага: Месцазнаходжанне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.", "MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым", - "MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі дадзенымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны", - "MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты дадзенымі з гэтага элемента", - "MessageBatchQuickMatchDescription": "Хуткі пошук паспрабуе дадаць адсутныя вокладкі і метададзеныя для выбраных элементаў. Уключыце ніжэй выкладзеныя опцыі, каб дазволіць Хуткаму пошуку замяняць існуючыя вокладкі і/або метададзеныя.", + "MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі данымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны", + "MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты данымі з гэтага элемента", + "MessageBatchQuickMatchDescription": "Хуткі пошук паспрабуе дадаць адсутныя вокладкі і метаданыя для выбраных элементаў. Уключыце параметры ніжэй, каб дазволіць Хуткаму пошуку замяняць існуючыя вокладкі і/або метаданыя.", + "MessageBookshelfNoCollections": "Вы пакуль не стварылі ніводнай калекцый", "MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак", + "MessageBookshelfNoResultsForFilter": "Няма вынікаў для фільтра \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "Няма вынікаў па запыце", + "MessageChapterErrorFirstNotZero": "Першы раздзел павінен пачынацца з 0", "MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі", "MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела", + "MessageChaptersNotFound": "Раздзелы не знойдзены", + "MessageCheckingCron": "Праверка cron...", "MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?", - "MessageConfirmDeleteMetadataProvider": "Ці ўпэўненыя вы, што жадаеце выдаліць карыстацкага пастаўшчыка метададзеных \"{0}\"?", - "MessageConfirmEmbedMetadataInAudioFiles": "Ці ўпэўненыя вы, што жадаеце ўбудаваць метададзеныя ў {0} аўдыёфайлаў?", - "MessageConfirmPurgeCache": "Ачышчэнне кэша выдаліць увесь каталог па адрасе /metadata/cache.

Ці сапраўды вы жадаеце выдаліць каталог кэша?", - "MessageConfirmPurgeItemsCache": "Ачышчэнне кэша элементаў выдаліць увесь каталог па адрасе /metadata/cache/items.
Вы ўпэўнены?", + "MessageConfirmDeleteApiKey": "Вы ўпэўнены, што хочаце выдаліць ключ API \"{0}\"?", + "MessageConfirmDeleteBackup": "Вы ўпэўнены, што хочаце выдаліць рэзервовую копію для {0}?", + "MessageConfirmDeleteFile": "Будзе выдалены файл з файлавай сістэмы. Вы ўпэўнены?", + "MessageConfirmDeleteLibrary": "Вы ўпэўнены, што хочаце назаўжды выдаліць бібліятэку \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Будзе выдалены элемент бібліятэкі з базы даных і файлавай сістэмы. Вы ўпэўнены?", + "MessageConfirmDeleteLibraryItems": "Будзе выдалена {0} элементаў бібліятэкі з базы даных і файлавай сістэмы. Вы ўпэўнены?", + "MessageConfirmDeleteMetadataProvider": "Вы ўпэўнены, што хочаце выдаліць карыстальніцкага пастаўшчыка метаданых \"{0}\"?", + "MessageConfirmDeleteNotification": "Вы ўпэўнены, што хочаце выдаліць гэта апавяшчэнне?", + "MessageConfirmDeleteSession": "Вы ўпэўнены, што хочаце выдаліць гэты сеанс?", + "MessageConfirmEmbedMetadataInAudioFiles": "Вы ўпэўнены, што хочаце ўбудаваць метаданыя ў {0} аўдыяфайлаў?", + "MessageConfirmPurgeCache": "Ачышчэнне кэшу выдаліць увесь каталог па адрасе /metadata/cache.

Вы сапраўды хочаце выдаліць каталог кэшу?", + "MessageConfirmPurgeItemsCache": "Ачышчэнне кэшу элементаў выдаліць увесь каталог па адрасе /metadata/cache/items.
Вы ўпэўнены?", "MessageConfirmQuickMatchEpisodes": "Хуткае супадзенне эпізодаў перазапіша дэталі, калі супадзенне будзе знойдзена. Будуць абноўлены толькі эпізоды, якія не супадаюць. Вы ўпэўнены?", + "MessageConfirmRemoveAllChapters": "Вы ўпэўнены, што хочаце выдаліць усе раздзелы?", + "MessageConfirmRemoveAuthor": "Вы ўпэўнены, што хочаце выдаліць аўтара \"{0}\"?", + "MessageConfirmRemoveCollection": "Вы ўпэўнены, што хочаце выдаліць калекцыю \"{0}\"?", "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?", - "MessageConfirmRemoveMetadataFiles": "Ці ўпэўненыя вы, што жадаеце выдаліць усе файлы метададзеных{0} у тэчках элементаў вашай бібліятэкі?", - "MessageConfirmRemovePlaylist": "Вы ўпэўненыя, што жадаеце выдаліць свой спіс прайгравання \"{0}\"?", + "MessageConfirmRemoveMetadataFiles": "Вы ўпэўнены, што хочаце выдаліць усе файлы metadata.{0} у папках элементаў бібліятэкі?", + "MessageConfirmRemovePlaylist": "Вы ўпэўнены, што хочаце выдаліць плэй-ліст \"{0}\"?", + "MessageConfirmRenameGenre": "Вы ўпэўнены, што хочаце перайменаваць жанр \"{0}\" на \"{1}\" для ўсіх элементаў?", + "MessageConfirmRenameGenreMergeNote": "Заўвага: Гэты жанр ужо існуе, таму яны будуць аб'яднаны.", + "MessageConfirmRenameGenreWarning": "Увага! Падобны жанр з іншым рэгістрам літар ужо існуе — \"{0}\".", + "MessageConfirmRenameTag": "Вы ўпэўнены, што хочаце перайменаваць тэг \"{0}\" на \"{1}\" для ўсіх элементаў?", + "MessageConfirmRenameTagMergeNote": "Заўвага: Гэты тэг ужо існуе, таму яны будуць аб'яднаны.", + "MessageConfirmRenameTagWarning": "Увага! Падобны тэг з іншым рэгістрам ужо існуе: \"{0}\".", + "MessageConfirmResetProgress": "Вы ўпэўнены, што хочаце скінуць свой прагрэс?", "MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?", "MessageDownloadingEpisode": "Спампоўка эпізоду", - "MessageEmbedQueue": "У чарзе на ўбудаванне метададзеных (у чарзе {0})", + "MessageDragFilesIntoTrackOrder": "Перацягніце файлы ў правільным парадку трэкаў", + "MessageEmbedQueue": "У чарзе на ўбудаванне метаданых (у чарзе {0})", "MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", "MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.", "MessageFeedURLWillBe": "URL стужкі будзе {0}", "MessageFetching": "Атрыманне...", + "MessageImportantNotice": "Важная заўвага!", "MessageInvalidAsin": "Няправільны ASIN", "MessageItemsUpdated": "{0} элементаў абноўлена", + "MessageJoinUsOn": "Далучайцеся да нас у", "MessageLoading": "Загрузка...", + "MessageLoadingFolders": "Загрузка папак...", "MessageLogsDescription": "Журналы захоўваюцца ў каталогу /metadata/logs у фармаце JSON. Журналы памылак захоўваюцца ў файле /metadata/logs/crashlogs.txt.", - "MessageMapChapterTitles": "Супаставіць назвы раздзелаў з вашымі існуючымі раздзеламі аўдыякнігі без змянення часовых метак", - "MessageMarkAsFinished": "Пазначыць як скончана", + "MessageMapChapterTitles": "Супаставіць загалоўкі раздзелаў з існуючымі раздзеламі аўдыякнігі без змянення пазнак часу", + "MessageMarkAsFinished": "Пазначыць як завершаную", + "MessageMatchBooksDescription": "паспрабуе параўнаць кнігі ў бібліятэцы з кнігай ад выбранай пошукавай сістэмы і запоўніць пустыя палі і вокладку. Не перазапісвае звесткі.", + "MessageNoAudioTracks": "Няма аўдыятрэкаў", "MessageNoBookmarks": "Няма закладак", "MessageNoChapters": "Няма раздзелаў", "MessageNoCollections": "Няма калекцый", - "MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак", + "MessageNoDownloadsInProgress": "Зараз няма актыўных спампованняў", "MessageNoDownloadsQueued": "Няма спамповак у чарзе", "MessageNoItems": "Няма элементаў", "MessageNoItemsFound": "Элементы не знойдзены", "MessageNoListeningSessions": "Няма сеансаў праслухоўвання", + "MessageNoLogs": "Няма журналаў", "MessageNoMediaProgress": "Няма прагрэсу медыя", "MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі", "MessageNoPodcastsFound": "Падкасты не знойдзены", "MessageNoTasksRunning": "Няма запушчаных задач", "MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся", - "MessageNoUserPlaylists": "У вас няма спісаў прайгравання", - "MessageNoUserPlaylistsHelp": "Спісы прайгравання прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.", - "MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага файла OPML. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.", - "MessagePlaylistCreateFromCollection": "Стварыць спіс прайгравання з калекцыі", + "MessageNoUserPlaylists": "У вас няма плэй-лістоў", + "MessageNoUserPlaylistsHelp": "Плэй-лісты прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.", + "MessageOpmlPreviewNote": "Заўвага: гэта перадпрагляд прааналізаванага файла OPML. Фактычны загаловак падкаста будзе ўзяты з RSS-ленты.", + "MessagePauseChapter": "Прыпыніць прайграванне раздзела", + "MessagePlaylistCreateFromCollection": "Стварыць плэй-ліст з калекцыі", "MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення", "MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі", - "MessageQuickMatchDescription": "Запоўніць пустыя дэталі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе дэталі, калі опцыя «Аддаваць перавагу супадаючым метададзеным» на серверы не ўключана.", + "MessageQuickEmbedInProgress": "Выконваецца хуткае ўбудаванне", + "MessageQuickMatchDescription": "Запоўніць пустыя звесткі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе звесткіі, калі параметр \"Аддаваць перавагу супадаючым метаданым\" на серверы не ўключана.", "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на", - "MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама выявы вокладкі ў /metadata/items і /metadata/authors.

Рэзервовыя копіі не змяняюць файлы ў вашых тэчках бібліятэкі. Калі вы ўключылі наладкі сервера для захоўвання воклак і метададзеных у тэчках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца.

Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.", + "MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама відарысы вокладкі ў /metadata/items і /metadata/authors.

Рэзервовыя копіі не змяняюць файлы ў папках бібліятэкі. Калі вы ўключылі налады сервера для захоўвання воклак і метаданых у папках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца.

Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.", "MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}", + "MessageSetChaptersFromTracksDescription": "Задаць раздзелы, выкарыстоўваючы кожны аўдыяфайл у якасці раздзела і назву аўдыяфайла ў якасці загалоўка раздзела", "MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?", - "MessageTaskAudioFileNotWritable": "Аўдыёфайл \"{0}\" недаступны для запісу", + "MessageTaskAudioFileNotWritable": "Аўдыяфайл \"{0}\" недаступны для запісу", "MessageTaskCanceledByUser": "Задача скасавана карыстальнікам", "MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"", - "MessageTaskEmbeddingMetadata": "Убудаванне метададзеных", - "MessageTaskEmbeddingMetadataDescription": "Убудаванне метададзеных у аўдыёкнігу \"{0}\"", + "MessageTaskEmbeddingMetadata": "Убудаванне метаданых", + "MessageTaskEmbeddingMetadataDescription": "Убудаванне метаданых у аўдыякнігу \"{0}\"", "MessageTaskEncodingM4b": "Кадаванне M4B", "MessageTaskEncodingM4bDescription": "Кадаванне аўдыякнігі \"{0}\" у адзін файл m4b", "MessageTaskFailed": "Не ўдалося", - "MessageTaskFailedToBackupAudioFile": "Не ўдалося зрабіць рэзервовую копію аўдыёфайла \"{0}\"", - "MessageTaskFailedToCreateCacheDirectory": "Не ўдалося стварыць каталог кэша", - "MessageTaskFailedToEmbedMetadataInFile": "Не ўдалося ўбудаваць метададзеныя ў файл \"{0}\"", - "MessageTaskFailedToMergeAudioFiles": "Не ўдалося аб’яднаць аўдыёфайлы", + "MessageTaskFailedToBackupAudioFile": "Не ўдалося зрабіць рэзервовую копію аўдыяфайла \"{0}\"", + "MessageTaskFailedToCreateCacheDirectory": "Не ўдалося стварыць каталог кэшу", + "MessageTaskFailedToEmbedMetadataInFile": "Не ўдалося ўбудаваць метаданыя ў файл \"{0}\"", + "MessageTaskFailedToMergeAudioFiles": "Не ўдалося аб’яднаць аўдыяфайлы", "MessageTaskFailedToMoveM4bFile": "Не ўдалося перамясціць файл m4b", - "MessageTaskFailedToWriteMetadataFile": "Не ўдалося захаваць файл метададзеных", + "MessageTaskFailedToWriteMetadataFile": "Не ўдалося захаваць файл метаданых", "MessageTaskMatchingBooksInLibrary": "Пошук супадзенняў кніг у бібліятэцы \"{0}\"", "MessageTaskNoFilesToScan": "Няма файлаў для сканавання", "MessageTaskOpmlImport": "Імпарт OPML", @@ -681,8 +773,8 @@ "MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху", "MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст", "MessageTaskOpmlImportFinished": "Дададзена {0} падкастаў", - "MessageTaskOpmlParseFailed": "Не ўдалося разабраць файл OPML", - "MessageTaskOpmlParseFastFail": "Неправільны файл OPML: тэг не знойдзены АБО тэг не знойдзены", + "MessageTaskOpmlParseFailed": "Не ўдалося прааналізаваць файл OPML", + "MessageTaskOpmlParseFastFail": "Памылковы файл OPML: тэг не знойдзены АБО тэг не знойдзены", "MessageTaskOpmlParseNoneFound": "У файле OPML не знойдзена стужак", "MessageTaskScanItemsAdded": "{0} дададзена", "MessageTaskScanItemsMissing": "{0} адсутнічае", @@ -694,32 +786,80 @@ "NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.", "NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", - "NoteUploaderFoldersWithMediaFiles": "Тэчкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.", + "NoteUploaderFoldersWithMediaFiles": "Папкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.", "NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца", - "PlaceholderNewPlaylist": "Імя новага спіса прайгравання", + "NotificationOnTestDescription": "Падзея для тэсціравання сістэмы апавяшчэнняў", + "PlaceholderBulkChapterInput": "Увядзіце загаловак раздзела або выкарыстоўвайце нумарацыю (напрыклад, «Эпізод 1», «Раздзел 10», «1.»)", + "PlaceholderNewCollection": "Назва новай калекцыі", + "PlaceholderNewFolderPath": "Шлях да новай папкі", + "PlaceholderNewPlaylist": "Назва новага плэй-ліста", + "PlaceholderSearch": "Пошук..", + "StatsAuthorsAdded": "дададзена аўтараў", + "StatsBooksAdded": "дададзена кніг", "StatsBooksFinished": "кнігі скончаны", "StatsBooksFinishedThisYear": "Некаторыя кнігі скончаны ў гэтым годзе…", "StatsBooksListenedTo": "кнігі, якія былі праслуханы", "StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…", + "StatsSessions": "сеансаў", "ToastAccountUpdateSuccess": "Уліковы запіс абноўлены", - "ToastAuthorImageRemoveSuccess": "Выява аўтара выдалена", + "ToastAsinRequired": "ASIN абавязковы", + "ToastAuthorImageRemoveSuccess": "Відарыс аўтара выдалены", + "ToastAuthorNotFound": "Аўтар \"{0}\" не знойдзены", + "ToastAuthorRemoveSuccess": "Аўтар выдалены", + "ToastAuthorSearchNotFound": "Аўтар не знойдзены", + "ToastAuthorUpdateMerged": "Аўтар аб'яднаны", "ToastAuthorUpdateSuccess": "Аўтар абноўлены", - "ToastAuthorUpdateSuccessNoImageFound": "Аўтар абноўлены (малюнак не знойдзены)", + "ToastAuthorUpdateSuccessNoImageFound": "Аўтар абноўлены (відарыс не знойдзены)", + "ToastBackupCreateFailed": "Не ўдалося стварыць рэзервовую копію", + "ToastBackupCreateSuccess": "Рэзервовая копія створана", + "ToastBackupDeleteFailed": "Не ўдалося выдаліць рэзервовую копію", + "ToastBackupDeleteSuccess": "Рэзервовая копія выдалена", "ToastBackupInvalidMaxKeep": "Няправільная колькасць рэзервовых копій для захоўвання", "ToastBackupInvalidMaxSize": "Няправільны максімальны памер рэзервовай копіі", + "ToastBackupRestoreFailed": "Не ўдалося аднавіць рэзервовую копію", + "ToastBackupUploadFailed": "Не ўдалося запампаваць рэзервовую копію", + "ToastBackupUploadSuccess": "Рэзервовая копія запампавана", "ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку", + "ToastBookmarkCreateSuccess": "Закладка дададзена", + "ToastBookmarkRemoveSuccess": "Закладка выдалена", + "ToastBulkChapterInvalidCount": "Увядзіце лік ад 1 да 150", + "ToastCachePurgeSuccess": "Кэш паспяхова ачышчаны", + "ToastChapterLocked": "Раздзел заблакіраваны.", + "ToastChaptersMustHaveTitles": "Раздзелы павінны мець загалоўкі", + "ToastChaptersRemoved": "Раздзелы выдалены", + "ToastChaptersUpdated": "Раздзелы абноўлены", + "ToastCollectionItemsAddFailed": "Не ўдалося дадаць элемент(ы) у калекцыю", + "ToastCollectionRemoveSuccess": "Калекцыя выдалена", + "ToastCollectionUpdateSuccess": "Калекцыя абноўлена", + "ToastConnectionNotAvailable": "Падключэнне недаступна. Паспрабуйце яшчэ раз пазней", + "ToastCoverSearchFailed": "Не ўдалося знайсці вокладку", + "ToastCoverUpdateFailed": "Не ўдалося абнавіць вокладку", "ToastDateTimeInvalidOrIncomplete": "Дата і час указаны некарэктна або не цалкам", + "ToastDeleteFileSuccess": "Файл выдалены", + "ToastDeviceAddFailed": "Не ўдалося дадаць прыладу", + "ToastDeviceNameAlreadyExists": "Прылада для чытання электронных кніг з такой назвай ужо існуе", "ToastDeviceTestEmailFailed": "Не ўдалося адправіць тэставае электроннае пісьмо", + "ToastEmailSettingsUpdateSuccess": "Налады электроннай пошты абноўлены", "ToastEncodeCancelFailed": "Не ўдалося скасаваць кадаванне", "ToastEncodeCancelSucces": "Кадаванне скасавана", "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана", - "ToastInvalidImageUrl": "Няправільны URL выявы", + "ToastFailedToCreate": "Не ўдалося стварыць", + "ToastFailedToDelete": "Не ўдалося выдаліць", + "ToastFailedToLoadData": "Не ўдалося загрузіць даныя", + "ToastFailedToMatch": "Не атрымалася знайсці супадзенне", + "ToastFailedToShare": "Не ўдалося абагуліць", + "ToastFailedToUpdate": "Не здалося абнавіць", + "ToastInvalidImageUrl": "Памылковы URL-адрас відарыса", "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі", - "ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана", - "ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як Завершаны", - "ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае", - "ToastItemMarkedAsNotFinishedSuccess": "Элемент пазначаны як Незавершаны", + "ToastItemCoverUpdateSuccess": "Вокладка элемента абноўлена", + "ToastItemDeletedFailed": "Не ўдалося выдаліць элемент", + "ToastItemDeletedSuccess": "Выдалены элемент", + "ToastItemDetailsUpdateSuccess": "Звесткі элемента абноўлены", + "ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як завершаны", + "ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як завершаны", + "ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як незавершаны", + "ToastItemMarkedAsNotFinishedSuccess": "Элемент пазначаны як незавершаны", "ToastItemUpdateSuccess": "Элемент абноўлены", "ToastLibraryCreateFailed": "Не ўдалося стварыць бібліятэку", "ToastLibraryCreateSuccess": "Бібліятэка \"{0}\" створана", @@ -738,22 +878,54 @@ "ToastNameRequired": "Імя абавязковае", "ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"", "ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны", + "ToastNewUserTagError": "Трэбаа выбраць хаця б адзін тэг", + "ToastNewUserUsernameError": "Увядзіце імя карыстальніка", "ToastNoRSSFeed": "У падкаста няма RSS-стужкі", - "ToastPlaylistCreateFailed": "Не ўдалося стварыць спіс прайгравання", - "ToastPlaylistCreateSuccess": "Спіс прайгравання створаны", - "ToastPlaylistRemoveSuccess": "Спіс прайгравання выдалены", - "ToastPlaylistUpdateSuccess": "Спіс прайгравання абноўлены", + "ToastNoUpdatesNecessary": "Абнаўленні не патрэбныя", + "ToastNotificationCreateFailed": "Не ўдалося стварыць апавяшчэнне", + "ToastNotificationDeleteFailed": "Не ўдалося выдаліць апавяшчэнне", + "ToastNotificationSettingsUpdateSuccess": "Налады апавяшчэнняў абноўлены", + "ToastNotificationUpdateSuccess": "Апавяшчэнне абноўлена", + "ToastPlaylistCreateFailed": "Не ўдалося стварыць плэй-ліст", + "ToastPlaylistCreateSuccess": "Плэй-ліст створаны", + "ToastPlaylistRemoveSuccess": "Плэй-ліст выдалены", + "ToastPlaylistUpdateSuccess": "Плэй-ліст абноўлены", "ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст", "ToastPodcastCreateSuccess": "Падкаст паспяхова створаны", "ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста", "ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў", "ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі", + "ToastProgressIsNotBeingSynced": "Прагрэс не сінхранізуецца, перазапусціце прайграванне", + "ToastProviderCreatedFailed": "Не ўдалося дадаць пастаўшчыка", + "ToastProviderCreatedSuccess": "Новы пастаўшчык дададзены", + "ToastProviderNameAndUrlRequired": "Назва і URL-адрас абавязковыя", + "ToastProviderRemoveSuccess": "Пастаўшчык выдалены", "ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку", "ToastRSSFeedCloseSuccess": "RSS-стужка закрыта", + "ToastRemoveFailed": "Не ўдалося выдаліць", + "ToastRemoveItemFromCollectionFailed": "Не ўдалося выдаліць элемент з калекцыі", + "ToastRemoveItemFromCollectionSuccess": "Элемент выдалены з калекцыі", + "ToastRenameFailed": "Не ўдалося перайменаваць", + "ToastSelectAtLeastOneUser": "Выберыце прынамсі аднаго карыстальніка", "ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу", "ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"", + "ToastSessionCloseFailed": "Не ўдалося закрыць сеанс", + "ToastSessionDeleteFailed": "Не ўдалося выдаліць сеанс", + "ToastSessionDeleteSuccess": "Сеанс выдалены", "ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р", + "ToastTitleRequired": "Загаловак абавязковы", + "ToastUnknownError": "Невядомая памылка", "ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.", + "ToastUserDeleteFailed": "Не ўдалося выдаліць карыстальніка", + "ToastUserDeleteSuccess": "Карыстальнік выдалены", + "ToastUserPasswordChangeSuccess": "Пароль паспяхова зменены", + "ToastUserPasswordMismatch": "Паролі не супадаюць", "ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым", - "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара" + "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара", + "TooltipAddChapters": "Дадаць раздзел(ы)", + "TooltipAddOneSecond": "Дадаць 1 секунду", + "TooltipLockAllChapters": "Заблакіраваць усе раздзелы", + "TooltipLockChapter": "Заблакіраваць раздзел (Shift+націсканне для дыяпазону)", + "TooltipSubtractOneSecond": "Адняць 1 секунду", + "TooltipUnlockAllChapters": "Разблакіраваць усе раздзелы" } From 5f8450602e302b67781d2b4d25e6d6c4e13a309e Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Thu, 26 Feb 2026 09:19:58 +0100 Subject: [PATCH 184/285] Translated using Weblate (Belarusian) Currently translated at 92.1% (1072 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/ --- client/strings/be.json | 242 ++++++++++++++++++++++++++++++++--------- 1 file changed, 192 insertions(+), 50 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index 799511d4c..cc5dc7bc5 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -17,7 +17,7 @@ "ButtonCancel": "Скасаваць", "ButtonCancelEncode": "Скасаваць кадзіраванне", "ButtonChangeRootPassword": "Зменіце Root пароль", - "ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды", + "ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя выпускі", "ButtonChooseAFolder": "Выбраць папку", "ButtonChooseFiles": "Выбраць файлы", "ButtonClearFilter": "Ачысціць фільтр", @@ -134,7 +134,7 @@ "HeaderCollection": "Калекцыя", "HeaderCollectionItems": "Элементы калекцыі", "HeaderCover": "Вокладка", - "HeaderCurrentDownloads": "Бягучыя спампоўкі", + "HeaderCurrentDownloads": "Бягучыя спампоўванні", "HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе", "HeaderCustomMetadataProviders": "Карыстальніцкія пастаўшчыкі метаданых", "HeaderDetails": "Падрабязнасці", @@ -142,7 +142,7 @@ "HeaderEbookFiles": "Файлы электронных кніг", "HeaderEmail": "Электронная пошта", "HeaderEmailSettings": "Налады электроннай пошты", - "HeaderEpisodes": "Эпізоды", + "HeaderEpisodes": "Выпускі", "HeaderEreaderDevices": "Прылады для чытання", "HeaderEreaderSettings": "Налады прылады для чытання", "HeaderFiles": "Файлы", @@ -151,7 +151,7 @@ "HeaderItemFiles": "Файлы элементаў", "HeaderItemMetadataUtils": "Утыліты для метаданых элементаў", "HeaderLastListeningSession": "Апошні сеанс праслухоўвання", - "HeaderLatestEpisodes": "Апошнія эпізоды", + "HeaderLatestEpisodes": "Апошнія выпускі", "HeaderLibraries": "Бібліятэкі", "HeaderLibraryFiles": "Файлы бібліятэкі", "HeaderLibraryStats": "Статыстыка бібліятэкі", @@ -187,11 +187,11 @@ "HeaderRSSFeedGeneral": "Падрабязнасці RSS", "HeaderRSSFeedIsOpen": "RSS-стужка адкрытая", "HeaderRSSFeeds": "RSS-стужкі", - "HeaderRemoveEpisode": "Выдаліць эпізод", - "HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў", + "HeaderRemoveEpisode": "Выдаліць выпуск", + "HeaderRemoveEpisodes": "Выдаліць {0} выпускаў", "HeaderSavedMediaProgress": "Захаваны прагрэс медыя", "HeaderSchedule": "Расклад", - "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў", + "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спампоўванняў выпускаў", "HeaderScheduleLibraryScans": "Расклад аўтаматычнага сканавання бібліятэкі", "HeaderSession": "Сеанс", "HeaderSetBackupSchedule": "Наладзіць расклад рэзервовага капіравання", @@ -237,7 +237,7 @@ "LabelAddedDate": "Дададзена {0}", "LabelAdminUsersOnly": "Толькі для адміністратараў", "LabelAll": "Усе", - "LabelAllEpisodesDownloaded": "Усе эпізоды спампаваныя", + "LabelAllEpisodesDownloaded": "Усе выпускі спампаваныя", "LabelAllUsers": "Усе карыстальнікі", "LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей", "LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей", @@ -255,7 +255,7 @@ "LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)", "LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)", "LabelAuthors": "Аўтары", - "LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", + "LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі", "LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых", "LabelAutoFetchMetadataHelp": "Атрыманне звестак пра загаловак, аўтара і серыю для спрашчэння запампоўвання. Пасля запампоўвання, магчыма, спатрэбіцца супаставіць дадатковыя метаданыя.", "LabelAutoLaunch": "Аўтазапуск", @@ -290,11 +290,12 @@ "LabelCollapseSubSeries": "Згарнуць падсерыі", "LabelCollection": "Калекцыя", "LabelCollections": "Калекцыі", - "LabelComplete": "Завершана", + "LabelComplete": "Завяршыць", "LabelConfirmPassword": "Пацвердзіце пароль", "LabelContinueListening": "Працягваць слухаць", "LabelContinueReading": "Працягнуць чытанне", "LabelContinueSeries": "Працягнуць серыі", + "LabelCorsAllowed": "Дазволеныя крыніцы CORS", "LabelCover": "Вокладка", "LabelCoverImageURL": "URL-адрас відарыса вокладкі", "LabelCoverProvider": "Пастаўшчык вокладак", @@ -317,7 +318,7 @@ "LabelDiscFromMetadata": "Дыск з метаданых", "LabelDiscover": "Знайсці", "LabelDownload": "Спампаваць", - "LabelDownloadNEpisodes": "Спампована {0} эпізодаў", + "LabelDownloadNEpisodes": "Спампавана {0} выпускаў", "LabelDownloadable": "Спампоўваецца", "LabelDuration": "Працягласць", "LabelDurationComparisonExactMatch": "(дакладнае супадзенне)", @@ -346,10 +347,13 @@ "LabelEncodingWarningAdvancedSettings": "Увага: Не абнаўляйце гэтыя налады, калі вы не знаёмыя з параметрамі кадавання ffmpeg.", "LabelEnd": "Канец", "LabelEndOfChapter": "Канец раздзела", - "LabelEpisode": "Эпізод", - "LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай", - "LabelEpisodeTitle": "Загаловак эпізоду", - "LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі", + "LabelEpisode": "Выпуск", + "LabelEpisodeNotLinkedToRssFeed": "Выпуск не звязаны з RSS-стужкай", + "LabelEpisodeNumber": "Выпуск №{0}", + "LabelEpisodeTitle": "Загаловак выпуску", + "LabelEpisodeType": "Тып выпуску", + "LabelEpisodeUrlFromRssFeed": "URL-адрас выпуску з RSS-стужкі", + "LabelEpisodes": "Выпускі", "LabelEpisodic": "Эпізадычны", "LabelExample": "Прыклад", "LabelExpandSeries": "Разгарнуць серыю", @@ -358,15 +362,20 @@ "LabelExpiresAt": "Тэрмін дзеяння заканчваецца ў", "LabelExpiresInSeconds": "Тэрмін дзеяння заканчваецца праз (секунд)", "LabelExpiresNever": "Ніколі", - "LabelExplicit": "Відверты", + "LabelExplicit": "Непрыстойнае", + "LabelExplicitChecked": "Непрыстойнае (пазначана)", + "LabelExplicitUnchecked": "Прыстойнае (не пазначана)", "LabelExportOPML": "Экспарт OPML", "LabelFeedURL": "URL стужкі", "LabelFetchingMetadata": "Атрыманне метаданых", "LabelFile": "Файл", "LabelFileBirthtime": "Час стварэння файла", + "LabelFileBornDate": "Створаны {0}", "LabelFileModified": "Час змянення файла", "LabelFileModifiedDate": "Зменены {0}", "LabelFilename": "Назва файла", + "LabelFilterByUser": "Фільтраваць па карыстальніку", + "LabelFindEpisodes": "Знайсці выпускі", "LabelFinished": "Завершана", "LabelFinishedDate": "Завершана {0}", "LabelFolder": "Папка", @@ -387,11 +396,13 @@ "LabelHighestPriority": "Найвышэйшы прыярытэт", "LabelHost": "Хост", "LabelHour": "Гадзіна", + "LabelHours": "Гадзіны", "LabelIcon": "Значок", "LabelImageURLFromTheWeb": "URL-адрас відарыса з інтэрнэту", "LabelInProgress": "У працэсе", "LabelIncludeInTracklist": "Уключыць у спіс трэкаў", "LabelIncomplete": "Незавершана", + "LabelInterval": "Інтэрвал", "LabelIntervalCustomDailyWeekly": "Карыстальніцкі штодзённы/штотыднёвы", "LabelIntervalEvery12Hours": "Кожныя 12 гадзін", "LabelIntervalEvery15Minutes": "Кожныя 15 хвілін", @@ -430,42 +441,73 @@ "LabelLogLevelDebug": "Debug", "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", + "LabelLookForNewEpisodesAfterDate": "Шукаць новыя выпускі пасля гэтай даты", "LabelLowestPriority": "Найніжэйшы прыярытэт", + "LabelMatchConfidence": "Упэўненасць", "LabelMatchExistingUsersByDescription": "Выкарыстоўваецца для падключэння існуючых карыстальнікаў. Пасля падключэння карыстальнікі будуць супастаўляцца з дапамогай унікальнага ідэнтыфікатара ад пастаўшчыка SSO", - "LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.", - "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку", - "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.", + "LabelMaxEpisodesToDownload": "Максімальная колькасць выпускаў для спампоўвання. 0 – неабмежаваная колькасць.", + "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых выпускаў для спампоўвання за праверку", + "LabelMaxEpisodesToKeep": "Максімальная колькасць выпускаў, якія будуць захоўвацца", + "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не задае максімальнага абмежавання. Пасля аўтаматычнага спампоўвання новага выпуску будзе выдалены самы стары выпуск, калі ў вас больш за X выпускаў. Пры кожным новым спампоўванні будзе выдаляцца толькі 1 выпуск.", "LabelMediaPlayer": "Медыяпрайгравальнік", "LabelMediaType": "Тып медыя", + "LabelMetaTag": "Метатэг", + "LabelMetaTags": "Метатэгі", "LabelMetadataOrderOfPrecedenceDescription": "Крыніцы метаданых з вышэйшым прыярытэтам будуць замяняць крыніцы з ніжэйшым прыярытэтам", "LabelMetadataProvider": "Пастаўшчык метаданых", + "LabelMinute": "Хвіліна", + "LabelMinutes": "Хвіліны", "LabelMissing": "Адсутнічае", + "LabelMissingEbook": "Няма электроннай кнігі", + "LabelMissingSupplementaryEbook": "Няма дадатковай электроннай кнігі", + "LabelMobileRedirectURIs": "Дазволеныя URI перанакіравання для мабільных прылад", + "LabelMobileRedirectURIsDescription": "Гэта белы спіс дапушчальных URI перанакіравання для мабільных праграм. Стандартным з'яўляецца audiobookshelf://oauth, які вы можаце выдаліць або дапоўніць дадатковымі URI для інтэграцыі са староннімі праграмамі. Выкарыстанне зорачкі (*) у якасці адзінага запісу дазваляе любы URI.", "LabelMore": "Больш", "LabelMoreInfo": "Больш інфармацыі", "LabelName": "Назва", - "LabelNarrator": "Чытальнік", - "LabelNarrators": "Чытальнікі", + "LabelNarrator": "Дыктар", + "LabelNarrators": "Дыктары", + "LabelNew": "Новы", + "LabelNewPassword": "Новы пароль", "LabelNewestAuthors": "Новыя аўтары", - "LabelNewestEpisodes": "Новыя эпізоды", + "LabelNewestEpisodes": "Найноўшыя выпускі", + "LabelNextBackupDate": "Дата наступнага рэзервовага капіравання", "LabelNextChapters": "Наступныя раздзелы:", "LabelNextScheduledRun": "Наступны запланаваны запуск", "LabelNoApiKeys": "Няма ключоў API", "LabelNoCustomMetadataProviders": "Няма карыстальніцкіх пастаўшчыкоў метаданых", - "LabelNotFinished": "Не завершана", + "LabelNoEpisodesSelected": "Не выбрана ніводнага выпуску", + "LabelNotFinished": "Незавершана", "LabelNotStarted": "Не пачата", + "LabelNotificationAppriseURL": "URL-адрасы Apprise", "LabelNotificationAvailableVariables": "Даступныя пераменныя", + "LabelNotificationBodyTemplate": "Шаблон зместу", + "LabelNotificationEvent": "Падзея апавяшчэння", "LabelNotificationTitleTemplate": "Шаблон загалоўка", + "LabelNotificationsMaxFailedAttempts": "Максімальная колькасць няўдалых спроб", "LabelNotificationsMaxFailedAttemptsHelp": "Апавяшчэнні адключаюцца пасля таго, як не ўдаецца іх адправіць гэтулькі разоў", + "LabelNotificationsMaxQueueSize": "Максімальны памер чаргі для падзей апавяшчэнняў", + "LabelNotificationsMaxQueueSizeHelp": "Падзеі могуць спрацоўваць толькі адзін раз у секунду. Падзеі будуць ігнаравацца пры дасягненні максімальнага памеру чаргі. Гэта прадухіляе рассылку спаму.", "LabelNumberOfBooks": "Колькасць кніг", "LabelNumberOfChapters": "Колькасць раздзелаў:", - "LabelNumberOfEpisodes": "# з эпізодаў", + "LabelNumberOfEpisodes": "Колькасць выпускаў", "LabelOpenRSSFeed": "Адкрыць RSS-стужку", "LabelOverwrite": "Перазапісаць", "LabelPaginationPageXOfY": "Старонка {0} з {1}", "LabelPassword": "Пароль", "LabelPath": "Шлях", - "LabelPermissionsDownload": "Можна спампаваць", + "LabelPermanent": "Пастаянны", + "LabelPermissionsAccessAllLibraries": "Мае доступ да ўсіх бібліятэк", + "LabelPermissionsAccessAllTags": "Мае доступ да ўсіх тэгаў", + "LabelPermissionsAccessExplicitContent": "Мае доступ да непрыстойнага змесціва", + "LabelPermissionsCreateEreader": "Можа ствараць прыладу для чытання", + "LabelPermissionsDelete": "Можа выдаляць", + "LabelPermissionsDownload": "Можа спампоўваць", + "LabelPermissionsUpdate": "Можа абнаўляць", + "LabelPermissionsUpload": "Можа запампоўваць", + "LabelPersonalYearReview": "Вынікі года ({0})", "LabelPhotoPathURL": "Шлях/URL-адрас фота", + "LabelPlayMethod": "Метад прайгравання", "LabelPlayerChapterNumberMarker": "{0} з {1}", "LabelPlaylists": "Плэй-лісты", "LabelPodcast": "Падкаст", @@ -480,6 +522,8 @@ "LabelPubDate": "Дата публікацыі", "LabelPublishYear": "Год публікацыі", "LabelPublishedDate": "Апублікавана {0}", + "LabelPublisher": "Выдавец", + "LabelPublishers": "Выдаўцы", "LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка", "LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка", "LabelRSSFeedOpen": "RSS-стужка адкрыта", @@ -508,8 +552,8 @@ "LabelSeason": "Сезон", "LabelSeasonNumber": "Сезон #{0}", "LabelSelectAll": "Выбраць усё", - "LabelSelectAllEpisodes": "Выбраць усе эпізоды", - "LabelSelectEpisodesShowing": "Выбраць {0} эпізодаў для паказу", + "LabelSelectAllEpisodes": "Выбраць усе выпускі", + "LabelSelectEpisodesShowing": "Выбраць {0} выпускаў для паказу", "LabelSelectUser": "Выберыце карыстальніка", "LabelSelectUsers": "Выбраць карыстальнікаў", "LabelSendEbookToDevice": "Адправіць электронную кнігу на...", @@ -529,8 +573,8 @@ "LabelSettingsDateFormat": "Фарматы даты", "LabelSettingsEnableWatcherForLibrary": "Аўтаматычна сачыць за зменамі ў бібліятэцы", "LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера", - "LabelSettingsEpubsAllowScriptedContent": "Дазволіць скрыптавы кантэнт у EPUB", - "LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць EPUB-файлам выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы EPUB-файлаў.", + "LabelSettingsEpubsAllowScriptedContent": "Дазваляць скрыпты ў файлах EPUB", + "LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць файлам EPUB выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы файлаў EPUB.", "LabelSettingsExperimentalFeatures": "Эксперыментальныя функцыі", "LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.", "LabelSettingsFindCovers": "Знайсці вокладкі", @@ -540,10 +584,10 @@ "LabelSettingsHomePageBookshelfView": "Галоўная старонка выкарыстоўвае выгляд кніжнай паліцы", "LabelSettingsLibraryBookshelfView": "Бібліятэка выкарыстоўвае выгляд кніжнай паліцы", "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Працэнт завяршэння большы за", - "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунды)", - "LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як скончаны, калі", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунд)", + "LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як завершаны, калі", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусціць папярэднія кнігі ў \"Працягнуць серыю\"", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Палка \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Паліца \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.", "LabelSettingsParseSubtitles": "Аналізаваць падзагалоўкі", "LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў папак аўдыякніг.
Падзагаловак павінен быць аддзелены сімвалам \" - \".
Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"", "LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метаданым", @@ -555,7 +599,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Прадвызначана метаданыя захоўваюцца ў /metadata/items. Пры ўключэнні гэтай опцыі файлаў метаданых будуць захоўвацца ў папках элементаў бібліятэкі", "LabelSettingsTimeFormat": "Фармат часу", "LabelShare": "Абагуліць", - "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.", + "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку, спампоўваць ZIP-архіў элемента бібліятэкі.", "LabelShowAll": "Паказаць усё", "LabelShowSeconds": "Паказваць секунды", "LabelShowSubtitles": "Паказаць падзагалоўкі", @@ -565,6 +609,9 @@ "LabelSortDescending": "Па ўбыванні", "LabelStart": "Пачаць", "LabelStartTime": "Час пачатку", + "LabelStarted": "Пачата", + "LabelStartedAt": "Пачата ў", + "LabelStartedDate": "Пачата {0}", "LabelStatsAudioTracks": "Аўдыятрэкі", "LabelStatsAuthors": "Аўтары", "LabelStatsBestDay": "Лепшы дзень", @@ -609,7 +656,9 @@ "LabelToolsEmbedMetadata": "Убудаваць метаданыя", "LabelToolsEmbedMetadataDescription": "Убудаваць метаданыя ў аўдыяфайлы, уключаючы відарыс вокладкі і раздзелы.", "LabelToolsM4bEncoder": "Кадавальнік M4B", + "LabelToolsMakeM4b": "Стварыць файл аўдыякнігі M4B", "LabelToolsMakeM4bDescription": "Стварыць аўдыякнігу ў фармаце .M4B з убудаванымі метаданымі, відарысам вокладкі і раздзеламі.", + "LabelToolsSplitM4b": "Падзяліць M4B на MP3", "LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метаданымі, відарысам вокладкі і раздзеламі.", "LabelTotalDuration": "Агульная працягласць", "LabelTotalTimeListened": "Агульны час праслухоўвання", @@ -619,7 +668,9 @@ "LabelTracksMultiTrack": "Некалькі трэкаў", "LabelTracksNone": "Няма трэкаў", "LabelTracksSingleTrack": "Адзін трэк", + "LabelTrailer": "Трэйлер", "LabelType": "Тып", + "LabelUnabridged": "Поўная версія", "LabelUndo": "Адрабіць", "LabelUnknown": "Невядома", "LabelUnknownPublishDate": "Невядомая дата публікацыі", @@ -635,6 +686,7 @@ "LabelUseAdvancedOptions": "Выкарыстоўваць пашыраныя параметры", "LabelUseChapterTrack": "Выкарыстоўваць трэк раздзела", "LabelUseFullTrack": "Выкарыстоўваць увесь трэк", + "LabelUseZeroForUnlimited": "0 – неабмежавана", "LabelUser": "Карыстальнік", "LabelUsername": "Імя карыстальніка", "LabelValue": "Значэнне", @@ -658,6 +710,7 @@ "MessageAddToPlayerQueue": "Дадаць у чаргу прайгравальніка", "MessageAppriseDescription": "Каб выкарыстоўваць гэтую функцыю, вам спатрэбіцца запусціць асобнік Apprise API або API, які будзе апрацоўваць тыя ж запыты.
URL Apprise API павінен быць поўным шляхам для адпраўкі апавяшчэння, напрыклад, калі ваш API працуе па адрасе http://192.168.1.1:8337, то вы павінны ўвесці http://192.168.1.1:8337/notify.", "MessageAuthenticationOIDCChangesRestart": "Перазапусціце сервер пасля захавання, каб прымяніць змены OIDC.", + "MessageAuthenticationSecurityMessage": "Дзеля бяспекі была палепшана аўтэнтыфікацыя. Усім карыстальнікам трэба паўторна ўвайсці ў сістэму.", "MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і відарысы, якія захоўваюцца ў /metadata/items і /metadata/authors. Рэзервовыя копіі не ўключаюць файлы, якія захоўваюцца ў папках бібліятэкі.", "MessageBackupsLocationEditNote": "Заўвага: Абнаўленне месцазнаходжання рэзервовых копій не перамяшчае і не змяняе існуючыя рэзервовыя копіі", "MessageBackupsLocationNoEditNote": "Заўвага: Месцазнаходжанне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.", @@ -666,17 +719,20 @@ "MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты данымі з гэтага элемента", "MessageBatchQuickMatchDescription": "Хуткі пошук паспрабуе дадаць адсутныя вокладкі і метаданыя для выбраных элементаў. Уключыце параметры ніжэй, каб дазволіць Хуткаму пошуку замяняць існуючыя вокладкі і/або метаданыя.", "MessageBookshelfNoCollections": "Вы пакуль не стварылі ніводнай калекцый", + "MessageBookshelfNoCollectionsHelp": "Калекцыі публічныя. Усе карыстальнікі, якія маюць доступ да бібліятэкі, могуць іх бачыць.", "MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак", "MessageBookshelfNoResultsForFilter": "Няма вынікаў для фільтра \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Няма вынікаў па запыце", + "MessageBookshelfNoSeries": "У вас няма серый", "MessageChapterErrorFirstNotZero": "Першы раздзел павінен пачынацца з 0", "MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі", "MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела", "MessageChaptersNotFound": "Раздзелы не знойдзены", "MessageCheckingCron": "Праверка cron...", - "MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?", + "MessageConfirmCloseFeed": "Вы ўпэўнены, што хочаце закрыць гэту стужку?", "MessageConfirmDeleteApiKey": "Вы ўпэўнены, што хочаце выдаліць ключ API \"{0}\"?", "MessageConfirmDeleteBackup": "Вы ўпэўнены, што хочаце выдаліць рэзервовую копію для {0}?", + "MessageConfirmDeleteDevice": "Вы ўпэўнены, што хочаце выдаліць прыладу для чытання \"{0}\"?", "MessageConfirmDeleteFile": "Будзе выдалены файл з файлавай сістэмы. Вы ўпэўнены?", "MessageConfirmDeleteLibrary": "Вы ўпэўнены, што хочаце назаўжды выдаліць бібліятэку \"{0}\"?", "MessageConfirmDeleteLibraryItem": "Будзе выдалены элемент бібліятэкі з базы даных і файлавай сістэмы. Вы ўпэўнены?", @@ -685,14 +741,23 @@ "MessageConfirmDeleteNotification": "Вы ўпэўнены, што хочаце выдаліць гэта апавяшчэнне?", "MessageConfirmDeleteSession": "Вы ўпэўнены, што хочаце выдаліць гэты сеанс?", "MessageConfirmEmbedMetadataInAudioFiles": "Вы ўпэўнены, што хочаце ўбудаваць метаданыя ў {0} аўдыяфайлаў?", - "MessageConfirmPurgeCache": "Ачышчэнне кэшу выдаліць увесь каталог па адрасе /metadata/cache.

Вы сапраўды хочаце выдаліць каталог кэшу?", + "MessageConfirmMarkAllEpisodesFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як завершаныя?", + "MessageConfirmMarkAllEpisodesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як незавершаныя?", + "MessageConfirmMarkItemFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як завершаны?", + "MessageConfirmMarkItemNotFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як незавершаны?", + "MessageConfirmMarkSeriesFinished": "Вы ўпэўнены, што хочаце пазначыць усе кнігі ў гэтай серыі як завершаныя?", + "MessageConfirmMarkSeriesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе кнігі ў гэтай серыі як незавершаныя?", + "MessageConfirmPurgeCache": "Ачышчэнне кэшу выдаліць увесь каталог па адрасе /metadata/cache.

Вы ўпэўнены, што хочаце выдаліць каталог кэшу?", "MessageConfirmPurgeItemsCache": "Ачышчэнне кэшу элементаў выдаліць увесь каталог па адрасе /metadata/cache/items.
Вы ўпэўнены?", - "MessageConfirmQuickMatchEpisodes": "Хуткае супадзенне эпізодаў перазапіша дэталі, калі супадзенне будзе знойдзена. Будуць абноўлены толькі эпізоды, якія не супадаюць. Вы ўпэўнены?", + "MessageConfirmQuickMatchEpisodes": "Хуткае супадзенне выпускаў перазапіша дэталі, калі супадзенне будзе знойдзена. Будуць абноўлены толькі выпускі, якія не супадаюць. Вы ўпэўнены?", "MessageConfirmRemoveAllChapters": "Вы ўпэўнены, што хочаце выдаліць усе раздзелы?", "MessageConfirmRemoveAuthor": "Вы ўпэўнены, што хочаце выдаліць аўтара \"{0}\"?", "MessageConfirmRemoveCollection": "Вы ўпэўнены, што хочаце выдаліць калекцыю \"{0}\"?", - "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?", + "MessageConfirmRemoveEpisode": "Вы ўпэўнены, што хочаце выдаліць выпуск \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Вы ўпэўнены, што хочаце выдаліць {0} выпускаў?", + "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што хочаце выдаліць {0} сеансаў праслухоўвання?", "MessageConfirmRemoveMetadataFiles": "Вы ўпэўнены, што хочаце выдаліць усе файлы metadata.{0} у папках элементаў бібліятэкі?", + "MessageConfirmRemoveNarrator": "Вы ўпэўнены, што хочаце выдаліць дыктара \"{0}\"?", "MessageConfirmRemovePlaylist": "Вы ўпэўнены, што хочаце выдаліць плэй-ліст \"{0}\"?", "MessageConfirmRenameGenre": "Вы ўпэўнены, што хочаце перайменаваць жанр \"{0}\" на \"{1}\" для ўсіх элементаў?", "MessageConfirmRenameGenreMergeNote": "Заўвага: Гэты жанр ужо існуе, таму яны будуць аб'яднаны.", @@ -702,55 +767,94 @@ "MessageConfirmRenameTagWarning": "Увага! Падобны тэг з іншым рэгістрам ужо існуе: \"{0}\".", "MessageConfirmResetProgress": "Вы ўпэўнены, што хочаце скінуць свой прагрэс?", "MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?", - "MessageDownloadingEpisode": "Спампоўка эпізоду", + "MessageDownloadingEpisode": "Спампоўванне выпуску", "MessageDragFilesIntoTrackOrder": "Перацягніце файлы ў правільным парадку трэкаў", + "MessageEmbedFinished": "Убудаванне завершана!", "MessageEmbedQueue": "У чарзе на ўбудаванне метаданых (у чарзе {0})", - "MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", + "MessageEpisodesQueuedForDownload": "{0} выпуск(-аў) у чарзе спампоўвання", "MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.", "MessageFeedURLWillBe": "URL стужкі будзе {0}", "MessageFetching": "Атрыманне...", + "MessageHeatmapNoListeningSessions": "Няма сеансаў праслухоўвання на {0}", "MessageImportantNotice": "Важная заўвага!", + "MessageInsertChapterBelow": "Уставіць раздзел ніжэй", "MessageInvalidAsin": "Няправільны ASIN", + "MessageItemsSelected": "Выбрана элементаў: {0}", "MessageItemsUpdated": "{0} элементаў абноўлена", "MessageJoinUsOn": "Далучайцеся да нас у", "MessageLoading": "Загрузка...", "MessageLoadingFolders": "Загрузка папак...", "MessageLogsDescription": "Журналы захоўваюцца ў каталогу /metadata/logs у фармаце JSON. Журналы памылак захоўваюцца ў файле /metadata/logs/crashlogs.txt.", + "MessageM4BFinished": "M4B завершана!", "MessageMapChapterTitles": "Супаставіць загалоўкі раздзелаў з існуючымі раздзеламі аўдыякнігі без змянення пазнак часу", + "MessageMarkAllEpisodesFinished": "Пазначыць усе выпускі як завершаныя", + "MessageMarkAllEpisodesNotFinished": "Пазначыць усе выпускі як незавершаныя", "MessageMarkAsFinished": "Пазначыць як завершаную", + "MessageMarkAsNotFinished": "Пазначыць як незавершаную", "MessageMatchBooksDescription": "паспрабуе параўнаць кнігі ў бібліятэцы з кнігай ад выбранай пошукавай сістэмы і запоўніць пустыя палі і вокладку. Не перазапісвае звесткі.", "MessageNoAudioTracks": "Няма аўдыятрэкаў", + "MessageNoAuthors": "Няма аўтараў", + "MessageNoBackups": "Няма рэзервовых копій", "MessageNoBookmarks": "Няма закладак", "MessageNoChapters": "Няма раздзелаў", "MessageNoCollections": "Няма калекцый", + "MessageNoCoversFound": "Не знойдзена вокладак", + "MessageNoDescription": "Няма апісання", + "MessageNoDevices": "Няма прылад", "MessageNoDownloadsInProgress": "Зараз няма актыўных спампованняў", "MessageNoDownloadsQueued": "Няма спамповак у чарзе", + "MessageNoEpisodeMatchesFound": "Адпаведных выпускаў не знойдзена", + "MessageNoEpisodes": "Няма выпускаў", + "MessageNoFoldersAvailable": "Няма даступных папак", + "MessageNoGenres": "Няма жанраў", + "MessageNoIssues": "Няма праблем", "MessageNoItems": "Няма элементаў", "MessageNoItemsFound": "Элементы не знойдзены", "MessageNoListeningSessions": "Няма сеансаў праслухоўвання", "MessageNoLogs": "Няма журналаў", "MessageNoMediaProgress": "Няма прагрэсу медыя", + "MessageNoNotifications": "Няма апавяшчэнняў", "MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі", "MessageNoPodcastsFound": "Падкасты не знойдзены", + "MessageNoResults": "Няма вынікаў", + "MessageNoSearchResultsFor": "Няма вынікаў пошуку па запыце \"{0}\"", + "MessageNoSeries": "Няма серый", + "MessageNoTags": "Няма тэгаў", "MessageNoTasksRunning": "Няма запушчаных задач", "MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся", "MessageNoUserPlaylists": "У вас няма плэй-лістоў", "MessageNoUserPlaylistsHelp": "Плэй-лісты прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.", - "MessageOpmlPreviewNote": "Заўвага: гэта перадпрагляд прааналізаванага файла OPML. Фактычны загаловак падкаста будзе ўзяты з RSS-ленты.", + "MessageNotYetImplemented": "Пакуль не рэалізавана", + "MessageOpmlPreviewNote": "Заўвага: гэта перадпрагляд прааналізаванага файла OPML. Фактычны загаловак падкаста будзе ўзяты з RSS-стужкі.", + "MessageOr": "або", "MessagePauseChapter": "Прыпыніць прайграванне раздзела", + "MessagePlayChapter": "Паслухаць пачатак раздзела", "MessagePlaylistCreateFromCollection": "Стварыць плэй-ліст з калекцыі", + "MessagePleaseWait": "Пачакайце...", "MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення", "MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі", "MessageQuickEmbedInProgress": "Выконваецца хуткае ўбудаванне", "MessageQuickMatchDescription": "Запоўніць пустыя звесткі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе звесткіі, калі параметр \"Аддаваць перавагу супадаючым метаданым\" на серверы не ўключана.", + "MessageRemoveChapter": "Выдаліць раздзел", + "MessageRemoveEpisodes": "Выдаліць выпускі ({0})", + "MessageRemoveFromPlayerQueue": "Выдаліць з чаргі прагравання", + "MessageRemoveUserWarning": "Вы ўпэўнены, што хочаце назаўжды выдаліць карыстальніка \"{0}\"?", "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на", + "MessageResetChaptersConfirm": "Вы ўпэўнены, што хочаце скінуць раздзелы і адрабіць зробленыя вамі змены?", + "MessageRestoreBackupConfirm": "Вы ўпэўнены, што хочаце аднавіць рэзервовую копію, створаную", "MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама відарысы вокладкі ў /metadata/items і /metadata/authors.

Рэзервовыя копіі не змяняюць файлы ў папках бібліятэкі. Калі вы ўключылі налады сервера для захоўвання воклак і метаданых у папках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца.

Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.", "MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}", + "MessageSearchResultsFor": "Вынікі пошуку для", + "MessageSelected": "Выбрана: {0}", + "MessageSeriesSequenceCannotContainSpaces": "Паслядоўнасць серый не можа ўтрымліваць прабелы", + "MessageServerCouldNotBeReached": "Сервер недаступны", "MessageSetChaptersFromTracksDescription": "Задаць раздзелы, выкарыстоўваючы кожны аўдыяфайл у якасці раздзела і назву аўдыяфайла ў якасці загалоўка раздзела", + "MessageShareExpirationWillBe": "Тэрмін дзеяння будзе {0}", + "MessageShareExpiresIn": "Тэрмін дзеяння заканчваецца праз {0}", "MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?", "MessageTaskAudioFileNotWritable": "Аўдыяфайл \"{0}\" недаступны для запісу", "MessageTaskCanceledByUser": "Задача скасавана карыстальнікам", - "MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"", + "MessageTaskDownloadingEpisodeDescription": "Спампоўванне выпуску \"{0}\"", "MessageTaskEmbeddingMetadata": "Убудаванне метаданых", "MessageTaskEmbeddingMetadataDescription": "Убудаванне метаданых у аўдыякнігу \"{0}\"", "MessageTaskEncodingM4b": "Кадаванне M4B", @@ -785,23 +889,34 @@ "MessageTaskTargetDirectoryNotWritable": "Мэтавы каталог недаступны для запісу", "NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.", "NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", + "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш выпускаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", "NoteUploaderFoldersWithMediaFiles": "Папкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.", - "NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца", + "NotificationOnEpisodeDownloadedDescription": "Спрацоўвае, калі выпуск падкаста аўтаматычна спампоўваецца", + "NotificationOnRSSFeedDisabledDescription": "Спрацоўвае, калі аўтаматычнае спампоўванне выпускаў адключана з-за занадта вялікай колькасці няўдалых спроб", + "NotificationOnRSSFeedFailedDescription": "Спрацоўвае, пры памылцы запыту RSS-стужкі для аўтаматычнага спампоўвання выпуску", "NotificationOnTestDescription": "Падзея для тэсціравання сістэмы апавяшчэнняў", - "PlaceholderBulkChapterInput": "Увядзіце загаловак раздзела або выкарыстоўвайце нумарацыю (напрыклад, «Эпізод 1», «Раздзел 10», «1.»)", + "PlaceholderBulkChapterInput": "Увядзіце загаловак раздзела або выкарыстоўвайце нумарацыю (напрыклад, «Выпуск 1», «Раздзел 10», «1.»)", "PlaceholderNewCollection": "Назва новай калекцыі", "PlaceholderNewFolderPath": "Шлях да новай папкі", "PlaceholderNewPlaylist": "Назва новага плэй-ліста", "PlaceholderSearch": "Пошук..", + "PlaceholderSearchEpisode": "Пошук выпуску...", "StatsAuthorsAdded": "дададзена аўтараў", "StatsBooksAdded": "дададзена кніг", - "StatsBooksFinished": "кнігі скончаны", - "StatsBooksFinishedThisYear": "Некаторыя кнігі скончаны ў гэтым годзе…", + "StatsBooksFinished": "завершана кніг", + "StatsBooksFinishedThisYear": "Некаторыя кнігі завершаны ў гэтым годзе…", "StatsBooksListenedTo": "кнігі, якія былі праслуханы", "StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…", "StatsSessions": "сеансаў", + "StatsTopAuthor": "ТОП АЎТАР", + "StatsTopAuthors": "ТОП АЎТАРЫ", + "StatsTopGenre": "ТОП ЖАНР", + "StatsTopGenres": "ТОП ЖАНРЫ", + "StatsTopMonth": "ТОП МЕСЯЦ", + "StatsTopNarrator": "ТОП ДЫКТАР", + "StatsTopNarrators": "ТОП ДЫКТАРЫ", "ToastAccountUpdateSuccess": "Уліковы запіс абноўлены", + "ToastAppriseUrlRequired": "Неабходна ўвесці URL-адрас Apprise", "ToastAsinRequired": "ASIN абавязковы", "ToastAuthorImageRemoveSuccess": "Відарыс аўтара выдалены", "ToastAuthorNotFound": "Аўтар \"{0}\" не знойдзены", @@ -810,6 +925,7 @@ "ToastAuthorUpdateMerged": "Аўтар аб'яднаны", "ToastAuthorUpdateSuccess": "Аўтар абноўлены", "ToastAuthorUpdateSuccessNoImageFound": "Аўтар абноўлены (відарыс не знойдзены)", + "ToastBackupAppliedSuccess": "Рэзервовая копія прыменена", "ToastBackupCreateFailed": "Не ўдалося стварыць рэзервовую копію", "ToastBackupCreateSuccess": "Рэзервовая копія створана", "ToastBackupDeleteFailed": "Не ўдалося выдаліць рэзервовую копію", @@ -819,12 +935,15 @@ "ToastBackupRestoreFailed": "Не ўдалося аднавіць рэзервовую копію", "ToastBackupUploadFailed": "Не ўдалося запампаваць рэзервовую копію", "ToastBackupUploadSuccess": "Рэзервовая копія запампавана", + "ToastBatchApplyDetailsToItemsSuccess": "Звесткі прыменены да элементаў", "ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку", "ToastBookmarkCreateSuccess": "Закладка дададзена", "ToastBookmarkRemoveSuccess": "Закладка выдалена", "ToastBulkChapterInvalidCount": "Увядзіце лік ад 1 да 150", + "ToastCachePurgeFailed": "Не ўдалося ачысціць кэш", "ToastCachePurgeSuccess": "Кэш паспяхова ачышчаны", "ToastChapterLocked": "Раздзел заблакіраваны.", + "ToastChaptersHaveErrors": "Раздзелы маюць памылкі", "ToastChaptersMustHaveTitles": "Раздзелы павінны мець загалоўкі", "ToastChaptersRemoved": "Раздзелы выдалены", "ToastChaptersUpdated": "Раздзелы абноўлены", @@ -835,15 +954,18 @@ "ToastCoverSearchFailed": "Не ўдалося знайсці вокладку", "ToastCoverUpdateFailed": "Не ўдалося абнавіць вокладку", "ToastDateTimeInvalidOrIncomplete": "Дата і час указаны некарэктна або не цалкам", + "ToastDeleteFileFailed": "Не ўдалося выдаліць файл", "ToastDeleteFileSuccess": "Файл выдалены", "ToastDeviceAddFailed": "Не ўдалося дадаць прыладу", "ToastDeviceNameAlreadyExists": "Прылада для чытання электронных кніг з такой назвай ужо існуе", "ToastDeviceTestEmailFailed": "Не ўдалося адправіць тэставае электроннае пісьмо", + "ToastDeviceTestEmailSuccess": "Тэставы электронны ліст адпраўлены", "ToastEmailSettingsUpdateSuccess": "Налады электроннай пошты абноўлены", "ToastEncodeCancelFailed": "Не ўдалося скасаваць кадаванне", "ToastEncodeCancelSucces": "Кадаванне скасавана", "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", - "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана", + "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўвання выпускаў ачышчана", + "ToastEpisodeUpdateSuccess": "Абноўлена выпускаў: {0}", "ToastFailedToCreate": "Не ўдалося стварыць", "ToastFailedToDelete": "Не ўдалося выдаліць", "ToastFailedToLoadData": "Не ўдалося загрузіць даныя", @@ -851,7 +973,7 @@ "ToastFailedToShare": "Не ўдалося абагуліць", "ToastFailedToUpdate": "Не здалося абнавіць", "ToastInvalidImageUrl": "Памылковы URL-адрас відарыса", - "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі", + "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць выпускаў для спампоўвання", "ToastItemCoverUpdateSuccess": "Вокладка элемента абноўлена", "ToastItemDeletedFailed": "Не ўдалося выдаліць элемент", "ToastItemDeletedSuccess": "Выдалены элемент", @@ -876,15 +998,24 @@ "ToastMustHaveAtLeastOnePath": "Павінен быць хаця б адзін шлях", "ToastNameEmailRequired": "Імя і электронная пошта абавязковыя", "ToastNameRequired": "Імя абавязковае", + "ToastNewApiKeyUserError": "Трэба выбраць карыстальніка", + "ToastNewEpisodesFound": "Знойдзена новых выпускаў: {0}", "ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"", "ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны", + "ToastNewUserLibraryError": "Трэба выбраць хаця б адну бібліятэку", + "ToastNewUserPasswordError": "Мусіць мець пароль, толькі карыстальнік root можа мець пусты пароль", "ToastNewUserTagError": "Трэбаа выбраць хаця б адзін тэг", "ToastNewUserUsernameError": "Увядзіце імя карыстальніка", + "ToastNoNewEpisodesFound": "Новых выпускаў не знойдзена", "ToastNoRSSFeed": "У падкаста няма RSS-стужкі", "ToastNoUpdatesNecessary": "Абнаўленні не патрэбныя", "ToastNotificationCreateFailed": "Не ўдалося стварыць апавяшчэнне", "ToastNotificationDeleteFailed": "Не ўдалося выдаліць апавяшчэнне", + "ToastNotificationFailedMaximum": "Максімальная колькасць няўдалых спроб павінна быць >= 0", + "ToastNotificationQueueMaximum": "Максімальная чарга апавяшчэнняў павінна быць >= 0", "ToastNotificationSettingsUpdateSuccess": "Налады апавяшчэнняў абноўлены", + "ToastNotificationTestTriggerFailed": "Не ўдалося ўключыць тэставае апавяшчэнне", + "ToastNotificationTestTriggerSuccess": "Уключана тэставае апавяшчэнне", "ToastNotificationUpdateSuccess": "Апавяшчэнне абноўлена", "ToastPlaylistCreateFailed": "Не ўдалося стварыць плэй-ліст", "ToastPlaylistCreateSuccess": "Плэй-ліст створаны", @@ -892,8 +1023,9 @@ "ToastPlaylistUpdateSuccess": "Плэй-ліст абноўлены", "ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст", "ToastPodcastCreateSuccess": "Падкаст паспяхова створаны", + "ToastPodcastEpisodeUpdated": "Выпуск абноўлены", "ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста", - "ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў", + "ToastPodcastNoEpisodesInFeed": "У RSS-ленце не знойдзена выпускаў", "ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі", "ToastProgressIsNotBeingSynced": "Прагрэс не сінхранізуецца, перазапусціце прайграванне", "ToastProviderCreatedFailed": "Не ўдалося дадаць пастаўшчыка", @@ -905,16 +1037,24 @@ "ToastRemoveFailed": "Не ўдалося выдаліць", "ToastRemoveItemFromCollectionFailed": "Не ўдалося выдаліць элемент з калекцыі", "ToastRemoveItemFromCollectionSuccess": "Элемент выдалены з калекцыі", + "ToastRemoveItemsWithIssuesFailed": "Не ўдалося выдаліць элементы бібліятэкі з праблемамі", + "ToastRemoveItemsWithIssuesSuccess": "Выдалены элементы бібліятэкі з праблемамі", "ToastRenameFailed": "Не ўдалося перайменаваць", + "ToastScanFailed": "Не ўдалося адсканіраваць элемент бібліятэкі", "ToastSelectAtLeastOneUser": "Выберыце прынамсі аднаго карыстальніка", "ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу", "ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Немагчыма дадаць дзве серыі з аднолькавай назвай", + "ToastServerSettingsUpdateSuccess": "Налады сервера абноўлены", "ToastSessionCloseFailed": "Не ўдалося закрыць сеанс", "ToastSessionDeleteFailed": "Не ўдалося выдаліць сеанс", "ToastSessionDeleteSuccess": "Сеанс выдалены", "ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р", + "ToastSortingPrefixesEmptyError": "Мусіць мець хаця б адзін прэфікс сартавання", + "ToastSortingPrefixesUpdateSuccess": "Прэфіксы сартавання абноўлены ({0} элементаў)", "ToastTitleRequired": "Загаловак абавязковы", "ToastUnknownError": "Невядомая памылка", + "ToastUploaderFilepathExistsError": "Файл \"{0}\" ужо існуе на серверы", "ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.", "ToastUserDeleteFailed": "Не ўдалося выдаліць карыстальніка", "ToastUserDeleteSuccess": "Карыстальнік выдалены", @@ -924,8 +1064,10 @@ "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара", "TooltipAddChapters": "Дадаць раздзел(ы)", "TooltipAddOneSecond": "Дадаць 1 секунду", + "TooltipAdjustChapterStart": "Націсніце, каб наладзіць час пачатку", "TooltipLockAllChapters": "Заблакіраваць усе раздзелы", "TooltipLockChapter": "Заблакіраваць раздзел (Shift+націсканне для дыяпазону)", "TooltipSubtractOneSecond": "Адняць 1 секунду", - "TooltipUnlockAllChapters": "Разблакіраваць усе раздзелы" + "TooltipUnlockAllChapters": "Разблакіраваць усе раздзелы", + "TooltipUnlockChapter": "Разблакіраваць раздзел (Shift+націсканне для выбару дыяпазону)" } From 9afa39e29e55c3b94a218e6242c1fa9646f04435 Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Thu, 26 Feb 2026 13:42:50 +0100 Subject: [PATCH 185/285] Translated using Weblate (Belarusian) 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/be/ --- client/strings/be.json | 104 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index cc5dc7bc5..6dbef9cb9 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -36,7 +36,7 @@ "ButtonEnable": "Уключыць", "ButtonFireAndFail": "Агонь і няўдача", "ButtonFireOnTest": "Тэст на вогнеўстойлівасць", - "ButtonForceReScan": "Прымусовае паўторнае сканаванне", + "ButtonForceReScan": "Прымусова паўторна сканіраваць", "ButtonFullPath": "Поўны шлях", "ButtonHide": "Схаваць", "ButtonHome": "Галоўная", @@ -73,7 +73,7 @@ "ButtonQuickEmbed": "Хуткае ўбудаванне", "ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метаданых", "ButtonQuickMatch": "Хуткі пошук", - "ButtonReScan": "Паўторнае сканаванне", + "ButtonReScan": "Паўторна сканіраваць", "ButtonRead": "Чытаць", "ButtonReadLess": "Чытаць менш", "ButtonReadMore": "Чытаць больш", @@ -314,7 +314,7 @@ "LabelDeviceInfo": "Інфармацыя пра прыладу", "LabelDeviceIsAvailableTo": "Прылада даступная для...", "LabelDirectory": "Каталог", - "LabelDiscFromFilename": "Дыск з назвы файла", + "LabelDiscFromFilename": "Дыск з файла", "LabelDiscFromMetadata": "Дыск з метаданых", "LabelDiscover": "Знайсці", "LabelDownload": "Спампаваць", @@ -345,6 +345,7 @@ "LabelEncodingStartedNavigation": "Пасля запуску задачы вы можаце перайсці на іншую старонку.", "LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.", "LabelEncodingWarningAdvancedSettings": "Увага: Не абнаўляйце гэтыя налады, калі вы не знаёмыя з параметрамі кадавання ffmpeg.", + "LabelEncodingWatcherDisabled": "Калі ў вас адключана адсочванне змен у папцы, пасля трэба будзе паўторна сканіраваць гэту аўдыякнігу.", "LabelEnd": "Канец", "LabelEndOfChapter": "Канец раздзела", "LabelEpisode": "Выпуск", @@ -390,6 +391,7 @@ "LabelFull": "Поўны", "LabelGenre": "Жанр", "LabelGenres": "Жанры", + "LabelHardDeleteFile": "Жорстка выдаляць файл", "LabelHasEbook": "Мае электронную кнігу", "LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу", "LabelHideSubtitles": "Схаваць падзагалоўкі", @@ -414,6 +416,8 @@ "LabelIntervalEveryMinute": "Кожную хвіліну", "LabelInvert": "Інвертаваць", "LabelItem": "Элемент", + "LabelJumpBackwardAmount": "Час пераходу назад", + "LabelJumpForwardAmount": "Час пераходу наперад", "LabelLanguage": "Мова", "LabelLanguageDefaultServer": "Прадвызначаная мова сервера", "LabelLanguages": "Мовы", @@ -444,6 +448,7 @@ "LabelLookForNewEpisodesAfterDate": "Шукаць новыя выпускі пасля гэтай даты", "LabelLowestPriority": "Найніжэйшы прыярытэт", "LabelMatchConfidence": "Упэўненасць", + "LabelMatchExistingUsersBy": "Параўноўваць існуючых карыстальнікаў па", "LabelMatchExistingUsersByDescription": "Выкарыстоўваецца для падключэння існуючых карыстальнікаў. Пасля падключэння карыстальнікі будуць супастаўляцца з дапамогай унікальнага ідэнтыфікатара ад пастаўшчыка SSO", "LabelMaxEpisodesToDownload": "Максімальная колькасць выпускаў для спампоўвання. 0 – неабмежаваная колькасць.", "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых выпускаў для спампоўвання за праверку", @@ -479,6 +484,7 @@ "LabelNoEpisodesSelected": "Не выбрана ніводнага выпуску", "LabelNotFinished": "Незавершана", "LabelNotStarted": "Не пачата", + "LabelNotes": "Заўвагі", "LabelNotificationAppriseURL": "URL-адрасы Apprise", "LabelNotificationAvailableVariables": "Даступныя пераменныя", "LabelNotificationBodyTemplate": "Шаблон зместу", @@ -491,6 +497,9 @@ "LabelNumberOfBooks": "Колькасць кніг", "LabelNumberOfChapters": "Колькасць раздзелаў:", "LabelNumberOfEpisodes": "Колькасць выпускаў", + "LabelOpenIDAdvancedPermsClaimDescription": "Назва прэтэнзіі OpenID, якая змяшчае пашыраныя дазволы для дзеянняў карыстальніка ў праграме, якія будуць прымяняцца да роляў, якія не з'яўляюцца адміністратарамі (калі наладжана). Калі прэтэнзія адсутнічае ў адказе, доступ да ABS будзе забаронены. Калі адсутнічае адзін параметр, ён будзе разглядацца як false. Пераканайцеся, што прэтэнзія пастаўшчыка ідэнтыфікацыі адпавядае чаканай структуры:", + "LabelOpenIDClaims": "Пакіньце наступныя параметры пустымі, каб адключыць пашыранае прызначэнне груп і дазволаў, аўтаматычна прызначаючы групу \"Карыстальнік\".", + "LabelOpenIDGroupClaimDescription": "Назва прэтэнзіі OpenID, якая змяшчае спіс груп карыстальніка. Звычайна іх называюць групамі. Калі наладжана, праграма будзе аўтаматычна прызначаць ролі на аснове членства карыстальніка ў групах, пры ўмове, што ў прэтэнзіі гэтыя групы названы без уліку рэгістра: \"адміністратар\", \"карыстальнік або \"госць\". Прэтэнзія павінна ўтрымліваць спіс, і калі карыстальнік належыць да некалькіх груп, праграма прызначыць ролю, якая адпавядае найвышэйшаму ўзроўню доступу. Калі ніводная група не супадае, доступ будзе забаронены.", "LabelOpenRSSFeed": "Адкрыць RSS-стужку", "LabelOverwrite": "Перазапісаць", "LabelPaginationPageXOfY": "Старонка {0} з {1}", @@ -508,6 +517,7 @@ "LabelPersonalYearReview": "Вынікі года ({0})", "LabelPhotoPathURL": "Шлях/URL-адрас фота", "LabelPlayMethod": "Метад прайгравання", + "LabelPlaybackRateIncrementDecrement": "Павелічэнне/памяншэнне хуткасці прайгравання", "LabelPlayerChapterNumberMarker": "{0} з {1}", "LabelPlaylists": "Плэй-лісты", "LabelPodcast": "Падкаст", @@ -515,13 +525,17 @@ "LabelPodcastType": "Тып падкаста", "LabelPodcasts": "Падкасты", "LabelPort": "Порт", + "LabelPrefixesToIgnore": "Прэфіксы, якія трэба ігнараваць (без уліку рэгістра)", "LabelPreventIndexing": "Прадухіліць індэксацыю вашай стужкі каталогамі падкастаў iTunes і Google", + "LabelPrimaryEbook": "Асноўная электронная кніга", "LabelProgress": "Прагрэс", "LabelProvider": "Пастаўшчык", "LabelProviderAuthorizationValue": "Значэнне загалоўка аўтарызацыі", "LabelPubDate": "Дата публікацыі", "LabelPublishYear": "Год публікацыі", "LabelPublishedDate": "Апублікавана {0}", + "LabelPublishedDecade": "Дзесяцігоддзе публікацыі", + "LabelPublishedDecades": "Дзесяцігоддзі публікацыі", "LabelPublisher": "Выдавец", "LabelPublishers": "Выдаўцы", "LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка", @@ -540,8 +554,10 @@ "LabelRecommended": "Рэкамендаваныя", "LabelRedo": "Узнавіць", "LabelRegion": "Рэгіён", + "LabelReleaseDate": "Дата выпуску", "LabelRemoveAllMetadataAbs": "Выдаліць усе файлы metadata.abs", "LabelRemoveAllMetadataJson": "Выдаліць усе файлы metadata.json", + "LabelRemoveAudibleBranding": "Выдаляць уступленне і завяршэнне Audible з раздзелаў", "LabelRemoveCover": "Выдаліць вокладку", "LabelRemoveMetadataFile": "Выдаліць файлы метаданых у папках элементаў бібліятэкі", "LabelRemoveMetadataFileHelp": "Выдаліць усе файлы metadata.json і metadata.abs у {0} папках.", @@ -570,7 +586,9 @@ "LabelSettingsAudiobooksOnly": "Толькі аўдыякнігі", "LabelSettingsAudiobooksOnlyHelp": "Пры ўключэнні гэтай налады файлы электронных кніг будуць ігнаравацца, калі толькі яны не знаходзяцца ў папцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.", "LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі", + "LabelSettingsChromecastSupport": "Падтрымка Chromecast", "LabelSettingsDateFormat": "Фарматы даты", + "LabelSettingsEnableWatcher": "Аўтаматычна сачыць за зменамі ў бібліятэцках", "LabelSettingsEnableWatcherForLibrary": "Аўтаматычна сачыць за зменамі ў бібліятэцы", "LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера", "LabelSettingsEpubsAllowScriptedContent": "Дазваляць скрыпты ў файлах EPUB", @@ -592,21 +610,30 @@ "LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў папак аўдыякніг.
Падзагаловак павінен быць аддзелены сімвалам \" - \".
Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"", "LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метаданым", "LabelSettingsPreferMatchedMetadataHelp": "Супадаючыя даныя будуць замяняць звесткі элемента пры выкарыстанні функцыі Хуткі пошук. Прадвызначана Хуткі пошук запаўняе толькі адсутныя звесткі.", + "LabelSettingsSkipMatchingBooksWithASIN": "Прапусціць параўнанне кніг, якія ўжо маюць ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Прапусціць параўнанне кніг, якія ўжо маюць ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ігнараваць прэфіксы пры сартаванні", "LabelSettingsSortingIgnorePrefixesHelp": "напрыклад, для прэфікса \"the\" загаловак кнігі \"The Book Title\" будзе сартавацца як \"Book Title, The\"", "LabelSettingsSquareBookCovers": "Выкарыстоўваць квадратныя вокладкі кніг", + "LabelSettingsSquareBookCoversHelp": "Аддаваць перавагу квадратным вокладкам замест стандартных вокладак з суадносінамі бакоў 1.6:1", + "LabelSettingsStoreCoversWithItem": "Захоўваць вокладкі з элементам", "LabelSettingsStoreCoversWithItemHelp": "Прадвызначана вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у папцы элемента бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"", "LabelSettingsStoreMetadataWithItem": "Захоўваць метаданыя разам з элементам", "LabelSettingsStoreMetadataWithItemHelp": "Прадвызначана метаданыя захоўваюцца ў /metadata/items. Пры ўключэнні гэтай опцыі файлаў метаданых будуць захоўвацца ў папках элементаў бібліятэкі", "LabelSettingsTimeFormat": "Фармат часу", "LabelShare": "Абагуліць", "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку, спампоўваць ZIP-архіў элемента бібліятэкі.", + "LabelShareOpen": "Абагульванне адкрыта", + "LabelShareURL": "URL-адрас для абагульвання", "LabelShowAll": "Паказаць усё", "LabelShowSeconds": "Паказваць секунды", "LabelShowSubtitles": "Паказаць падзагалоўкі", "LabelSize": "Памер", "LabelSleepTimer": "Таймер сну", + "LabelSlug": "Ідэнтыфікатар", "LabelSortAscending": "Па ўзрастанні", "LabelSortDescending": "Па ўбыванні", + "LabelSortPubDate": "Сартаваць па даце публікацыі", "LabelStart": "Пачаць", "LabelStartTime": "Час пачатку", "LabelStarted": "Пачата", @@ -662,7 +689,7 @@ "LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метаданымі, відарысам вокладкі і раздзеламі.", "LabelTotalDuration": "Агульная працягласць", "LabelTotalTimeListened": "Агульны час праслухоўвання", - "LabelTrackFromFilename": "Трэк з назвы файла", + "LabelTrackFromFilename": "Трэк з файла", "LabelTrackFromMetadata": "Трэк з метаданых", "LabelTracks": "Трэкі", "LabelTracksMultiTrack": "Некалькі трэкаў", @@ -703,12 +730,14 @@ "LabelXItems": "{0} элементаў", "LabelYearReviewHide": "Схаваць вынікі года", "LabelYearReviewShow": "Азнаёміцца з вынікамі года", - "LabelYourAudiobookDuration": "Працягласць вашай аўдыякнігі", + "LabelYourAudiobookDuration": "Працягласць аўдыякнігі", "LabelYourBookmarks": "Вашы закладкі", "LabelYourPlaylists": "Вашы плэй-лісты", "LabelYourProgress": "Ваш прагрэс", "MessageAddToPlayerQueue": "Дадаць у чаргу прайгравальніка", "MessageAppriseDescription": "Каб выкарыстоўваць гэтую функцыю, вам спатрэбіцца запусціць асобнік Apprise API або API, які будзе апрацоўваць тыя ж запыты.
URL Apprise API павінен быць поўным шляхам для адпраўкі апавяшчэння, напрыклад, калі ваш API працуе па адрасе http://192.168.1.1:8337, то вы павінны ўвесці http://192.168.1.1:8337/notify.", + "MessageAsinCheck": "Пераканайцеся, што выкарыстоўваеце ASIN з правільнага рэгіёна Audible, а не Amazon.", + "MessageAuthenticationLegacyTokenWarning": "Састарэлыя токены API будуць выдалены ў будучыні. Замест іх выкарыстоўвайце ключы API.", "MessageAuthenticationOIDCChangesRestart": "Перазапусціце сервер пасля захавання, каб прымяніць змены OIDC.", "MessageAuthenticationSecurityMessage": "Дзеля бяспекі была палепшана аўтэнтыфікацыя. Усім карыстальнікам трэба паўторна ўвайсці ў сістэму.", "MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і відарысы, якія захоўваюцца ў /metadata/items і /metadata/authors. Рэзервовыя копіі не ўключаюць файлы, якія захоўваюцца ў папках бібліятэкі.", @@ -724,9 +753,12 @@ "MessageBookshelfNoResultsForFilter": "Няма вынікаў для фільтра \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Няма вынікаў па запыце", "MessageBookshelfNoSeries": "У вас няма серый", + "MessageBulkChapterPattern": "Колькі раздзелаў вы хочаце дадаць з дапамогай гэтага ўзору нумарацыі?", + "MessageChapterEndIsAfter": "Канец раздзела ідзе пасля канца аўдыякнігі", "MessageChapterErrorFirstNotZero": "Першы раздзел павінен пачынацца з 0", "MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі", "MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела", + "MessageChapterStartIsAfter": "Пачатак раздзела ідзе пасля канца аўдыякнігі", "MessageChaptersNotFound": "Раздзелы не знойдзены", "MessageCheckingCron": "Праверка cron...", "MessageConfirmCloseFeed": "Вы ўпэўнены, што хочаце закрыць гэту стужку?", @@ -741,19 +773,24 @@ "MessageConfirmDeleteNotification": "Вы ўпэўнены, што хочаце выдаліць гэта апавяшчэнне?", "MessageConfirmDeleteSession": "Вы ўпэўнены, што хочаце выдаліць гэты сеанс?", "MessageConfirmEmbedMetadataInAudioFiles": "Вы ўпэўнены, што хочаце ўбудаваць метаданыя ў {0} аўдыяфайлаў?", + "MessageConfirmForceReScan": "Вы ўпэўнены, што хочаце прымусова паўторна сканіраваць?", "MessageConfirmMarkAllEpisodesFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як завершаныя?", "MessageConfirmMarkAllEpisodesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як незавершаныя?", "MessageConfirmMarkItemFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як завершаны?", "MessageConfirmMarkItemNotFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як незавершаны?", "MessageConfirmMarkSeriesFinished": "Вы ўпэўнены, што хочаце пазначыць усе кнігі ў гэтай серыі як завершаныя?", "MessageConfirmMarkSeriesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе кнігі ў гэтай серыі як незавершаныя?", + "MessageConfirmNotificationTestTrigger": "Уключыць гэта апавяшчэнне з тэставымі данымі?", "MessageConfirmPurgeCache": "Ачышчэнне кэшу выдаліць увесь каталог па адрасе /metadata/cache.

Вы ўпэўнены, што хочаце выдаліць каталог кэшу?", "MessageConfirmPurgeItemsCache": "Ачышчэнне кэшу элементаў выдаліць увесь каталог па адрасе /metadata/cache/items.
Вы ўпэўнены?", + "MessageConfirmQuickEmbed": "Увага! Хуткае ўбудаванне не стварае рэзервовых копій аўдыяфайлаў. Пераканайцеся, што ў вас ёсць рэзервовая копія аўдыяфайлаў.

Хочаце працягнуць?", "MessageConfirmQuickMatchEpisodes": "Хуткае супадзенне выпускаў перазапіша дэталі, калі супадзенне будзе знойдзена. Будуць абноўлены толькі выпускі, якія не супадаюць. Вы ўпэўнены?", + "MessageConfirmReScanLibraryItems": "Вы ўпэўнены, што хочаце паўторна сканіраваць {0} элементаў?", "MessageConfirmRemoveAllChapters": "Вы ўпэўнены, што хочаце выдаліць усе раздзелы?", "MessageConfirmRemoveAuthor": "Вы ўпэўнены, што хочаце выдаліць аўтара \"{0}\"?", "MessageConfirmRemoveCollection": "Вы ўпэўнены, што хочаце выдаліць калекцыю \"{0}\"?", "MessageConfirmRemoveEpisode": "Вы ўпэўнены, што хочаце выдаліць выпуск \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Заўвага: Аўдыяфайл не будзе выдалены, калі не ўключыць параметр \"Жорстка выдаляць файл\"", "MessageConfirmRemoveEpisodes": "Вы ўпэўнены, што хочаце выдаліць {0} выпускаў?", "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што хочаце выдаліць {0} сеансаў праслухоўвання?", "MessageConfirmRemoveMetadataFiles": "Вы ўпэўнены, што хочаце выдаліць усе файлы metadata.{0} у папках элементаў бібліятэкі?", @@ -767,14 +804,19 @@ "MessageConfirmRenameTagWarning": "Увага! Падобны тэг з іншым рэгістрам ужо існуе: \"{0}\".", "MessageConfirmResetProgress": "Вы ўпэўнены, што хочаце скінуць свой прагрэс?", "MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?", + "MessageConfirmUnlinkOpenId": "Вы ўпэўнены, што хочаце адвязаць гэтага карыстальніка ад OpenID?", + "MessageDaysListenedInTheLastYear": "{0} дзён праслухоўвання за апошні год", "MessageDownloadingEpisode": "Спампоўванне выпуску", "MessageDragFilesIntoTrackOrder": "Перацягніце файлы ў правільным парадку трэкаў", + "MessageEmbedFailed": "Не ўдалося ўбудаваць!", "MessageEmbedFinished": "Убудаванне завершана!", "MessageEmbedQueue": "У чарзе на ўбудаванне метаданых (у чарзе {0})", "MessageEpisodesQueuedForDownload": "{0} выпуск(-аў) у чарзе спампоўвання", "MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.", "MessageFeedURLWillBe": "URL стужкі будзе {0}", "MessageFetching": "Атрыманне...", + "MessageForceReScanDescription": "прасканіруе ўсе файлы зноў, як пры новым сканаванні. Тэгі ID3 аўдыёфайлаў, файлы OPF і тэкставыя файлы будуць сканіравацца як новыя.", + "MessageHeatmapListeningTimeTooltip": "{0} праслухана on {1}", "MessageHeatmapNoListeningSessions": "Няма сеансаў праслухоўвання на {0}", "MessageImportantNotice": "Важная заўвага!", "MessageInsertChapterBelow": "Уставіць раздзел ніжэй", @@ -785,6 +827,7 @@ "MessageLoading": "Загрузка...", "MessageLoadingFolders": "Загрузка папак...", "MessageLogsDescription": "Журналы захоўваюцца ў каталогу /metadata/logs у фармаце JSON. Журналы памылак захоўваюцца ў файле /metadata/logs/crashlogs.txt.", + "MessageM4BFailed": "Памылка M4B!", "MessageM4BFinished": "M4B завершана!", "MessageMapChapterTitles": "Супаставіць загалоўкі раздзелаў з існуючымі раздзеламі аўдыякнігі без змянення пазнак часу", "MessageMarkAllEpisodesFinished": "Пазначыць усе выпускі як завершаныя", @@ -834,6 +877,8 @@ "MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення", "MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі", "MessageQuickEmbedInProgress": "Выконваецца хуткае ўбудаванне", + "MessageQuickEmbedQueue": "Пастаўлена ў чаргу для хуткага ўбудавання ({0} у чарзе)", + "MessageQuickMatchAllEpisodes": "Хуткае параўнанне ўсіх выпускаў", "MessageQuickMatchDescription": "Запоўніць пустыя звесткі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе звесткіі, калі параметр \"Аддаваць перавагу супадаючым метаданым\" на серверы не ўключана.", "MessageRemoveChapter": "Выдаліць раздзел", "MessageRemoveEpisodes": "Выдаліць выпускі ({0})", @@ -843,6 +888,7 @@ "MessageResetChaptersConfirm": "Вы ўпэўнены, што хочаце скінуць раздзелы і адрабіць зробленыя вамі змены?", "MessageRestoreBackupConfirm": "Вы ўпэўнены, што хочаце аднавіць рэзервовую копію, створаную", "MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама відарысы вокладкі ў /metadata/items і /metadata/authors.

Рэзервовыя копіі не змяняюць файлы ў папках бібліятэкі. Калі вы ўключылі налады сервера для захоўвання воклак і метаданых у папках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца.

Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.", + "MessageScheduleLibraryScanNote": "Большасці карыстальнікаў рэкамендуецца не выключаць гэтую функцыю і пакідаць уключанай наладу \"Аўтаматычна сачыць за зменамі ў бібліятэцы\" — яна будзе аўтаматычна выяўляць змены ў папках бібліятэкі. Уключыце гэту функцыю, калі \"Аўтаматычна сачыць за зменамі ў бібліятэцы\" не працуе для вашай файлавай сістэмы (напрыклад, NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}", "MessageSearchResultsFor": "Вынікі пошуку для", "MessageSelected": "Выбрана: {0}", @@ -851,6 +897,7 @@ "MessageSetChaptersFromTracksDescription": "Задаць раздзелы, выкарыстоўваючы кожны аўдыяфайл у якасці раздзела і назву аўдыяфайла ў якасці загалоўка раздзела", "MessageShareExpirationWillBe": "Тэрмін дзеяння будзе {0}", "MessageShareExpiresIn": "Тэрмін дзеяння заканчваецца праз {0}", + "MessageShareURLWillBe": "URL-адрас для абагульвання будзе {0", "MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?", "MessageTaskAudioFileNotWritable": "Аўдыяфайл \"{0}\" недаступны для запісу", "MessageTaskCanceledByUser": "Задача скасавана карыстальнікам", @@ -887,10 +934,25 @@ "MessageTaskScanningFileChanges": "Сканіраванне змяненняў у файле \"{0}\"", "MessageTaskScanningLibrary": "Сканіраванне бібліятэкі \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "Мэтавы каталог недаступны для запісу", + "MessageThinking": "Думаю...", + "MessageUploaderItemFailed": "Не ўдалося запампаваць", + "MessageUploaderItemSuccess": "Паспяхова запампавана!", + "MessageUploading": "Запампоўванне...", + "MessageValidCronExpression": "Карэктны выраз cron", + "MessageWatcherIsDisabledGlobally": "Адсочванне змен у папках адключана глабальна ў наладах сервера", + "MessageXLibraryIsEmpty": "{0} Бібліятэка пустая!", + "MessageYourAudiobookDurationIsLonger": "Працягласць аўдыякнігі большая за знойдзеную працягласць", + "MessageYourAudiobookDurationIsShorter": "Працягласць аўдыякнігі карацейшая за знойдзеную працягласць", + "NoteChangeRootPassword": "Толькі карыстальнік root можа мець пусты пароль", "NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.", + "NoteFolderPicker": "Заўвага: ужо супастаўленыя папкі адлюстроўвацца не будуць", "NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш выпускаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", "NoteUploaderFoldersWithMediaFiles": "Папкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.", + "NoteUploaderOnlyAudioFiles": "Пры запампоўванні толькі аўдыяфайлаў кожны аўдыяфайл будзе апрацоўвацца як асобная аўдыякніга.", + "NoteUploaderUnsupportedFiles": "Файлы, якія не падтрымліваюцца, ігнаруюцца. Пры выбары або выдаленні папкі іншыя файлы, якія не знаходзяцца ў папцы элемента, ігнаруюцца.", + "NotificationOnBackupCompletedDescription": "Спрацоўвае пасля завяршэння рэзервовага капіравання", + "NotificationOnBackupFailedDescription": "Спрацоўвае пры збоі рэзервовага капіравання", "NotificationOnEpisodeDownloadedDescription": "Спрацоўвае, калі выпуск падкаста аўтаматычна спампоўваецца", "NotificationOnRSSFeedDisabledDescription": "Спрацоўвае, калі аўтаматычнае спампоўванне выпускаў адключана з-за занадта вялікай колькасці няўдалых спроб", "NotificationOnRSSFeedFailedDescription": "Спрацоўвае, пры памылцы запыту RSS-стужкі для аўтаматычнага спампоўвання выпуску", @@ -903,11 +965,13 @@ "PlaceholderSearchEpisode": "Пошук выпуску...", "StatsAuthorsAdded": "дададзена аўтараў", "StatsBooksAdded": "дададзена кніг", + "StatsBooksAdditional": "Некаторыя дапаўненні ўключаюць…", "StatsBooksFinished": "завершана кніг", "StatsBooksFinishedThisYear": "Некаторыя кнігі завершаны ў гэтым годзе…", "StatsBooksListenedTo": "кнігі, якія былі праслуханы", "StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…", "StatsSessions": "сеансаў", + "StatsSpentListening": "праслухана", "StatsTopAuthor": "ТОП АЎТАР", "StatsTopAuthors": "ТОП АЎТАРЫ", "StatsTopGenre": "ТОП ЖАНР", @@ -915,6 +979,8 @@ "StatsTopMonth": "ТОП МЕСЯЦ", "StatsTopNarrator": "ТОП ДЫКТАР", "StatsTopNarrators": "ТОП ДЫКТАРЫ", + "StatsTotalDuration": "З агульнай працягласцю…", + "StatsYearInReview": "ВЫНІКІ ГОДА", "ToastAccountUpdateSuccess": "Уліковы запіс абноўлены", "ToastAppriseUrlRequired": "Неабходна ўвесці URL-адрас Apprise", "ToastAsinRequired": "ASIN абавязковы", @@ -936,6 +1002,12 @@ "ToastBackupUploadFailed": "Не ўдалося запампаваць рэзервовую копію", "ToastBackupUploadSuccess": "Рэзервовая копія запампавана", "ToastBatchApplyDetailsToItemsSuccess": "Звесткі прыменены да элементаў", + "ToastBatchDeleteFailed": "Памылка групавога выдалення", + "ToastBatchDeleteSuccess": "Групавое выдаленне выканана", + "ToastBatchQuickMatchFailed": "Памылка групавога хуткага параўнання!", + "ToastBatchQuickMatchStarted": "Групавое хуткае параўнанне {0} кніг запушчана!", + "ToastBatchUpdateFailed": "Памылка групавога абнаўлення", + "ToastBatchUpdateSuccess": "Групавое абнаўленне выканана", "ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку", "ToastBookmarkCreateSuccess": "Закладка дададзена", "ToastBookmarkRemoveSuccess": "Закладка выдалена", @@ -943,7 +1015,11 @@ "ToastCachePurgeFailed": "Не ўдалося ачысціць кэш", "ToastCachePurgeSuccess": "Кэш паспяхова ачышчаны", "ToastChapterLocked": "Раздзел заблакіраваны.", + "ToastChapterStartTimeAdjusted": "Час пачатку раздзела адкарэктаваны на {0} секунд", + "ToastChaptersAllLocked": "Усе раздзелы заблакіраваны. Разблакіруйце некаторыя раздзелы, каб зрушыць іх час.", "ToastChaptersHaveErrors": "Раздзелы маюць памылкі", + "ToastChaptersInvalidShiftAmountLast": "Памылковая велічыня зруху. Час пачатку апошняга раздзела перавышае працягласць гэтай аўдыякнігі.", + "ToastChaptersInvalidShiftAmountStart": "Памылковая велічыня зруху. Першы раздзел будзе мець нулявую або адмоўную працягласць і будзе перазапісаны другім раздзелам. Павялічце пачатковую працягласць другога раздзела.", "ToastChaptersMustHaveTitles": "Раздзелы павінны мець загалоўкі", "ToastChaptersRemoved": "Раздзелы выдалены", "ToastChaptersUpdated": "Раздзелы абноўлены", @@ -966,6 +1042,7 @@ "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўвання выпускаў ачышчана", "ToastEpisodeUpdateSuccess": "Абноўлена выпускаў: {0}", + "ToastErrorCannotShare": "Немагчыма абагуліць на гэтай прыладзе", "ToastFailedToCreate": "Не ўдалося стварыць", "ToastFailedToDelete": "Не ўдалося выдаліць", "ToastFailedToLoadData": "Не ўдалося загрузіць даныя", @@ -974,6 +1051,8 @@ "ToastFailedToUpdate": "Не здалося абнавіць", "ToastInvalidImageUrl": "Памылковы URL-адрас відарыса", "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць выпускаў для спампоўвання", + "ToastInvalidUrl": "Памылковы URL-адрас", + "ToastInvalidUrls": "Адзін або некалькі URL-адрасоў памылковыя", "ToastItemCoverUpdateSuccess": "Вокладка элемента абноўлена", "ToastItemDeletedFailed": "Не ўдалося выдаліць элемент", "ToastItemDeletedSuccess": "Выдалены элемент", @@ -1040,20 +1119,33 @@ "ToastRemoveItemsWithIssuesFailed": "Не ўдалося выдаліць элементы бібліятэкі з праблемамі", "ToastRemoveItemsWithIssuesSuccess": "Выдалены элементы бібліятэкі з праблемамі", "ToastRenameFailed": "Не ўдалося перайменаваць", + "ToastRescanFailed": "Не ўдалося паўторна прасканіраваць {0}", + "ToastRescanRemoved": "Паўторнае сканаванне завершана, элемент быў выдалены", + "ToastRescanUpToDate": "Паўторнае сканаванне завершана, элемент быў у актуальным стане", + "ToastRescanUpdated": "Паўторнае сканаванне завершана, элемент быў абноўлены", "ToastScanFailed": "Не ўдалося адсканіраваць элемент бібліятэкі", "ToastSelectAtLeastOneUser": "Выберыце прынамсі аднаго карыстальніка", "ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу", "ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"", "ToastSeriesSubmitFailedSameName": "Немагчыма дадаць дзве серыі з аднолькавай назвай", + "ToastSeriesUpdateFailed": "Не ўдалося абнавіць серыі", + "ToastSeriesUpdateSuccess": "Серыі абноўлены", "ToastServerSettingsUpdateSuccess": "Налады сервера абноўлены", "ToastSessionCloseFailed": "Не ўдалося закрыць сеанс", "ToastSessionDeleteFailed": "Не ўдалося выдаліць сеанс", "ToastSessionDeleteSuccess": "Сеанс выдалены", "ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р", + "ToastSlugMustChange": "Ідэнтыфікатар змяшчае недапушчальныя сімвалы", + "ToastSlugRequired": "Ідэнтыфікатар абавязковы", + "ToastSocketConnected": "Сокет падключаны", + "ToastSocketDisconnected": "Сокет адключаны", + "ToastSocketFailedToConnect": "Не ўдалося падключыць сокет", "ToastSortingPrefixesEmptyError": "Мусіць мець хаця б адзін прэфікс сартавання", "ToastSortingPrefixesUpdateSuccess": "Прэфіксы сартавання абноўлены ({0} элементаў)", "ToastTitleRequired": "Загаловак абавязковы", "ToastUnknownError": "Невядомая памылка", + "ToastUnlinkOpenIdFailed": "Не ўдалося адвязаць карыстальніка ад OpenID", + "ToastUnlinkOpenIdSuccess": "Карыстальнік адвязаны ад OpenID", "ToastUploaderFilepathExistsError": "Файл \"{0}\" ужо існуе на серверы", "ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.", "ToastUserDeleteFailed": "Не ўдалося выдаліць карыстальніка", @@ -1064,7 +1156,7 @@ "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара", "TooltipAddChapters": "Дадаць раздзел(ы)", "TooltipAddOneSecond": "Дадаць 1 секунду", - "TooltipAdjustChapterStart": "Націсніце, каб наладзіць час пачатку", + "TooltipAdjustChapterStart": "Націсніце, каб адкарэкціраваць час пачатку", "TooltipLockAllChapters": "Заблакіраваць усе раздзелы", "TooltipLockChapter": "Заблакіраваць раздзел (Shift+націсканне для дыяпазону)", "TooltipSubtractOneSecond": "Адняць 1 секунду", From 7c956b15820efaaf731a906075f21a74cbfcbfb5 Mon Sep 17 00:00:00 2001 From: Torstein Eide Date: Sat, 28 Feb 2026 20:31:23 +0100 Subject: [PATCH 186/285] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/nb_NO/ --- client/strings/no.json | 113 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/client/strings/no.json b/client/strings/no.json index 1a19cfa1e..87840c3c1 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -309,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "Slett fra filsystemet (fjern haken for kun å ta bort fra databasen)", "LabelDescription": "Beskrivelse", "LabelDeselectAll": "Fjern valg", + "LabelDetectedPattern": "Oppdaget mønster:", "LabelDevice": "Enhet", "LabelDeviceInfo": "Enhetsinformasjon", "LabelDeviceIsAvailableTo": "Enheten er tilgjengelig for...", @@ -377,11 +378,12 @@ "LabelFilterByUser": "Filtrer etter bruker", "LabelFindEpisodes": "Finn episoder", "LabelFinished": "Fullført", + "LabelFinishedDate": "Fullført {0}", "LabelFolder": "Mappe", "LabelFolders": "Mapper", "LabelFontBold": "Fet", "LabelFontBoldness": "Skrifttykkelse", - "LabelFontFamily": "Fontfamilie", + "LabelFontFamily": "Skriftfamilie", "LabelFontItalic": "Kursiv", "LabelFontScale": "Font størrelse", "LabelFontStrikethrough": "Gjennomstreking", @@ -434,7 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ingen {0}", "LabelLibraryItem": "Bibliotek enhet", "LabelLibraryName": "Bibliotek navn", - "LabelLibrarySortByProgress": "Fremgang: Sist oppdatert", + "LabelLibrarySortByProgress": "Fremdrift: Sist oppdatert", + "LabelLibrarySortByProgressFinished": "Fremdrift: Fullført", + "LabelLibrarySortByProgressStarted": "Fremdrift: Startet", "LabelLimit": "Begrensning", "LabelLineSpacing": "Linjemellomrom", "LabelListenAgain": "Lytt igjen", @@ -443,8 +447,9 @@ "LabelLogLevelWarn": "Varsel", "LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen", "LabelLowestPriority": "Laveste prioritet", + "LabelMatchConfidence": "Konfidens", "LabelMatchExistingUsersBy": "Knytt sammen eksisterende brukere basert på", - "LabelMatchExistingUsersByDescription": "Brukes for å koble til eksisterende brukere. Når koblingen er i orden vil brukerne bli identifisert med en unik id fra SSO-tilbyderen.", + "LabelMatchExistingUsersByDescription": "Brukes for å koble til eksisterende brukere. Når koblingen er i orden vil brukerne bli identifisert med en unik id fra SSO-tilbyderen", "LabelMaxEpisodesToDownload": "Maksimalt antall episoder som skal lastes ned. Bruk 0 for ubegrenset.", "LabelMaxEpisodesToDownloadPerCheck": "Maksimalt antall nye episoder som skal lastes ned per sjekk", "LabelMaxEpisodesToKeep": "Maksimalt antall episoder som skal beholdes", @@ -472,7 +477,9 @@ "LabelNewestAuthors": "Nyeste forfattere", "LabelNewestEpisodes": "Nyeste episoder", "LabelNextBackupDate": "Neste sikkerhetskopi dato", + "LabelNextChapters": "Neste kapitler blir:", "LabelNextScheduledRun": "Neste planlagte kjøring", + "LabelNoApiKeys": "Ingen API-nøkler", "LabelNoCustomMetadataProviders": "Ingen egendefinerte tilbydere for metadata", "LabelNoEpisodesSelected": "Ingen episoder valgt", "LabelNotFinished": "Ikke fullført", @@ -488,6 +495,7 @@ "LabelNotificationsMaxQueueSize": "Maksimalt antall varslinger i kø", "LabelNotificationsMaxQueueSizeHelp": "Hendelser er begrenset til avfyre én gang per sekund. Hendelser blir ignorert om køen er full. Dette forhindrer overflod av varslinger.", "LabelNumberOfBooks": "Antall bøker", + "LabelNumberOfChapters": "Antall kapitler:", "LabelNumberOfEpisodes": "# episoder", "LabelOpenIDAdvancedPermsClaimDescription": "Navnet på OpenID claim'et som inneholder avanserte tilganger for brukerhandlinger i applikasjonen som vil brukes for ikke-administratorroller (hvis konfigurert). Hvis claim'et mangler fra responsen, nektes tilgang til ABS. Hvis en enkelt opsjon mangler, blir behandlet som false. Påse at identitetstilbyderens claim stemmer overens med den forventede strukturen:", "LabelOpenIDClaims": "La følge valg være tomme for å slå av avanserte gruppe og tillatelser. Gruppen \"Bruker\" vil da også automatisk legges til.", @@ -509,6 +517,7 @@ "LabelPersonalYearReview": "Oppsummering av året ditt ({0})", "LabelPhotoPathURL": "Bilde sti/URL", "LabelPlayMethod": "Avspillingsmetode", + "LabelPlaybackRateIncrementDecrement": "Trinnstørrelse for økning/senking av avspillingshastighet", "LabelPlayerChapterNumberMarker": "{0} av {1}", "LabelPlaylists": "Spilleliste", "LabelPodcast": "Podcast", @@ -561,6 +570,7 @@ "LabelSelectAll": "Velg alt", "LabelSelectAllEpisodes": "Velg alle episoder", "LabelSelectEpisodesShowing": "Velg {0} episoder vist", + "LabelSelectUser": "Velg bruker", "LabelSelectUsers": "Velg brukere", "LabelSendEbookToDevice": "Send Ebok til...", "LabelSequence": "Sekvens", @@ -628,6 +638,7 @@ "LabelStartTime": "Start Tid", "LabelStarted": "Startet", "LabelStartedAt": "Startet", + "LabelStartedDate": "Startet {0}", "LabelStatsAudioTracks": "Lydspor", "LabelStatsAuthors": "Forfattere", "LabelStatsBestDay": "Beste dag", @@ -657,6 +668,7 @@ "LabelTheme": "Tema", "LabelThemeDark": "Mørk", "LabelThemeLight": "Lys", + "LabelThemeSepia": "Sepia", "LabelTimeBase": "Tidsbase", "LabelTimeDurationXHours": "{0} timer", "LabelTimeDurationXMinutes": "{0} minutter", @@ -725,24 +737,32 @@ "MessageAddToPlayerQueue": "Legg til i kø", "MessageAppriseDescription": "For å bruke denne funksjonen trenger du en instans av Apprise API kjørende eller et API som håndterer disse forespørslene.
Apprise API URL skal være hele URL-en til varslingen, f.eks., hvis din API-instans er på http://192.168.1.1:8337 så skal du bruke http://192.168.1.1:8337/notify.", "MessageAsinCheck": "Påse at du bruker ASIN fra den riktige Audible-regionen, ikke Amazon.", + "MessageAuthenticationLegacyTokenWarning": "Eldre API-tokener vil bli fjernet i fremtiden. Bruk API-nøkler i stedet.", "MessageAuthenticationOIDCChangesRestart": "Etter å ha lagret, start serveren din på nytt for at OIDC-endringene skal tre i kraft.", + "MessageAuthenticationSecurityMessage": "Autentisering er forbedret av sikkerhetshensyn. Alle brukere må logge inn på nytt.", "MessageBackupsDescription": "Sikkerhetskopier inkluderer, brukerfremgang, detaljer om bibliotekgjenstander, tjener instillinger og bilder lagret under /metadata/items og /metadata/authors. Sikkerhetskopier vil ikke inkludere filer som er lagret i bibliotek mappene.", "MessageBackupsLocationEditNote": "Viktig: Endring av mappen for sikkerhetskopi hverken endrer eller flytter eksisterende sikkerhetskopier!", "MessageBackupsLocationNoEditNote": "NB: Mappen for sikkerhetskopi settes i en miljøvariabel og kan ikke endres her.", "MessageBackupsLocationPathEmpty": "Mappen for sikkerhetskopiering må angis", + "MessageBatchEditPopulateMapDetailsAllHelp": "Fyll aktiverte felt med data fra alle elementer. Felt med flere verdier blir slått sammen", + "MessageBatchEditPopulateMapDetailsItemHelp": "Fyll aktiverte kartdetaljfelt med data fra dette elementet", "MessageBatchQuickMatchDescription": "Kjapt søk vil forsøke å legge til manglende omslag og metadata for de valgte gjenstandene. Aktiver dette valget for å tillate Kjapt søk til å overskrive eksisterende omslag og/eller metadata.", "MessageBookshelfNoCollections": "Du har ikke laget noen samlinger ennå", + "MessageBookshelfNoCollectionsHelp": "Samlinger er offentlige. Alle brukere med tilgang til biblioteket kan se dem.", "MessageBookshelfNoRSSFeeds": "Ingen RSS feed er åpen", "MessageBookshelfNoResultsForFilter": "Ingen resultat for filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Ingen resultater for søket", "MessageBookshelfNoSeries": "Du har ingen serier", + "MessageBulkChapterPattern": "Hvor mange kapitler vil du legge til med dette nummereringsmønsteret?", "MessageChapterEndIsAfter": "Kapittel slutt er etter slutt av lydboken", "MessageChapterErrorFirstNotZero": "Første kapittel starter på 0", "MessageChapterErrorStartGteDuration": "Feil start tid, må være mindre enn lengde på lydbok", "MessageChapterErrorStartLtPrev": "Feil start tid, må være større eller det samme som forrige kapittel start tid", "MessageChapterStartIsAfter": "Kapittel start er etter slutten av din lydbok", + "MessageChaptersNotFound": "Fant ikke kapitler", "MessageCheckingCron": "Sjekker cron...", "MessageConfirmCloseFeed": "Er du sikker på at du vil lukke denne feeden?", + "MessageConfirmDeleteApiKey": "Er du sikker på at du vil slette API-nøkkelen \"{0}\"?", "MessageConfirmDeleteBackup": "Er du sikker på at du vil slette sikkerhetskopi for {0}?", "MessageConfirmDeleteDevice": "Er du sikker på at du vil slette e-leser enheten \"{0}\"?", "MessageConfirmDeleteFile": "Dette vil slette filen fra filsystemet. Er du sikker?", @@ -770,6 +790,7 @@ "MessageConfirmRemoveAuthor": "Er du sikker på at du vil fjerne forfatteren \"{0}\"?", "MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?", "MessageConfirmRemoveEpisode": "Er du sikker på at du vil fjerne episode \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Merk: Dette sletter ikke lydfilen med mindre du slår på \"Hard delete file\"", "MessageConfirmRemoveEpisodes": "Er du sikker på at du vil fjerne {0} episoder?", "MessageConfirmRemoveListeningSessions": "Er du sikker på at du vil fjerne {0} lytte-sesjoner?", "MessageConfirmRemoveMetadataFiles": "Er du sikker på at du vil fjerne alle metadata.{0}-filer i mappene for biblioteks-elementer?", @@ -795,8 +816,11 @@ "MessageFeedURLWillBe": "Feed URL vil bli {0}", "MessageFetching": "Henter...", "MessageForceReScanDescription": "vil skanne alle filene igjen som en ny skann. Lyd fil ID3 tagger, OPF filer og tekstfiler vil bli skannet som nye.", + "MessageHeatmapListeningTimeTooltip": "{0} lytter på {1}", + "MessageHeatmapNoListeningSessions": "Ingen lytteøkter på {0}", "MessageImportantNotice": "Viktig varsel!", "MessageInsertChapterBelow": "Sett inn kapittel under", + "MessageInvalidAsin": "Ugyldig ASIN", "MessageItemsSelected": "{0} Gjenstander valgt", "MessageItemsUpdated": "{0} Gjenstander oppdatert", "MessageJoinUsOn": "Følg oss nå", @@ -842,6 +866,7 @@ "MessageNoTasksRunning": "Ingen oppgaver kjører", "MessageNoUpdatesWereNecessary": "Ingen oppdatering var nødvendig", "MessageNoUserPlaylists": "Du har ingen spillelister", + "MessageNoUserPlaylistsHelp": "Spillelister er private. Bare brukeren som oppretter dem kan se dem.", "MessageNotYetImplemented": "Ikke implementert ennå", "MessageOpmlPreviewNote": "PS: Dette er en forhåndvisning av en OPML-fil. Den faktiske podcast-tittelen hentes direkte fra RSS-feeden.", "MessageOr": "eller", @@ -864,8 +889,10 @@ "MessageRestoreBackupConfirm": "Er du sikker på at du vil gjenopprette sikkerhetskopien som var laget", "MessageRestoreBackupWarning": "gjenoppretting av sikkerhetskopi vil overskrive hele databasen under /config og omslagsbilde under /metadata/items og /metadata/authors.

Sikkerhetskopier endrer ikke noen filer under dine bibliotekmapper. Hvis du har aktivert tjenerinstillingen for å lagre omslagsbilder og metadata i bibliotekmapper så vil ikke de filene bli tatt sikkerhetskopi eller overskrevet.

Alle klientene som bruker din tjener vil bli fornyet automatisk.", "MessageScheduleLibraryScanNote": "For de fleste brukere er det anbefalt å la denne funksjonen være slått av, og la mappeovervåkeren stå på. Mappeovervåkeren oppdager automatisk endringer i biblioteksmappene. Mappeovervåkeren fungerer ikke med alle filsystemer (f.eks. NFS) og da kan planlagt skanning av bibliotekene brukes i steden for.", + "MessageScheduleRunEveryWeekdayAtTime": "Kjør hver {0} kl. {1}", "MessageSearchResultsFor": "Søk resultat for", "MessageSelected": "{0} valgt", + "MessageSeriesSequenceCannotContainSpaces": "Serienummer kan ikke inneholde mellomrom", "MessageServerCouldNotBeReached": "Tjener kunne ikke bli nådd", "MessageSetChaptersFromTracksDescription": "Sett kapitler ved å bruke hver lydfil som kapittel og kapitteltittel som lydfilnavnet", "MessageShareExpirationWillBe": "Utløp vil være {0}", @@ -886,6 +913,27 @@ "MessageTaskFailedToMergeAudioFiles": "Kunne ikke slå sammen lydfiler", "MessageTaskFailedToMoveM4bFile": "Kunne ikke flytte M4B-fil", "MessageTaskFailedToWriteMetadataFile": "Kunne ikke lagre metadata-fil", + "MessageTaskMatchingBooksInLibrary": "Samsvarende bøker i biblioteket \"{0}\"", + "MessageTaskNoFilesToScan": "Ingen filer å skanne", + "MessageTaskOpmlImport": "OPML-import", + "MessageTaskOpmlImportDescription": "Oppretter podkaster fra {0} RSS-feeder", + "MessageTaskOpmlImportFeed": "OPML-importfeed", + "MessageTaskOpmlImportFeedDescription": "Importerer RSS-feed \"{0}\"", + "MessageTaskOpmlImportFeedFailed": "Kunne ikke hente podcast-feed", + "MessageTaskOpmlImportFeedPodcastDescription": "Oppretter podkast \"{0}\"", + "MessageTaskOpmlImportFeedPodcastExists": "Podkast finnes allerede på stien", + "MessageTaskOpmlImportFeedPodcastFailed": "Misslykkes å opprette podcast", + "MessageTaskOpmlImportFinished": "La til {0} podkaster", + "MessageTaskOpmlParseFailed": "Klarte ikke å tolke OPML-fil", + "MessageTaskOpmlParseFastFail": "Ugyldig OPML-fil: -tagg ble ikke funnet ELLER en -tagg ble ikke funnet", + "MessageTaskOpmlParseNoneFound": "Fant ingen feeder i OPML-filen", + "MessageTaskScanItemsAdded": "{0} lagt til", + "MessageTaskScanItemsMissing": "{0} mangler", + "MessageTaskScanItemsUpdated": "{0} oppdatert", + "MessageTaskScanNoChangesNeeded": "Ingen endringer nødvendig", + "MessageTaskScanningFileChanges": "Skanner filendringer i \"{0}\"", + "MessageTaskScanningLibrary": "Skanner biblioteket \"{0}\"", + "MessageTaskTargetDirectoryNotWritable": "Målkatalogen er ikke skrivbar", "MessageThinking": "Tenker...", "MessageUploaderItemFailed": "Opplastning mislykkes", "MessageUploaderItemSuccess": "Opplastning fullført!", @@ -903,13 +951,43 @@ "NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler vil bli behandlet som separate bibliotekgjenstander.", "NoteUploaderOnlyAudioFiles": "Om man laster opp kun lydfiler så vil hver lydfil bli behandlet som en separat lydbok.", "NoteUploaderUnsupportedFiles": "Filer som ikke er støttet vil bli ignorert. Når man velger eller slipper en mappe, filer som ikke er en mappe vil bli ignorert.", + "NotificationOnBackupCompletedDescription": "Utløses når en sikkerhetskopi er fullført", + "NotificationOnBackupFailedDescription": "Utløses når en sikkerhetskopi mislykkes", + "NotificationOnEpisodeDownloadedDescription": "Utløses når en podkastepisode lastes ned automatisk", + "NotificationOnRSSFeedDisabledDescription": "Utløses når automatiske episodenedlastinger deaktiveres på grunn av for mange mislykkede forsøk", + "NotificationOnRSSFeedFailedDescription": "Utløses når RSS-feedforespørselen mislykkes for en automatisk episodenedlasting", + "NotificationOnTestDescription": "Hendelse for testing av varslingssystemet", + "PlaceholderBulkChapterInput": "Skriv inn kapitteltittel eller bruk nummerering (f.eks. 'Episode 1', 'Kapittel 10', '1.')", "PlaceholderNewCollection": "Ny samlingsnavn", "PlaceholderNewFolderPath": "Ny mappesti", "PlaceholderNewPlaylist": "Ny spillelistenavn", "PlaceholderSearch": "Søk..", "PlaceholderSearchEpisode": "Søk episode..", + "StatsAuthorsAdded": "forfattere lagt til", + "StatsBooksAdded": "bøker lagt til", + "StatsBooksAdditional": "Noen av tilleggene inkluderer…", + "StatsBooksFinished": "bøker fullført", + "StatsBooksFinishedThisYear": "Noen bøker fullført i år…", + "StatsBooksListenedTo": "bøker lyttet til", + "StatsCollectionGrewTo": "Boksamlingen din vokste til…", + "StatsSessions": "økter", + "StatsSpentListening": "brukt på lytting", + "StatsTopAuthor": "BESTE FORFATTER", + "StatsTopAuthors": "BESTE FORFATTERE", + "StatsTopGenre": "BESTE SJANGER", + "StatsTopGenres": "BESTE SJANGRE", + "StatsTopMonth": "BESTE MÅNED", + "StatsTopNarrator": "BESTE FORTELLER", + "StatsTopNarrators": "BESTE FORTELLERE", + "StatsTotalDuration": "Med en total varighet på…", + "StatsYearInReview": "ÅRET SOM GIKK", "ToastAccountUpdateSuccess": "Konto oppdatert", + "ToastAppriseUrlRequired": "Du må angi en Apprise-URL", + "ToastAsinRequired": "ASIN er påkrevd", "ToastAuthorImageRemoveSuccess": "Forfatter bilde fjernet", + "ToastAuthorNotFound": "Fant ikke forfatter \"{0}\"", + "ToastAuthorRemoveSuccess": "Forfatter fjernet", + "ToastAuthorSearchNotFound": "Fant ikke forfatter", "ToastAuthorUpdateMerged": "Forfatter slått sammen", "ToastAuthorUpdateSuccess": "Forfatter oppdatert", "ToastAuthorUpdateSuccessNoImageFound": "Forfatter oppdater (ingen bilde funnet)", @@ -923,6 +1001,7 @@ "ToastBackupRestoreFailed": "Misslykkes å gjenopprette sikkerhetskopi", "ToastBackupUploadFailed": "Misslykkes å laste opp sikkerhetskopi", "ToastBackupUploadSuccess": "Sikkerhetskopi lastet opp", + "ToastBatchApplyDetailsToItemsSuccess": "Detaljer brukt på elementene", "ToastBatchDeleteFailed": "Sletting feilet på utvalget", "ToastBatchDeleteSuccess": "Sletting av samling utført", "ToastBatchQuickMatchFailed": "Feil ved rask integrering av metadata!", @@ -932,17 +1011,25 @@ "ToastBookmarkCreateFailed": "Misslykkes å opprette bokmerke", "ToastBookmarkCreateSuccess": "Bokmerke lagt til", "ToastBookmarkRemoveSuccess": "Bokmerke fjernet", + "ToastBulkChapterInvalidCount": "Skriv inn et tall mellom 1 og 150", "ToastCachePurgeFailed": "Kunne ikke å slette mellomlager", "ToastCachePurgeSuccess": "Mellomlager slettet", + "ToastChapterLocked": "Kapittelet er låst.", + "ToastChapterStartTimeAdjusted": "Kapittelstart ble justert med {0} sekunder", + "ToastChaptersAllLocked": "Alle kapitler er låst. Lås opp noen kapitler for å flytte tidene.", "ToastChaptersHaveErrors": "Kapittel har feil", + "ToastChaptersInvalidShiftAmountLast": "Ugyldig forskyvningsverdi. Starttid for siste kapittel vil gå utover varigheten til denne lydboken.", + "ToastChaptersInvalidShiftAmountStart": "Ugyldig forskyvningsverdi. Det første kapitlet ville fått null eller negativ lengde og blitt overskrevet av det andre kapitlet. Øk starttiden til det andre kapitlet.", "ToastChaptersMustHaveTitles": "Kapittel må ha titler", "ToastChaptersRemoved": "Kapitler fjernet", "ToastChaptersUpdated": "Kapitler oppdatert", "ToastCollectionItemsAddFailed": "Feil med å legge til element(er)", "ToastCollectionRemoveSuccess": "Samling fjernet", "ToastCollectionUpdateSuccess": "samlingupdated", + "ToastConnectionNotAvailable": "Tilkobling er ikke tilgjengelig. Prøv igjen senere", "ToastCoverSearchFailed": "Finner ikke bokomslag", "ToastCoverUpdateFailed": "Oppdatering av bilde feilet", + "ToastDateTimeInvalidOrIncomplete": "Dato og klokkeslett er ugyldig eller ufullstendig", "ToastDeleteFileFailed": "Kunne ikke slette fil", "ToastDeleteFileSuccess": "Fil slettet", "ToastDeviceAddFailed": "Kunne ikke legge til enhet", @@ -955,6 +1042,9 @@ "ToastEpisodeDownloadQueueClearFailed": "Kunne ikke tømme køen", "ToastEpisodeDownloadQueueClearSuccess": "Nedlastingskø for eposider tømt", "ToastEpisodeUpdateSuccess": "{0} episoder oppdatert", + "ToastErrorCannotShare": "Kan ikke dele direkte på denne enheten", + "ToastFailedToCreate": "Kunne ikke opprette", + "ToastFailedToDelete": "Kunne ikke slette", "ToastFailedToLoadData": "Kunne ikke laste inn data", "ToastFailedToMatch": "Kunne ikke matche", "ToastFailedToShare": "Deling feilet", @@ -962,6 +1052,7 @@ "ToastInvalidImageUrl": "Ugyldig URL for bilde", "ToastInvalidMaxEpisodesToDownload": "Ugyldig maksimalt antall for nedlasting av episoder", "ToastInvalidUrl": "Ugyldig URL", + "ToastInvalidUrls": "Én eller flere URL-er er ugyldige", "ToastItemCoverUpdateSuccess": "Omslag oppdatert", "ToastItemDeletedFailed": "Kunne ikke slette element", "ToastItemDeletedSuccess": "Element slettet", @@ -986,6 +1077,7 @@ "ToastMustHaveAtLeastOnePath": "Påkrevd med minst én mappe", "ToastNameEmailRequired": "Navn og e-post påkrevd", "ToastNameRequired": "Navn er påkrevd", + "ToastNewApiKeyUserError": "Du må velge en bruker", "ToastNewEpisodesFound": "{0} nye episoder funnet", "ToastNewUserCreatedFailed": "Kunne ikke opprette konto: \"{0}\"", "ToastNewUserCreatedSuccess": "Ny konto opprettet", @@ -994,6 +1086,7 @@ "ToastNewUserTagError": "Velg minst en tag", "ToastNewUserUsernameError": "Skriv inn brukernavn", "ToastNoNewEpisodesFound": "Ingen nye episoder funnet", + "ToastNoRSSFeed": "Podkasten har ikke en RSS-feed", "ToastNoUpdatesNecessary": "Ingen oppdateringer nødvendig", "ToastNotificationCreateFailed": "Kunne ikke opprette varsling", "ToastNotificationDeleteFailed": "Kunne ikke slette varsling", @@ -1009,6 +1102,7 @@ "ToastPlaylistUpdateSuccess": "Spilleliste oppdatert", "ToastPodcastCreateFailed": "Misslykkes å opprette podcast", "ToastPodcastCreateSuccess": "Podcast opprettet", + "ToastPodcastEpisodeUpdated": "Episode oppdatert", "ToastPodcastGetFeedFailed": "Kunne ikke hente podcast-feed", "ToastPodcastNoEpisodesInFeed": "Ingen episoder funnet i RSS-feed", "ToastPodcastNoRssFeed": "Podcast har ingen RSS-feed", @@ -1033,6 +1127,7 @@ "ToastSelectAtLeastOneUser": "Velg minst én bruker", "ToastSendEbookToDeviceFailed": "Misslykkes å sende ebok", "ToastSendEbookToDeviceSuccess": "Ebok sendt til \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Kan ikke legge til to serier med samme navn", "ToastSeriesUpdateFailed": "Misslykkes å oppdatere serie", "ToastSeriesUpdateSuccess": "Serie oppdatert", "ToastServerSettingsUpdateSuccess": "Server-innstillinger oppdatert", @@ -1051,10 +1146,20 @@ "ToastUnknownError": "Ukjent feil", "ToastUnlinkOpenIdFailed": "Kunne ikke koble bruker fra OpenID", "ToastUnlinkOpenIdSuccess": "Bruker koblet fra OpenID", + "ToastUploaderFilepathExistsError": "Filstien \"{0}\" finnes allerede på serveren", + "ToastUploaderItemExistsInSubdirectoryError": "Elementet \"{0}\" bruker en underkatalog av opplastingsstien.", "ToastUserDeleteFailed": "Misslykkes å slette bruker", "ToastUserDeleteSuccess": "Bruker slettet", "ToastUserPasswordChangeSuccess": "Passord ble endret", "ToastUserPasswordMismatch": "Passord må stemme overens", "ToastUserPasswordMustChange": "Nytt passord kan ikke være identisk med gammelt passord", - "ToastUserRootRequireName": "Root-brukernavn er påkrevd" + "ToastUserRootRequireName": "Root-brukernavn er påkrevd", + "TooltipAddChapters": "Legg til kapittel(er)", + "TooltipAddOneSecond": "Legg til 1 sekund", + "TooltipAdjustChapterStart": "Klikk for å justere starttid", + "TooltipLockAllChapters": "Lås alle kapitler", + "TooltipLockChapter": "Lås kapittel (Shift+klikk for område)", + "TooltipSubtractOneSecond": "Trekk fra 1 sekund", + "TooltipUnlockAllChapters": "Lås opp alle kapitler", + "TooltipUnlockChapter": "Lås opp kapittel (Shift+klikk for område)" } From 45dd843ce106f164771294e2d58b8f588cd5ab35 Mon Sep 17 00:00:00 2001 From: thehijacker Date: Sun, 1 Mar 2026 10:44:10 +0100 Subject: [PATCH 187/285] Translated using Weblate (Slovenian) 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/sl/ --- client/strings/sl.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/sl.json b/client/strings/sl.json index 3b7ce53a2..c88be4ba1 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -104,7 +104,7 @@ "ButtonStartM4BEncode": "Zaženi M4B prekodiranje", "ButtonStartMetadataEmbed": "Začni vdelavo metapodatkov", "ButtonStats": "Statistika", - "ButtonSubmit": "Potrdi", + "ButtonSubmit": "Pošlji", "ButtonTest": "Test", "ButtonUnlinkOpenId": "Prekini povezavo OpenID", "ButtonUpload": "Naloži", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ne {0}", "LabelLibraryItem": "Element knjižnice", "LabelLibraryName": "Ime knjižnice", - "LabelLibrarySortByProgress": "Napredek: Nazadnje posodobljen", - "LabelLibrarySortByProgressFinished": "Napredej: Končano", - "LabelLibrarySortByProgressStarted": "Napredek: Začeto", + "LabelLibrarySortByProgress": "Napredek: Zadnja posodobitev", + "LabelLibrarySortByProgressFinished": "Napredek: Končano", + "LabelLibrarySortByProgressStarted": "Napredek: Začelo se je", "LabelLimit": "Omejitev", "LabelLineSpacing": "Vrstični razmak", "LabelListenAgain": "Poslušaj znova", From 635e132325e086b34964ccc401e6726922ebc5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20S=2E=20Hegnander?= Date: Sat, 28 Feb 2026 20:38:16 +0100 Subject: [PATCH 188/285] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/nb_NO/ --- client/strings/no.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/no.json b/client/strings/no.json index 87840c3c1..bc39982ea 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -34,7 +34,7 @@ "ButtonEditChapters": "Rediger kapittel", "ButtonEditPodcast": "Rediger podcast", "ButtonEnable": "Aktiver", - "ButtonFireAndFail": "Kjør ved feil", + "ButtonFireAndFail": "Utfør og feil", "ButtonFireOnTest": "Kjør onTest-kommando", "ButtonForceReScan": "Tving skann", "ButtonFullPath": "Full sti", From 6479cdb66deeec828137e808ff1e356fd0b78cef Mon Sep 17 00:00:00 2001 From: "weblate.user.1274" Date: Sat, 28 Feb 2026 20:38:28 +0100 Subject: [PATCH 189/285] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/nb_NO/ --- client/strings/no.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/no.json b/client/strings/no.json index bc39982ea..9f79312d7 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -113,7 +113,7 @@ "ButtonUploadOPMLFile": "Last opp OPML fil", "ButtonUserDelete": "Slett bruker {0}", "ButtonUserEdit": "Rediger bruker {0}", - "ButtonViewAll": "Vis alle", + "ButtonViewAll": "Vis alt", "ButtonYes": "Ja", "ErrorUploadFetchMetadataAPI": "Feil ved innhenting av metadata", "ErrorUploadFetchMetadataNoResults": "Kunne ikke hente metadata - forsøk å oppdatere tittel og/eller forfatter", From 6e6c43c53ceb0985e85d4e40372f39fb5cea67a9 Mon Sep 17 00:00:00 2001 From: Torstein Eide Date: Sat, 28 Feb 2026 20:44:56 +0100 Subject: [PATCH 190/285] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/nb_NO/ --- client/strings/no.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/no.json b/client/strings/no.json index 9f79312d7..8e1c9cfbb 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -458,7 +458,7 @@ "LabelMediaType": "Medie type", "LabelMetaTag": "Meta tag", "LabelMetaTags": "Meta tags", - "LabelMetadataOrderOfPrecedenceDescription": "Høyere prioritert kilder for metadata overstyrer laverer prioriterte kilder for metadata.", + "LabelMetadataOrderOfPrecedenceDescription": "Høyere prioritert kilder for metadata overstyrer laverer prioriterte kilder for metadata", "LabelMetadataProvider": "Metadata Leverandør", "LabelMinute": "Minutt", "LabelMinutes": "Minutter", @@ -499,7 +499,7 @@ "LabelNumberOfEpisodes": "# episoder", "LabelOpenIDAdvancedPermsClaimDescription": "Navnet på OpenID claim'et som inneholder avanserte tilganger for brukerhandlinger i applikasjonen som vil brukes for ikke-administratorroller (hvis konfigurert). Hvis claim'et mangler fra responsen, nektes tilgang til ABS. Hvis en enkelt opsjon mangler, blir behandlet som false. Påse at identitetstilbyderens claim stemmer overens med den forventede strukturen:", "LabelOpenIDClaims": "La følge valg være tomme for å slå av avanserte gruppe og tillatelser. Gruppen \"Bruker\" vil da også automatisk legges til.", - "LabelOpenIDGroupClaimDescription": "Navn på OpenID-forespørsel som inneholder en lite over brukerens grupper. Vanligvis kalt grupper. Om konfigurert, vil applikasjonen tildele roller baseret på brukerens gruppemedlemsskaper, gitt disse grupper er navngitt (uten forbehold for store og små bokstaver) 'admin', 'user' eller 'guest' i forespørsel. Forespørselen burde inneholde en liste (og hvis brukeren tilhører flere grupper), applikasjonen vil tildele rolle med høyeste adgangsnivå. Hvis ingen grupper matcher vil adgang bli nektet.", + "LabelOpenIDGroupClaimDescription": "Navn på OpenID-forespørsel som inneholder en lite over brukerens grupper. Vanligvis kalt grupper. Om konfigurert, vil applikasjonen tildele roller baseret på brukerens gruppemedlemsskaper, gitt disse grupper er navngitt (uten forbehold for store og små bokstaver) 'admin', 'user' eller 'guest' i forespørsel. Forespørselen burde inneholde en liste (og hvis brukeren tilhører flere grupper), applikasjonen vil tildele rolle med høyeste adgangsnivå. Hvis ingen grupper matcher vil adgang bli nektet.", "LabelOpenRSSFeed": "Åpne RSS Feed", "LabelOverwrite": "Overskriv", "LabelPaginationPageXOfY": "Side {0} av {1}", @@ -742,7 +742,7 @@ "MessageAuthenticationSecurityMessage": "Autentisering er forbedret av sikkerhetshensyn. Alle brukere må logge inn på nytt.", "MessageBackupsDescription": "Sikkerhetskopier inkluderer, brukerfremgang, detaljer om bibliotekgjenstander, tjener instillinger og bilder lagret under /metadata/items og /metadata/authors. Sikkerhetskopier vil ikke inkludere filer som er lagret i bibliotek mappene.", "MessageBackupsLocationEditNote": "Viktig: Endring av mappen for sikkerhetskopi hverken endrer eller flytter eksisterende sikkerhetskopier!", - "MessageBackupsLocationNoEditNote": "NB: Mappen for sikkerhetskopi settes i en miljøvariabel og kan ikke endres her.", + "MessageBackupsLocationNoEditNote": "Viktig: Mappen for sikkerhetskopi satt i en miljøvariabel og kan ikke endres her.", "MessageBackupsLocationPathEmpty": "Mappen for sikkerhetskopiering må angis", "MessageBatchEditPopulateMapDetailsAllHelp": "Fyll aktiverte felt med data fra alle elementer. Felt med flere verdier blir slått sammen", "MessageBatchEditPopulateMapDetailsItemHelp": "Fyll aktiverte kartdetaljfelt med data fra dette elementet", @@ -781,7 +781,7 @@ "MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?", "MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?", "MessageConfirmNotificationTestTrigger": "Utløs dette varselet med test-data?", - "MessageConfirmPurgeCache": "Tømming av mellomlagring vil slette hele mappen /metadata/cache.

Er du sikker på at du du vil slette mappen?", + "MessageConfirmPurgeCache": "Tømming av mellomlagring vil slette hele mappen /metadata/cache.

Er du sikker på at du vil slette mappen?", "MessageConfirmPurgeItemsCache": "(Purge items cache) Dette vil sletter hele mappen /metadata/cache/items.
Er du sikker?", "MessageConfirmQuickEmbed": "Advarsel! Rask innbygging av metadata tar ikke backup av lyd-filene først. Forsikre deg om at du har sikkerhetskopi av filene.

Fortsett?", "MessageConfirmQuickMatchEpisodes": "Hurtig gjenkjenning av episoder overskriver detaljene hvis en match blir funnet. Kun episoder som ikke allerede er matchet blir oppdatert. Er du sikker?", From 367826ce64c7b042d6750cced698ac6660c76c6e Mon Sep 17 00:00:00 2001 From: Artur Date: Mon, 2 Mar 2026 18:25:52 +0100 Subject: [PATCH 191/285] Translated using Weblate (Polish) Currently translated at 95.7% (1113 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 3a9593db0..3be1c2dfe 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -11,7 +11,7 @@ "ButtonApplyChapters": "Zatwierdź rozdziały", "ButtonAuthors": "Autorzy", "ButtonBack": "Wstecz", - "ButtonBatchEditPopulateFromExisting": "Powiel z poprzednich", + "ButtonBatchEditPopulateFromExisting": "Uzupełnij na podstawie istniejących", "ButtonBatchEditPopulateMapDetails": "Powiel szczegóły mapy", "ButtonBrowseForFolder": "Wyszukaj folder", "ButtonCancel": "Anuluj", @@ -55,7 +55,7 @@ "ButtonNext": "Następny", "ButtonNextChapter": "Następny rozdział", "ButtonNextItemInQueue": "Następny element w kolejce", - "ButtonOk": "Ok", + "ButtonOk": "OK", "ButtonOpenFeed": "Otwórz feed", "ButtonOpenManager": "Otwórz menadżera", "ButtonPause": "Wstrzymaj", @@ -127,7 +127,7 @@ "HeaderAudiobookTools": "Narzędzia do zarządzania audiobookami", "HeaderAuthentication": "Uwierzytelnianie", "HeaderBackups": "Kopie zapasowe", - "HeaderBulkChapterModal": "Dodaj wiele rozdziałów", + "HeaderBulkChapterModal": "Dodaj kilka rozdziałów", "HeaderChangePassword": "Zmień hasło", "HeaderChapters": "Rozdziały", "HeaderChooseAFolder": "Wybierz folder", @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Książki", "LabelButtonText": "Tekst przycisku", - "LabelByAuthor": "autorstwa {0}", + "LabelByAuthor": "Autor {0}", "LabelChangePassword": "Zmień hasło", "LabelChannels": "Kanały", "LabelChapterCount": "{0} rozdziałów", @@ -286,7 +286,7 @@ "LabelClickToUseCurrentValue": "Kliknij by zastosować aktualną wartość", "LabelClosePlayer": "Zamknij odtwarzacz", "LabelCodec": "Kodek", - "LabelCollapseSeries": "Podsumuj serię", + "LabelCollapseSeries": "Zwiń serię", "LabelCollapseSubSeries": "Zwiń podserie", "LabelCollection": "Kolekcja", "LabelCollections": "Kolekcje", @@ -300,7 +300,7 @@ "LabelCoverImageURL": "URL okładki", "LabelCoverProvider": "Dostawca okładki", "LabelCreatedAt": "Utworzone", - "LabelCronExpression": "Wyrażenie CRON", + "LabelCronExpression": "Wyrażenie harmonogramowania zadań cron", "LabelCurrent": "Aktualny", "LabelCurrently": "Obecnie:", "LabelCustomCronExpression": "Niestandardowe wyrażenie Cron:", @@ -339,11 +339,11 @@ "LabelEnable": "Włącz", "LabelEncodingBackupLocation": "Kopia zapasowa twoich oryginalnych plików audio będzie się znajdować w:", "LabelEncodingChaptersNotEmbedded": "W audiobookach wielościeżkowych rozdziały nie są osadzone.", - "LabelEncodingClearItemCache": "Pamiętaj o okresowym czyszczeniu pamięci podręcznej elementów.", + "LabelEncodingClearItemCache": "Pamiętaj, aby okresowo czyścić pamięć podręczną elementów.", "LabelEncodingFinishedM4B": "Ukończony plik M4B zostanie umieszczony w folderze audiobooka pod adresem:", "LabelEncodingInfoEmbedded": "Metadane zostaną osadzone w ścieżkach audio w folderze z audiobookiem.", "LabelEncodingStartedNavigation": "Po uruchomieniu zadania możesz opuścić tę stronę.", - "LabelEncodingTimeWarning": "Konwersja może potrwać do 30 minut.", + "LabelEncodingTimeWarning": "Kodowanie może potrwać do 30 minut.", "LabelEncodingWarningAdvancedSettings": "Ostrzeżenie: Nie aktualizuj tych ustawień, jeśli nie jesteś zaznajomiony ze sposobem działania ffmpeg i opcji konwersji.", "LabelEncodingWatcherDisabled": "Jeśli monitorowanie folderów jest wyłączone, należy ponownie przeskanować audiobooka.", "LabelEnd": "Zakończ", @@ -436,7 +436,7 @@ "LabelLibraryFilterSublistEmpty": "Brak {0}", "LabelLibraryItem": "Element biblioteki", "LabelLibraryName": "Nazwa biblioteki", - "LabelLibrarySortByProgress": "Postęp: Ostatnio zaktualizowane", + "LabelLibrarySortByProgress": "Postęp: Ostatnia aktualizacja", "LabelLibrarySortByProgressFinished": "Postęp: Ukończony", "LabelLibrarySortByProgressStarted": "Postęp: Rozpoczęty", "LabelLimit": "Limit", @@ -450,7 +450,7 @@ "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.", + "LabelMaxEpisodesToDownload": "Maksymalna liczba odcinków do pobrania. Użyj 0, aby uzyskać nieograniczoną liczbę.", "LabelMaxEpisodesToDownloadPerCheck": "Maksymalna liczba nowych odcinków do pobrania na jedno sprawdzenie", "LabelMaxEpisodesToKeep": "Maksymalna liczba odcinków do zachowania", "LabelMaxEpisodesToKeepHelp": "Wartość 0 wyłącza maksymalny limit. Po automatycznym pobraniu nowego odcinka, najstarszy odcinek zostanie usunięty, jeśli masz ich więcej niż X. Spowoduje to usunięcie tylko 1 odcinka na nowe pobieranie.", @@ -498,7 +498,8 @@ "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 (jeśli skonfigurowano). Jeśli deklaracja nie zostanie uwzględniona w odpowiedzi, dostęp do ABS zostanie zablokowany. Brak jednej opcji zostanie uznany za fałsz. 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”.", + "LabelOpenIDClaims": "Pozostaw poniższe opcje puste, aby wyłączyć zaawansowane przypisywanie grup i uprawnień. Wówczas automatycznie zostanie przypisana grupa „Użytkownik”.", + "LabelOpenIDGroupClaimDescription": "Nazwa roszczenia OpenID zawierającego listę grup użytkownika. Powszechnie nazywane grupami. Jeśli skonfigurowano, aplikacja automatycznie przypisze role na podstawie przynależności użytkownika do grup, pod warunkiem, że te grupy są nazwane bez uwzględniania wielkości liter „admin”, „user” lub „guest” w roszczeniu. Roszczenie powinno zawierać listę, a jeśli użytkownik należy do wielu grup, aplikacja przypisze rolę odpowiadającą najwyższemu poziomowi dostępu. Jeśli żadna grupa nie będzie pasować, dostęp zostanie odrzucony.", "LabelOpenRSSFeed": "Otwórz kanał RSS", "LabelOverwrite": "Nadpisz", "LabelPaginationPageXOfY": "Strona {0} z {1}", @@ -577,7 +578,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Nazwy serii", "LabelSeriesProgress": "Postęp w serii", - "LabelServerLogLevel": "Poziom logowania servera", + "LabelServerLogLevel": "Poziom logów servera", "LabelServerYearReview": "Podsumowanie serwera w roku ({0})", "LabelSetEbookAsPrimary": "Ustaw jako pierwszy", "LabelSetEbookAsSupplementary": "Ustaw jako dodatkowy", @@ -686,7 +687,7 @@ "LabelToolsMakeM4bDescription": "Tworzy plik w formacie .M4B, który zawiera metadane, okładkę oraz rozdziały.", "LabelToolsSplitM4b": "Podziel plik .M4B na pliki .MP3", "LabelToolsSplitM4bDescription": "Podziel plik .M4B na pliki .MP3 na rozdziały z załączonymi metadanymi oraz okładką.", - "LabelTotalDuration": "TCałkowita długość", + "LabelTotalDuration": "Całkowita długość", "LabelTotalTimeListened": "Całkowity czas odtwarzania", "LabelTrackFromFilename": "Ścieżka z nazwy pliku", "LabelTrackFromMetadata": "Ścieżka z metadanych", @@ -723,6 +724,7 @@ "LabelViewQueue": "Wyświetlaj kolejkę odtwarzania", "LabelVolume": "Głośność", "LabelWebRedirectURLsDescription": "Zezwól na te adresy URL w swoim dostawcy OAuth, aby umożliwić przekierowanie z powrotem do aplikacji internetowej po zalogowaniu:", + "LabelWebRedirectURLsSubfolder": "Podfolder dla adresów URL przekierowań", "LabelWeekdaysToRun": "Dni tygodnia", "LabelXBooks": "{0} książek", "LabelXItems": "{0} elementów", @@ -915,15 +917,21 @@ "MessageTaskNoFilesToScan": "Brak plików do skanowania", "MessageTaskOpmlImport": "Importuj OPML", "MessageTaskOpmlImportDescription": "Tworzenie {0} podcastów z kanałów RSS", + "MessageTaskOpmlImportFeed": "Importuje plik OPML", + "MessageTaskOpmlImportFeedDescription": "Importowanie kanału RSS „{0}”", + "MessageTaskOpmlImportFeedFailed": "Nie udało się pobrać kanału podcastowego", "MessageTaskOpmlImportFeedPodcastDescription": "Tworzenie podcastu \"{0}\"", "MessageTaskOpmlImportFeedPodcastExists": "Podcast już istnieje pod podaną ścieżką", "MessageTaskOpmlImportFeedPodcastFailed": "Nie udało się utworzyć podcastu", "MessageTaskOpmlImportFinished": "Dodano {0} podcastów", "MessageTaskOpmlParseFailed": "Błąd parsowania pliku OPML", + "MessageTaskOpmlParseFastFail": "Nieprawidłowy plik OPML. Nie znaleziono tagu LUB nie znaleziono tagu .", + "MessageTaskOpmlParseNoneFound": "Nie znaleziono kanałów w pliku OPML", "MessageTaskScanItemsAdded": "Dodano {0}", "MessageTaskScanItemsMissing": "Brakuje {0}", "MessageTaskScanItemsUpdated": "Zaktualizowano {0}", "MessageTaskScanNoChangesNeeded": "Brak zmian", + "MessageTaskScanningFileChanges": "Skanowanie zmian w plikach w „{0}”", "MessageTaskScanningLibrary": "Skanowanie biblioteki \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "Brak prawa zapisu do folderu docelowego", "MessageThinking": "Myślę...", @@ -944,6 +952,7 @@ "NoteUploaderOnlyAudioFiles": "Jeśli przesyłasz tylko pliki audio, każdy plik audio będzie traktowany jako osobny audiobook.", "NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.", "NotificationOnTestDescription": "Zdarzenie używane do testowania systemu powiadomień", + "PlaceholderBulkChapterInput": "Wpisz tytuł rozdziału lub użyj numeracji (np. „Odcinek 1”, „Rozdział 10”, „1.”)", "PlaceholderNewCollection": "Nowa nazwa kolekcji", "PlaceholderNewFolderPath": "Nowa ścieżka folderu", "PlaceholderNewPlaylist": "Nowa nazwa playlisty", @@ -1018,8 +1027,12 @@ "ToastDeviceTestEmailFailed": "NIeudana próba wysłania testowego maila", "ToastDeviceTestEmailSuccess": "Testowy email został wysłany", "ToastEmailSettingsUpdateSuccess": "Ustawienia email zaktualizowane", + "ToastEncodeCancelFailed": "Nie udało się anulować kodowania", + "ToastEncodeCancelSucces": "Kodowanie anulowane", + "ToastEpisodeDownloadQueueClearFailed": "Nie udało się wyczyścić kolejki", "ToastEpisodeDownloadQueueClearSuccess": "Wyczyszczono kolejkę epizodów do ściągnięcia", "ToastEpisodeUpdateSuccess": "Zaktualizowano {0} odcinków", + "ToastErrorCannotShare": "Nie można udostępniać natywnie na tym urządzeniu.", "ToastInvalidImageUrl": "Nieprawidłowy URL obrazu", "ToastInvalidUrl": "Nieprawidłowy URL", "ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe", From e83aca572e901c7bfa17fb61f8a06cd38ef39f0e Mon Sep 17 00:00:00 2001 From: Grzegorz Orlowski Date: Mon, 2 Mar 2026 18:22:18 +0100 Subject: [PATCH 192/285] Translated using Weblate (Polish) Currently translated at 95.7% (1113 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/strings/pl.json b/client/strings/pl.json index 3be1c2dfe..d1bc6c062 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -359,8 +359,8 @@ "LabelExample": "Przykład", "LabelExpandSeries": "Rozwiń serie", "LabelExpandSubSeries": "Rozwiń podserie", - "LabelExpired": "Przeterminowane", - "LabelExpiresAt": "Wygasa w", + "LabelExpired": "Wygasły", + "LabelExpiresAt": "Wygasa o", "LabelExpiresInSeconds": "Wygasa za (sekund)", "LabelExpiresNever": "Nigdy", "LabelExplicit": "18+", @@ -574,7 +574,7 @@ "LabelSelectUsers": "Wybór użytkowników", "LabelSendEbookToDevice": "Wyślij ebook do...", "LabelSequence": "Kolejność", - "LabelSerial": "Seria", + "LabelSerial": "Numer serii", "LabelSeries": "Serie", "LabelSeriesName": "Nazwy serii", "LabelSeriesProgress": "Postęp w serii", @@ -622,7 +622,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", + "LabelShareDownloadableHelp": "Zezwala użytkownikom z linkiem udostępniania na pobranie pliku zip elementu biblioteki.", "LabelShareOpen": "Otwórz udział", "LabelShareURL": "Link do udziału", "LabelShowAll": "Pokaż wszystko", @@ -661,11 +661,11 @@ "LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika", "LabelTagsNotAccessibleToUser": "Znaczniki niedostępne dla użytkownika", "LabelTasks": "Uruchomione zadania", - "LabelTextEditorBulletedList": "Lista punktowana", + "LabelTextEditorBulletedList": "Lista wypunktowana", "LabelTextEditorLink": "Link", "LabelTextEditorNumberedList": "Lista numerowana", "LabelTextEditorUnlink": "Usuń link", - "LabelTheme": "Kompozycja", + "LabelTheme": "Motyw", "LabelThemeDark": "Ciemny", "LabelThemeLight": "Jasny", "LabelThemeSepia": "Sepia", @@ -693,7 +693,7 @@ "LabelTrackFromMetadata": "Ścieżka z metadanych", "LabelTracks": "Ścieżki", "LabelTracksMultiTrack": "Wielościeżkowy", - "LabelTracksNone": "Brak ścieżek", + "LabelTracksNone": "Brak utworów", "LabelTracksSingleTrack": "Pojedyncza ścieżka", "LabelTrailer": "Zwiastun", "LabelType": "Typ", @@ -710,7 +710,7 @@ "LabelUploaderDragAndDropFilesOnly": "Przeciągnij i upuść pliki", "LabelUploaderDropFiles": "Puść pliki", "LabelUploaderItemFetchMetadataHelp": "Automatycznie pobierz tytuł, autora i serie", - "LabelUseAdvancedOptions": "Opcje zaawansowane", + "LabelUseAdvancedOptions": "Użyj ustawień zaawansowanych", "LabelUseChapterTrack": "Użyj ścieżki rozdziału", "LabelUseFullTrack": "Użycie ścieżki rozdziału", "LabelUseZeroForUnlimited": "Użyj 0, aby wyłączyć ograniczenia", @@ -745,7 +745,7 @@ "MessageBackupsLocationNoEditNote": "Uwaga: Lokalizacja kopii zapasowej jest ustawiona poprzez zmienną środowiskową i nie może być tutaj zmieniona.", "MessageBackupsLocationPathEmpty": "Ścieżka do kopii zapasowej nie może być pusta", "MessageBatchEditPopulateMapDetailsAllHelp": "Wypełnij włączone pola danymi ze wszystkich elementów. Pola z wieloma wartościami zostaną scalone.", - "MessageBatchEditPopulateMapDetailsItemHelp": "Wypełnij pola szczegółów mapy włączonej danymi z tego elementu", + "MessageBatchEditPopulateMapDetailsItemHelp": "Wypełnia włączone pola szczegółów mapy danymi z tego elementu", "MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.", "MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji", "MessageBookshelfNoCollectionsHelp": "Kolekcje są publiczne. Wszyscy użytkownicy mający dostęp do biblioteki mogą je zobaczyć.", From d9355ac3aa175e9184db64e42c62a934aab933a0 Mon Sep 17 00:00:00 2001 From: Oliver Marriott Date: Tue, 10 Mar 2026 23:51:57 +1100 Subject: [PATCH 193/285] Force AAC transcode when streaming mka+opus to desktop client Matroska audio containers (aka mka files) with Opus codec streams inside were unplayable on the desktop client because hls.js was unable to decode the stream, resulting in an infinitely "spinning" play button. When configuring a stream, we now check for the opus codec and force AAC transcoding. Matroska containers support other codecs besides Opus, eg: mp3, which do not require transcoding and work fine before this patch, which is why we check for opus in codecsToForceAAC instead of AudioMimeType.MKA in mimeTypesToForceAAC. The AudioMimeType.OPUS mimetype is already marked as requiring transcoding but since its inside a container this check does not evaluate to true, we must check the codec explicitly. --- server/objects/Stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 5aa013e8e..70361463f 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -73,7 +73,7 @@ class Stream extends EventEmitter { return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF] } get codecsToForceAAC() { - return ['alac', 'ac3', 'eac3'] + return ['alac', 'ac3', 'eac3', 'opus'] } get userToken() { return this.user.token From 690a7e0da91485d8c7521b930c0ae9f4909ce24f Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 11 Mar 2026 17:03:07 -0500 Subject: [PATCH 194/285] Update session DeviceInfo with sanitize on clientDeviceInfo --- client/pages/config/sessions.vue | 26 +++++++++++----- client/pages/config/users/_id/sessions.vue | 16 ++++++---- server/objects/DeviceInfo.js | 36 +++++++++------------- server/utils/htmlSanitizer.js | 2 ++ 4 files changed, 46 insertions(+), 34 deletions(-) diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index 86ec7eec6..4341ea07d 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -66,7 +66,11 @@

{{ getPlayMethodName(session.playMethod) }}

-

+

+ +

{{ $elapsedPrettyLocalized(session.timeListening) }}

@@ -130,7 +134,11 @@

{{ getPlayMethodName(session.playMethod) }}

-

+

+ +

{{ $elapsedPretty(session.timeListening) }}

@@ -172,7 +180,11 @@

{{ getPlayMethodName(session.playMethod) }}

-

+

+ +

{{ $secondsToTimestamp(session.currentTime) }}

@@ -433,16 +445,16 @@ export default { this.selectedSession = session this.showSessionModal = true }, - getDeviceInfoString(deviceInfo) { - if (!deviceInfo) return '' - var lines = [] + getDeviceInfoLines(deviceInfo) { + if (!deviceInfo) return [] + const lines = [] if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.browserName) lines.push(deviceInfo.browserName) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) - return lines.join('
') + return lines }, getPlayMethodName(playMethod) { if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue index 060f34be8..cb18f5ee3 100644 --- a/client/pages/config/users/_id/sessions.vue +++ b/client/pages/config/users/_id/sessions.vue @@ -38,8 +38,12 @@

{{ getPlayMethodName(session.playMethod) }}

-

- +

+ +

+

{{ $elapsedPrettyLocalized(session.timeListening) }}

@@ -193,16 +197,16 @@ export default { this.selectedSession = session this.showSessionModal = true }, - getDeviceInfoString(deviceInfo) { - if (!deviceInfo) return '' - var lines = [] + getDeviceInfoLines(deviceInfo) { + if (!deviceInfo) return [] + const lines = [] if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.browserName) lines.push(deviceInfo.browserName) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) - return lines.join('
') + return lines }, getPlayMethodName(playMethod) { if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' diff --git a/server/objects/DeviceInfo.js b/server/objects/DeviceInfo.js index ceff6c32e..22ebfbea4 100644 --- a/server/objects/DeviceInfo.js +++ b/server/objects/DeviceInfo.js @@ -1,6 +1,10 @@ -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 +const { stripAllTags } = require('../utils/htmlSanitizer') class DeviceInfo { + /** @type {string[]} Fields to sanitize when loading from stored data */ + static stringFields = ['deviceId', 'clientVersion', 'manufacturer', 'model', 'sdkVersion', 'clientName', 'deviceName'] + constructor(deviceInfo = null) { this.id = null this.userId = null @@ -31,7 +35,7 @@ class DeviceInfo { construct(deviceInfo) { for (const key in deviceInfo) { if (deviceInfo[key] !== undefined && this[key] !== undefined) { - this[key] = deviceInfo[key] + this[key] = DeviceInfo.stringFields.includes(key) ? stripAllTags(deviceInfo[key]) : deviceInfo[key] } } } @@ -63,7 +67,8 @@ class DeviceInfo { } get deviceDescription() { - if (this.model) { // Set from mobile apps + if (this.model) { + // Set from mobile apps if (this.sdkVersion) return `${this.model} SDK ${this.sdkVersion} / v${this.clientVersion}` return `${this.model} / v${this.clientVersion}` } @@ -72,18 +77,7 @@ class DeviceInfo { // When client doesn't send a device id getTempDeviceId() { - const keys = [ - this.userId, - this.browserName, - this.browserVersion, - this.osName, - this.osVersion, - this.clientVersion, - this.manufacturer, - this.model, - this.sdkVersion, - this.ipAddress - ].map(k => k || '') + const keys = [this.userId, this.browserName, this.browserVersion, this.osName, this.osVersion, this.clientVersion, this.manufacturer, this.model, this.sdkVersion, this.ipAddress].map((k) => k || '') return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') } @@ -99,12 +93,12 @@ class DeviceInfo { this.osVersion = ua?.os.version || null this.deviceType = ua?.device.type || null - this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion - this.manufacturer = clientDeviceInfo?.manufacturer || null - this.model = clientDeviceInfo?.model || null - this.sdkVersion = clientDeviceInfo?.sdkVersion || null + this.clientVersion = stripAllTags(clientDeviceInfo?.clientVersion) || serverVersion + this.manufacturer = stripAllTags(clientDeviceInfo?.manufacturer) || null + this.model = stripAllTags(clientDeviceInfo?.model) || null + this.sdkVersion = stripAllTags(clientDeviceInfo?.sdkVersion) || null - this.clientName = clientDeviceInfo?.clientName || null + this.clientName = stripAllTags(clientDeviceInfo?.clientName) || null if (this.sdkVersion) { if (!this.clientName) this.clientName = 'Abs Android' this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}` @@ -149,4 +143,4 @@ class DeviceInfo { return hasUpdates } } -module.exports = DeviceInfo \ No newline at end of file +module.exports = DeviceInfo diff --git a/server/utils/htmlSanitizer.js b/server/utils/htmlSanitizer.js index 4ed30e727..dbac1e5e0 100644 --- a/server/utils/htmlSanitizer.js +++ b/server/utils/htmlSanitizer.js @@ -27,6 +27,8 @@ function sanitize(html) { module.exports.sanitize = sanitize function stripAllTags(html, shouldDecodeEntities = true) { + if (typeof html !== 'string') return '' + const sanitizerOptions = { allowedTags: [], disallowedTagsMode: 'discard' From 972193b19352752966f5afbbbb1e264868ff78d9 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 11 Mar 2026 17:18:05 -0500 Subject: [PATCH 195/285] Update server settings authLoginCustomMessage to sanitize on save and load --- server/objects/settings/ServerSettings.js | 8 ++++++-- server/utils/htmlSanitizer.js | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index a03e17c75..99f5b76aa 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -3,6 +3,7 @@ const packageJson = require('../../../package.json') const { BookshelfView } = require('../../utils/constants') const Logger = require('../../Logger') const User = require('../../models/User') +const { sanitize } = require('../../utils/htmlSanitizer') class ServerSettings { constructor(settings) { @@ -126,7 +127,7 @@ class ServerSettings { this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 - this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0 + this.authLoginCustomMessage = sanitize(settings.authLoginCustomMessage) || null // Added v2.8.0 this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null @@ -309,7 +310,7 @@ class ServerSettings { get authFormData() { const clientFormData = { - authLoginCustomMessage: this.authLoginCustomMessage + authLoginCustomMessage: sanitize(this.authLoginCustomMessage) } if (this.authActiveAuthMethods.includes('openid')) { clientFormData.authOpenIDButtonText = this.authOpenIDButtonText @@ -327,6 +328,9 @@ class ServerSettings { update(payload) { let hasUpdates = false for (const key in payload) { + if (key === 'authLoginCustomMessage') { + payload[key] = sanitize(payload[key]) + } if (key === 'sortingPrefixes') { // Sorting prefixes are updated with the /api/sorting-prefixes endpoint continue diff --git a/server/utils/htmlSanitizer.js b/server/utils/htmlSanitizer.js index dbac1e5e0..be839b7c2 100644 --- a/server/utils/htmlSanitizer.js +++ b/server/utils/htmlSanitizer.js @@ -5,11 +5,10 @@ const { entities } = require('./htmlEntities') * * @param {string} html * @returns {string} - * @throws {Error} if input is not a string */ function sanitize(html) { if (typeof html !== 'string') { - throw new Error('sanitizeHtml: input must be a string') + return '' } const sanitizerOptions = { From e66ffb9c23cdc7f7b45538c443b1bc96828b1f7a Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 12 Mar 2026 16:37:59 -0500 Subject: [PATCH 196/285] Add indexes to MediaProgress and BookSeries models --- server/migrations/v2.32.2-add-discover-query-indexes.js | 2 +- server/models/BookSeries.js | 4 ++++ server/models/MediaProgress.js | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/migrations/v2.32.2-add-discover-query-indexes.js b/server/migrations/v2.32.2-add-discover-query-indexes.js index 26b12383d..801561fc9 100644 --- a/server/migrations/v2.32.2-add-discover-query-indexes.js +++ b/server/migrations/v2.32.2-add-discover-query-indexes.js @@ -14,7 +14,7 @@ const loggerPrefix = `[${migrationVersion} migration]` const indexes = [ { table: 'mediaProgresses', - name: 'media_progress_user_item_finished_time', + name: 'media_progresses_user_item_finished_time', fields: ['userId', 'mediaItemId', 'isFinished', 'currentTime'] }, { diff --git a/server/models/BookSeries.js b/server/models/BookSeries.js index 31eccb9fa..e71f28a8e 100644 --- a/server/models/BookSeries.js +++ b/server/models/BookSeries.js @@ -48,6 +48,10 @@ class BookSeries extends Model { { name: 'bookSeries_seriesId', fields: ['seriesId'] + }, + { + name: 'book_series_series_book', + fields: ['seriesId', 'bookId'] } ] } diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 0ebe2f598..9c0269a9e 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -80,6 +80,10 @@ class MediaProgress extends Model { indexes: [ { fields: ['updatedAt'] + }, + { + name: 'media_progresses_user_item_finished_time', + fields: ['userId', 'mediaItemId', 'isFinished', 'currentTime'] } ] } From a47c869d0b252c9bfd8ecc0b902e5a06d0cf1742 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 12 Mar 2026 16:45:08 -0500 Subject: [PATCH 197/285] Update migration file to v2.33.0 --- ...r-query-indexes.js => v2.33.0-add-discover-query-indexes.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename server/migrations/{v2.32.2-add-discover-query-indexes.js => v2.33.0-add-discover-query-indexes.js} (98%) diff --git a/server/migrations/v2.32.2-add-discover-query-indexes.js b/server/migrations/v2.33.0-add-discover-query-indexes.js similarity index 98% rename from server/migrations/v2.32.2-add-discover-query-indexes.js rename to server/migrations/v2.33.0-add-discover-query-indexes.js index 801561fc9..ebd92bbaa 100644 --- a/server/migrations/v2.32.2-add-discover-query-indexes.js +++ b/server/migrations/v2.33.0-add-discover-query-indexes.js @@ -7,7 +7,7 @@ * @property {MigrationContext} context */ -const migrationVersion = '2.32.2' +const migrationVersion = '2.33.0' const migrationName = `${migrationVersion}-add-discover-query-indexes` const loggerPrefix = `[${migrationVersion} migration]` From 6d3773a0b83b61b07bb29632c8deda404f865d3a Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 12 Mar 2026 17:01:54 -0500 Subject: [PATCH 198/285] Version bump v2.33.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 1e2d52c11..299741bd5 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.33.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.33.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 0eaffb106..a1503a506 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.33.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 08707893d..e07fba51d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.32.1", + "version": "2.33.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.32.1", + "version": "2.33.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 3ee3fb391..3108b5170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.32.1", + "version": "2.33.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 874e9e18564650bcedd33d63b5cdbb3ba86f5541 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 18 Mar 2026 16:17:45 -0500 Subject: [PATCH 199/285] Update API Key jwtAuthCheck to check user active status --- server/auth/TokenManager.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index faa6774a3..5efeb7a64 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -234,6 +234,13 @@ class TokenManager { } const user = await Database.userModel.getUserById(apiKey.userId) + + if (!user?.isActive) { + // deny login + done(null, null) + return + } + done(null, user) } else { // JWT based authentication From 7c0d9efe9165ea07cf1ff7ff5404b6a8d35af148 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 18 Mar 2026 16:51:51 -0500 Subject: [PATCH 200/285] Update Confirm component to support allowHtml prompt option --- client/components/app/Appbar.vue | 1 + client/components/modals/item/tabs/Tools.vue | 1 + client/components/prompt/Confirm.vue | 22 ++++++++++++++++++- client/pages/config/index.vue | 2 +- .../config/item-metadata-utils/genres.vue | 4 ++-- .../pages/config/item-metadata-utils/tags.vue | 4 ++-- 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index f74134041..428ae6ebf 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -196,6 +196,7 @@ export default { requestBatchQuickEmbed() { const payload = { message: this.$strings.MessageConfirmQuickEmbed, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index d96550887..8e40ca3c0 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -107,6 +107,7 @@ export default { quickEmbed() { const payload = { message: this.$strings.MessageConfirmQuickEmbed, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/components/prompt/Confirm.vue b/client/components/prompt/Confirm.vue index 361765e2d..d0828c7d1 100644 --- a/client/components/prompt/Confirm.vue +++ b/client/components/prompt/Confirm.vue @@ -3,7 +3,8 @@
-

+

+

{{ message }}

@@ -52,6 +53,17 @@ export default { message() { return this.confirmPromptOptions.message || '' }, + allowHtmlMessage() { + return !!this.confirmPromptOptions.allowHtml + }, + sanitizedMessage() { + if (!this.allowHtmlMessage) return this.message + + return this.escapeHtml(this.message) + .replace(/<br\s*\/?>/gi, '
') + .replace(/<code>/gi, '') + .replace(/<\/code>/gi, '') + }, callback() { return this.confirmPromptOptions.callback }, @@ -103,6 +115,14 @@ export default { if (this.callback) this.callback(true, this.checkboxValue) this.show = false }, + escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + }, setShow() { this.checkboxValue = this.checkboxDefaultValue this.$eventBus.$emit('showing-prompt', true) diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index b8cf3cff2..7fb84d468 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -390,8 +390,8 @@ export default { }, purgeItemsCache() { const payload = { - // message: `This will delete the entire folder at /metadata/cache/items.
Are you sure you want to purge items cache?`, message: this.$strings.MessageConfirmPurgeItemsCache, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.sendPurgeItemsCache() diff --git a/client/pages/config/item-metadata-utils/genres.vue b/client/pages/config/item-metadata-utils/genres.vue index 87e14bbd8..8abe34eef 100644 --- a/client/pages/config/item-metadata-utils/genres.vue +++ b/client/pages/config/item-metadata-utils/genres.vue @@ -90,9 +90,9 @@ export default { let message = this.$getString('MessageConfirmRenameGenre', [this.editingGenre, this.newGenreName]) if (genreNameExists) { - message += `
${this.$strings.MessageConfirmRenameGenreMergeNote}` + message += ` ${this.$strings.MessageConfirmRenameGenreMergeNote}` } else if (genreNameExistsOfDifferentCase) { - message += `
${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}` + message += ` ${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}` } const payload = { diff --git a/client/pages/config/item-metadata-utils/tags.vue b/client/pages/config/item-metadata-utils/tags.vue index 2fe5c0f1a..4dfc39424 100644 --- a/client/pages/config/item-metadata-utils/tags.vue +++ b/client/pages/config/item-metadata-utils/tags.vue @@ -86,9 +86,9 @@ export default { let message = this.$getString('MessageConfirmRenameTag', [this.editingTag, this.newTagName]) if (tagNameExists) { - message += `
${this.$strings.MessageConfirmRenameTagMergeNote}` + message += ` ${this.$strings.MessageConfirmRenameTagMergeNote}` } else if (tagNameExistsOfDifferentCase) { - message += `
${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}` + message += ` ${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}` } const payload = { From 4bdd76d94c450f2de329379cecf4a672dbd23a9d Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 18 Mar 2026 17:01:19 -0500 Subject: [PATCH 201/285] Update podcast episode update endpoint to sanitize subtitle --- server/controllers/PodcastController.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 1ebe1d110..c70287600 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -412,6 +412,12 @@ class PodcastController { Logger.debug(`[PodcastController] Sanitized description from "${req.body[key]}" to "${sanitizedDescription}"`) req.body[key] = sanitizedDescription } + } else if (key === 'subtitle' && req.body[key]) { + const sanitizedSubtitle = htmlSanitizer.sanitize(req.body[key]) + if (sanitizedSubtitle !== req.body[key]) { + Logger.debug(`[PodcastController] Sanitized subtitle from "${req.body[key]}" to "${sanitizedSubtitle}"`) + req.body[key] = sanitizedSubtitle + } } updatePayload[key] = req.body[key] From 5d9682410a62d54ba71df7f89e2542ed529c69b2 Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 13 Mar 2026 03:00:30 +0100 Subject: [PATCH 202/285] Translated using Weblate (French) 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/fr/ --- client/strings/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index 497426d37..507574033 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -622,7 +622,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les fichiers de métadonnées sont stockés dans /metadata/items. En activant ce paramètre, les fichiers de métadonnées seront stockés dans les dossiers des éléments de votre bibliothèque", "LabelSettingsTimeFormat": "Format d’heure", "LabelShare": "Partager", - "LabelShareDownloadableHelp": "Permet aux utilisateurs de télécharger un fichier ZIP de l'élément de la bibliothèque.", + "LabelShareDownloadableHelp": "Permet aux utilisateurs disposant du lien de partage de télécharger un fichier zip contenant l'élément de la bibliothèque.", "LabelShareOpen": "Ouvrir le partage", "LabelShareURL": "Partager l’URL", "LabelShowAll": "Tout afficher", From 15af7407ffed805645eca78034a9350e7ad4c3b6 Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Fri, 13 Mar 2026 14:22:29 +0100 Subject: [PATCH 203/285] Translated using Weblate (Belarusian) 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/be/ --- client/strings/be.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index 6dbef9cb9..90504121e 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -81,7 +81,7 @@ "ButtonRemove": "Выдаліць", "ButtonRemoveAll": "Выдаліць усе", "ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі", - "ButtonRemoveFromContinueListening": "Выдаліць з Працягваць слухаць", + "ButtonRemoveFromContinueListening": "Выдаліць з Працягнуць праслухоўванне", "ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне", "ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю", "ButtonReset": "Скінуць", @@ -107,10 +107,10 @@ "ButtonSubmit": "Адправіць", "ButtonTest": "Тэст", "ButtonUnlinkOpenId": "Адвязаць OpenID", - "ButtonUpload": "Загрузіць", - "ButtonUploadBackup": "Загрузіць рэзервовую копію", - "ButtonUploadCover": "Загрузіць вокладку", - "ButtonUploadOPMLFile": "Загрузіць файл OPML", + "ButtonUpload": "Запампаваць", + "ButtonUploadBackup": "Запампаваць рэзервовую копію", + "ButtonUploadCover": "Запампаваць вокладку", + "ButtonUploadOPMLFile": "Запампаваць файл OPML", "ButtonUserDelete": "Выдаліць карыстальніка {0}", "ButtonUserEdit": "Рэдагаваць карыстальніка {0}", "ButtonViewAll": "Прагледзець усе", @@ -149,7 +149,7 @@ "HeaderFindChapters": "Знайсці раздзелы", "HeaderIgnoredFiles": "Ігнараваныя файлы", "HeaderItemFiles": "Файлы элементаў", - "HeaderItemMetadataUtils": "Утыліты для метаданых элементаў", + "HeaderItemMetadataUtils": "Утыліты для метаданых", "HeaderLastListeningSession": "Апошні сеанс праслухоўвання", "HeaderLatestEpisodes": "Апошнія выпускі", "HeaderLibraries": "Бібліятэкі", @@ -207,8 +207,8 @@ "HeaderStatsLongestItems": "Найдаўжэйшыя элементы (гадзіны)", "HeaderStatsMinutesListeningChart": "Хвілін праслухоўвання (апошнія 7 дзён)", "HeaderStatsRecentSessions": "Апошнія сеансы", - "HeaderStatsTop10Authors": "10 лепшых аўтараў", - "HeaderStatsTop5Genres": "5 лепшых жанраў", + "HeaderStatsTop10Authors": "Топ 10 аўтараў", + "HeaderStatsTop5Genres": "Топ 5 жанраў", "HeaderTableOfContents": "Змест", "HeaderTools": "Інструменты", "HeaderUpdateAccount": "Абнавіць уліковы запіс", @@ -292,7 +292,7 @@ "LabelCollections": "Калекцыі", "LabelComplete": "Завяршыць", "LabelConfirmPassword": "Пацвердзіце пароль", - "LabelContinueListening": "Працягваць слухаць", + "LabelContinueListening": "Працягнуць праслухоўванне", "LabelContinueReading": "Працягнуць чытанне", "LabelContinueSeries": "Працягнуць серыі", "LabelCorsAllowed": "Дазволеныя крыніцы CORS", @@ -545,7 +545,7 @@ "LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі", "LabelRSSFeedURL": "URL RSS-стужкі", "LabelRandomly": "Выпадкова", - "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць", + "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягнуць праслухоўванне", "LabelRead": "Чытаць", "LabelReadAgain": "Чытаць зноў", "LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу", @@ -588,7 +588,7 @@ "LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі", "LabelSettingsChromecastSupport": "Падтрымка Chromecast", "LabelSettingsDateFormat": "Фарматы даты", - "LabelSettingsEnableWatcher": "Аўтаматычна сачыць за зменамі ў бібліятэцках", + "LabelSettingsEnableWatcher": "Аўтаматычна сачыць за зменамі ў бібліятэках", "LabelSettingsEnableWatcherForLibrary": "Аўтаматычна сачыць за зменамі ў бібліятэцы", "LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера", "LabelSettingsEpubsAllowScriptedContent": "Дазваляць скрыпты ў файлах EPUB", @@ -639,9 +639,9 @@ "LabelStarted": "Пачата", "LabelStartedAt": "Пачата ў", "LabelStartedDate": "Пачата {0}", - "LabelStatsAudioTracks": "Аўдыятрэкі", - "LabelStatsAuthors": "Аўтары", - "LabelStatsBestDay": "Лепшы дзень", + "LabelStatsAudioTracks": "Аўдыятрэкаў", + "LabelStatsAuthors": "Аўтараў", + "LabelStatsBestDay": "Найлепшы дзень", "LabelStatsDailyAverage": "У сярэднім за дзень", "LabelStatsDays": "Дзён", "LabelStatsDaysListened": "Дзён праслухана", @@ -968,7 +968,7 @@ "StatsBooksAdditional": "Некаторыя дапаўненні ўключаюць…", "StatsBooksFinished": "завершана кніг", "StatsBooksFinishedThisYear": "Некаторыя кнігі завершаны ў гэтым годзе…", - "StatsBooksListenedTo": "кнігі, якія былі праслуханы", + "StatsBooksListenedTo": "кніг праслухана", "StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…", "StatsSessions": "сеансаў", "StatsSpentListening": "праслухана", @@ -1147,7 +1147,7 @@ "ToastUnlinkOpenIdFailed": "Не ўдалося адвязаць карыстальніка ад OpenID", "ToastUnlinkOpenIdSuccess": "Карыстальнік адвязаны ад OpenID", "ToastUploaderFilepathExistsError": "Файл \"{0}\" ужо існуе на серверы", - "ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.", + "ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху запампоўкі.", "ToastUserDeleteFailed": "Не ўдалося выдаліць карыстальніка", "ToastUserDeleteSuccess": "Карыстальнік выдалены", "ToastUserPasswordChangeSuccess": "Пароль паспяхова зменены", From dc54d42dcfd204c93c82068d19c0f83bf53c3124 Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Fri, 13 Mar 2026 15:46:52 +0100 Subject: [PATCH 204/285] Translated using Weblate (Belarusian) 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/be/ --- client/strings/be.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index 90504121e..d598ef7bf 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -50,7 +50,7 @@ "ButtonManageTracks": "Кіраванне трэкамі", "ButtonMapChapterTitles": "Супаставіць загалоўкі раздзелаў", "ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў", - "ButtonMatchBooks": "Падбор кніг", + "ButtonMatchBooks": "Параўнаць кнігі", "ButtonNevermind": "Няважна", "ButtonNext": "Далей", "ButtonNextChapter": "Наступны раздзел", @@ -196,7 +196,7 @@ "HeaderSession": "Сеанс", "HeaderSetBackupSchedule": "Наладзіць расклад рэзервовага капіравання", "HeaderSettings": "Налады", - "HeaderSettingsDisplay": "Дысплей", + "HeaderSettingsDisplay": "Выгляд", "HeaderSettingsExperimental": "Эксперыментальныя функцыі", "HeaderSettingsGeneral": "Агульныя", "HeaderSettingsScanner": "Сканер", @@ -239,8 +239,8 @@ "LabelAll": "Усе", "LabelAllEpisodesDownloaded": "Усе выпускі спампаваныя", "LabelAllUsers": "Усе карыстальнікі", - "LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей", - "LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей", + "LabelAllUsersExcludingGuests": "Усіх карыстальнікаў, акрамя гасцей", + "LabelAllUsersIncludingGuests": "Усіх карыстальнікаў, уключаючы гасцей", "LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы", "LabelApiKeyCreated": "API-ключ \"{0}\" паспяхова створаны.", "LabelApiKeyCreatedDescription": "Пераканайцеся, што вы скапіявалі API-ключ зараз, бо паўторна яго ўбачыць не атрымаецца.", @@ -264,7 +264,7 @@ "LabelAutoRegisterDescription": "Аўтаматычна ствараць новых карыстальнікаў пасля ўваходу ў сістэму", "LabelBackToUser": "Вярнуцца да карыстальніка", "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыяфайлаў", - "LabelBackupLocation": "Месцазнаходжанне рэзервовых копій", + "LabelBackupLocation": "Размяшчэнне рэзервовых копій", "LabelBackupsEnableAutomaticBackups": "Аўтаматычнае рэзервовае капіраванне", "LabelBackupsEnableAutomaticBackupsHelp": "Рэзервовыя копіі захаваныя ў /metadata/backups", "LabelBackupsMaxBackupSize": "Максімальны памер рэзервовай копіі (у ГБ) (0 — неабмежавана)", @@ -316,7 +316,7 @@ "LabelDirectory": "Каталог", "LabelDiscFromFilename": "Дыск з файла", "LabelDiscFromMetadata": "Дыск з метаданых", - "LabelDiscover": "Знайсці", + "LabelDiscover": "Знаходкі", "LabelDownload": "Спампаваць", "LabelDownloadNEpisodes": "Спампавана {0} выпускаў", "LabelDownloadable": "Спампоўваецца", @@ -332,7 +332,7 @@ "LabelEmailSettingsFromAddress": "Адрас адпраўніка", "LabelEmailSettingsRejectUnauthorized": "Адхіляць неаўтарызаваныя сертыфікаты", "LabelEmailSettingsRejectUnauthorizedHelp": "Адключэнне праверкі SSL-сертыфіката можа зрабіць ваша злучэнне ўразлівым перад пагрозамі бяспекі, такімі як атакі \"чалавек пасярэдзіне\". Адключайце гэтую опцыю толькі калі цалкам разумееце наступствы і ўпэўнены ў надзейнасці паштовага сервера.", - "LabelEmailSettingsSecure": "Бяспечныя", + "LabelEmailSettingsSecure": "Бяспечна", "LabelEmailSettingsSecureHelp": "Калі ўключана, злучэнне будзе выкарыстоўваць TLS пры падключэнні да сервера. Калі выключана, TLS будзе выкарыстоўвацца толькі ў выпадку падтрымкі пашырэння STARTTLS на серверы. У большасці выпадкаў усталюйце значэнне true пры падключэнні да порта 465. Для партоў 587 або 25 не ўключайце яго. (інфармацыя з nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Тэставы адрас", "LabelEmbeddedCover": "Убудаваная вокладка", @@ -595,16 +595,16 @@ "LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць файлам EPUB выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы файлаў EPUB.", "LabelSettingsExperimentalFeatures": "Эксперыментальныя функцыі", "LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.", - "LabelSettingsFindCovers": "Знайсці вокладкі", + "LabelSettingsFindCovers": "Шукаць вокладкі", "LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або відарыса вокладкі ў папцы, сканер паспрабуе знайсці вокладку.
Заўвага: гэта павялічыць час сканіравання", "LabelSettingsHideSingleBookSeries": "Схаваць серыі з адной кнігай", "LabelSettingsHideSingleBookSeriesHelp": "Серыі, якія змяшчаюць толькі адну кнігу, будуць схаваны са старонкі серый і паліц на галоўнай старонцы.", - "LabelSettingsHomePageBookshelfView": "Галоўная старонка выкарыстоўвае выгляд кніжнай паліцы", - "LabelSettingsLibraryBookshelfView": "Бібліятэка выкарыстоўвае выгляд кніжнай паліцы", + "LabelSettingsHomePageBookshelfView": "Кніжныя паліцы на галоўнай старонцы", + "LabelSettingsLibraryBookshelfView": "Кніжныя паліцы ў бібліятэцы", "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Працэнт завяршэння большы за", - "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунд)", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, меншы за (секунд)", "LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як завершаны, калі", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусціць папярэднія кнігі ў \"Працягнуць серыю\"", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Прапусціць папярэднія кнігі ў \"Працягнуць серыю\"", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Паліца \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.", "LabelSettingsParseSubtitles": "Аналізаваць падзагалоўкі", "LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў папак аўдыякніг.
Падзагаловак павінен быць аддзелены сімвалам \" - \".
Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"", @@ -618,7 +618,7 @@ "LabelSettingsSquareBookCoversHelp": "Аддаваць перавагу квадратным вокладкам замест стандартных вокладак з суадносінамі бакоў 1.6:1", "LabelSettingsStoreCoversWithItem": "Захоўваць вокладкі з элементам", "LabelSettingsStoreCoversWithItemHelp": "Прадвызначана вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у папцы элемента бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"", - "LabelSettingsStoreMetadataWithItem": "Захоўваць метаданыя разам з элементам", + "LabelSettingsStoreMetadataWithItem": "Захоўваць метаданыя з элементам", "LabelSettingsStoreMetadataWithItemHelp": "Прадвызначана метаданыя захоўваюцца ў /metadata/items. Пры ўключэнні гэтай опцыі файлаў метаданых будуць захоўвацца ў папках элементаў бібліятэкі", "LabelSettingsTimeFormat": "Фармат часу", "LabelShare": "Абагуліць", @@ -655,7 +655,7 @@ "LabelStatsOverallHours": "Агульная колькасць гадзін", "LabelStatsWeekListening": "Праслухана за тыдзень", "LabelSubtitle": "Падзагаловак", - "LabelSupportedFileTypes": "Падтрымліваемыя тыпы файлаў", + "LabelSupportedFileTypes": "Падтрымліваюцца тыпы файлаў", "LabelTag": "Метка", "LabelTags": "Меткі", "LabelTagsAccessibleToUser": "Меткі, даступныя карыстальніку", @@ -742,7 +742,7 @@ "MessageAuthenticationSecurityMessage": "Дзеля бяспекі была палепшана аўтэнтыфікацыя. Усім карыстальнікам трэба паўторна ўвайсці ў сістэму.", "MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і відарысы, якія захоўваюцца ў /metadata/items і /metadata/authors. Рэзервовыя копіі не ўключаюць файлы, якія захоўваюцца ў папках бібліятэкі.", "MessageBackupsLocationEditNote": "Заўвага: Абнаўленне месцазнаходжання рэзервовых копій не перамяшчае і не змяняе існуючыя рэзервовыя копіі", - "MessageBackupsLocationNoEditNote": "Заўвага: Месцазнаходжанне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.", + "MessageBackupsLocationNoEditNote": "Заўвага: Размяшчэнне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.", "MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым", "MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі данымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны", "MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты данымі з гэтага элемента", From f84831d6f1b8563dc779ebec9ed22b3fee3906f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20J=C3=BClich?= Date: Sat, 14 Mar 2026 00:00:58 +0100 Subject: [PATCH 205/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 276348656..ed0cae52c 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -331,7 +331,7 @@ "LabelEmail": "E-Mail", "LabelEmailSettingsFromAddress": "Sender", "LabelEmailSettingsRejectUnauthorized": "Nicht autorisierte Zertifikate ablehnen", - "LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn due die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.", + "LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn du die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.", "LabelEmailSettingsSecure": "Sicher", "LabelEmailSettingsSecureHelp": "Wenn an, verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei „aus“ wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf „an“ schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert „aus“ bei. (von nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test-Adresse", From 32a17c0044919edbea24f1fc7b22e88bde635b83 Mon Sep 17 00:00:00 2001 From: Francisco Serrador Date: Wed, 18 Mar 2026 22:15:11 +0100 Subject: [PATCH 206/285] Translated using Weblate (Spanish) Currently translated at 99.7% (1160 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 138 ++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 57 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index 73ec1cd2d..4569f04bd 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -20,10 +20,10 @@ "ButtonCheckAndDownloadNewEpisodes": "Comprobar y descargar episodios nuevos", "ButtonChooseAFolder": "Elegir una carpeta", "ButtonChooseFiles": "Elegir archivos", - "ButtonClearFilter": "Quitar filtros", + "ButtonClearFilter": "Vaciar filtro", "ButtonClose": "Cerrar", "ButtonCloseFeed": "Cerrar suministro", - "ButtonCloseSession": "Cerrar la sesión abierta", + "ButtonCloseSession": "Cerrar sesión abierta", "ButtonCollections": "Colecciones", "ButtonConfigureScanner": "Configurar Escáner", "ButtonCreate": "Crear", @@ -33,27 +33,27 @@ "ButtonEdit": "Editar", "ButtonEditChapters": "Editar capítulos", "ButtonEditPodcast": "Editar pódcast", - "ButtonEnable": "Permitir", + "ButtonEnable": "Habilitar", "ButtonFireAndFail": "Ejecutado y fallido", "ButtonFireOnTest": "Activar evento de prueba", "ButtonForceReScan": "Forzar Re-Escaneo", "ButtonFullPath": "Ruta completa", "ButtonHide": "Ocultar", "ButtonHome": "Inicio", - "ButtonIssues": "Problemas", + "ButtonIssues": "Cuestiones", "ButtonJumpBackward": "Retroceder", "ButtonJumpForward": "Adelantar", "ButtonLatest": "Más recientes", "ButtonLibrary": "Biblioteca", - "ButtonLogout": "Salir", - "ButtonLookup": "Buscar", + "ButtonLogout": "Cerrar Sesión", + "ButtonLookup": "Averiguar", "ButtonManageTracks": "Gestionar pistas", "ButtonMapChapterTitles": "Asignar Títulos a Capítulos", "ButtonMatchAllAuthors": "Encontrar Todos los Autores", - "ButtonMatchBooks": "Encontrar Libros", + "ButtonMatchBooks": "Cotejar Libros", "ButtonNevermind": "Olvidar", "ButtonNext": "Siguiente", - "ButtonNextChapter": "Siguiente Capítulo", + "ButtonNextChapter": "Siguiente capítulo", "ButtonNextItemInQueue": "El siguiente elemento en cola", "ButtonOk": "Aceptar", "ButtonOpenFeed": "Abrir suministro", @@ -64,26 +64,26 @@ "ButtonPlaying": "Reproduciendo", "ButtonPlaylists": "Listas de reproducción", "ButtonPrevious": "Anterior", - "ButtonPreviousChapter": "Capítulo Anterior", + "ButtonPreviousChapter": "Capítulo anterior", "ButtonProbeAudioFile": "Examinar archivo de audio", - "ButtonPurgeAllCache": "Purgar toda la antememoria", - "ButtonPurgeItemsCache": "Purgar antememoria de elementos", - "ButtonQueueAddItem": "Añadir a la cola", - "ButtonQueueRemoveItem": "Quitar de la cola", + "ButtonPurgeAllCache": "Purgar toda la caché", + "ButtonPurgeItemsCache": "Purgar caché de elementos", + "ButtonQueueAddItem": "Añadir a cola", + "ButtonQueueRemoveItem": "Quitar de cola", "ButtonQuickEmbed": "Inserción rápida", - "ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente", - "ButtonQuickMatch": "Encontrar Rápido", + "ButtonQuickEmbedMetadata": "Empotrar metadatos rápidamente", + "ButtonQuickMatch": "Cotejo Rápido", "ButtonReScan": "Re-Escanear", "ButtonRead": "Leer", "ButtonReadLess": "Leer menos", "ButtonReadMore": "Leer más", - "ButtonRefresh": "Actualizar", + "ButtonRefresh": "Recargar", "ButtonRemove": "Quitar", "ButtonRemoveAll": "Quitar todo", "ButtonRemoveAllLibraryItems": "Quitar todos los elementos de la biblioteca", - "ButtonRemoveFromContinueListening": "Quitar de Continuar escuchando", - "ButtonRemoveFromContinueReading": "Quitar de Continuar leyendo", - "ButtonRemoveSeriesFromContinueSeries": "Quitar serie de Continuar serie", + "ButtonRemoveFromContinueListening": "Quitar desde Escucha Continua", + "ButtonRemoveFromContinueReading": "Quitar desde Continuar Leyendo", + "ButtonRemoveSeriesFromContinueSeries": "Quitar Series desde Series Continuas", "ButtonReset": "Restablecer", "ButtonResetToDefault": "Restaurar valores predeterminados", "ButtonRestore": "Restaurar", @@ -92,47 +92,47 @@ "ButtonSaveTracklist": "Guardar lista de pistas", "ButtonScan": "Escanear", "ButtonScanLibrary": "Escanear biblioteca", - "ButtonScrollLeft": "Desplazarse hacia la izquierda", - "ButtonScrollRight": "Desplazarse hacia la derecha", + "ButtonScrollLeft": "Desplazarse a la izquierda", + "ButtonScrollRight": "Desplazarse a la derecha", "ButtonSearch": "Buscar", "ButtonSelectFolderPath": "Seleccionar ruta de carpeta", "ButtonSeries": "Series", - "ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas", + "ButtonSetChaptersFromTracks": "Establecer capítulos según las pistas", "ButtonShare": "Compartir", - "ButtonShiftTimes": "Desplazar Tiempos", + "ButtonShiftTimes": "Veces de Desplazo", "ButtonShow": "Mostrar", "ButtonStartM4BEncode": "Iniciar Codificación M4B", - "ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata", + "ButtonStartMetadataEmbed": "Iniciar Inserción de Metadatos", "ButtonStats": "Estadísticas", - "ButtonSubmit": "Enviar", + "ButtonSubmit": "Entregar", "ButtonTest": "Prueba", - "ButtonUnlinkOpenId": "Desvincular OpenID", - "ButtonUpload": "Cargar", - "ButtonUploadBackup": "Cargar respaldo", - "ButtonUploadCover": "Cargar cubierta", - "ButtonUploadOPMLFile": "Cargar archivo OPML", + "ButtonUnlinkOpenId": "Desenlazar OpenID", + "ButtonUpload": "Subir", + "ButtonUploadBackup": "Subir Respaldo", + "ButtonUploadCover": "Subir Cubierta", + "ButtonUploadOPMLFile": "Subir archivo OPML", "ButtonUserDelete": "Eliminar usuario {0}", "ButtonUserEdit": "Editar usuario {0}", "ButtonViewAll": "Ver todo", "ButtonYes": "Sí", "ErrorUploadFetchMetadataAPI": "Error al recuperar los metadatos", - "ErrorUploadFetchMetadataNoResults": "No se pudieron recuperar los metadatos; pruebe a actualizar el título o autor", - "ErrorUploadLacksTitle": "Debe tener título", + "ErrorUploadFetchMetadataNoResults": "No se pudieron recuperar los metadatos; pruebe a actualizar el título y/o autor", + "ErrorUploadLacksTitle": "Debe tener un título", "HeaderAccount": "Cuenta", "HeaderAddCustomMetadataProvider": "Añadir proveedor de metadatos personalizado", "HeaderAdvanced": "Avanzado", "HeaderApiKeys": "Claves API", - "HeaderAppriseNotificationSettings": "Configuración de notificaciones de Apprise", - "HeaderAudioTracks": "Pistas de audio", + "HeaderAppriseNotificationSettings": "Ajustes de notificaciones de Apprise", + "HeaderAudioTracks": "Pistas de Audio", "HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro", "HeaderAuthentication": "Autenticación", "HeaderBackups": "Respaldos", "HeaderBulkChapterModal": "Añadir Múltiples Capítulos", - "HeaderChangePassword": "Cambiar contraseña", + "HeaderChangePassword": "Cambiar Contraseña", "HeaderChapters": "Capítulos", "HeaderChooseAFolder": "Escoger una Carpeta", "HeaderCollection": "Colección", - "HeaderCollectionItems": "Elementos en la colección", + "HeaderCollectionItems": "Elementos de colección", "HeaderCover": "Cubierta", "HeaderCurrentDownloads": "Descargas actuales", "HeaderCustomMessageOnLogin": "Mensaje personalizado al acceder", @@ -144,7 +144,7 @@ "HeaderEmailSettings": "Configuración de correo electrónico", "HeaderEpisodes": "Episodios", "HeaderEreaderDevices": "Dispositivos Ereader", - "HeaderEreaderSettings": "Configuración del lector", + "HeaderEreaderSettings": "Ajustes del lector", "HeaderFiles": "Archivos", "HeaderFindChapters": "Buscar capítulos", "HeaderIgnoredFiles": "Archivos ignorados", @@ -195,14 +195,14 @@ "HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca", "HeaderSession": "Sesión", "HeaderSetBackupSchedule": "Programar Respaldo", - "HeaderSettings": "Configuración", + "HeaderSettings": "Ajustes", "HeaderSettingsDisplay": "Interfaz", "HeaderSettingsExperimental": "Funcionalidades experimentales", "HeaderSettingsGeneral": "Generales", "HeaderSettingsScanner": "Escáner", "HeaderSettingsSecurity": "Seguridad", "HeaderSettingsWebClient": "Cliente web", - "HeaderSleepTimer": "Temporizador de apagado", + "HeaderSleepTimer": "Cronómetro de dormida", "HeaderStatsLargestItems": "Elementos más grandes", "HeaderStatsLongestItems": "Elementos más extensos (h)", "HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)", @@ -212,7 +212,7 @@ "HeaderTableOfContents": "Sumario", "HeaderTools": "Herramientas", "HeaderUpdateAccount": "Actualizar cuenta", - "HeaderUpdateApiKey": "Actualizar clave API", + "HeaderUpdateApiKey": "Actualizar Clave API", "HeaderUpdateAuthor": "Actualizar autor", "HeaderUpdateDetails": "Actualizar detalles", "HeaderUpdateLibrary": "Actualizar biblioteca", @@ -233,8 +233,8 @@ "LabelAddToCollectionBatch": "Añadir {0} libros a colección", "LabelAddToPlaylist": "Añadir a lista de reproducción", "LabelAddToPlaylistBatch": "Añadir {0} elementos a lista de reproducción", - "LabelAddedAt": "Añadido", - "LabelAddedDate": "{0} Añadido", + "LabelAddedAt": "Añadido en", + "LabelAddedDate": "Añadido {0}", "LabelAdminUsersOnly": "Solamente usuarios administradores", "LabelAll": "Todos", "LabelAllEpisodesDownloaded": "Todos los episodios descargados", @@ -255,7 +255,7 @@ "LabelAuthorFirstLast": "Autor (Nombre Apellido)", "LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthors": "Autores", - "LabelAutoDownloadEpisodes": "Descargar episodios automáticamente", + "LabelAutoDownloadEpisodes": "Auto-Descargar episodios", "LabelAutoFetchMetadata": "Recuperar metadatos automáticamente", "LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.", "LabelAutoLaunch": "Lanzamiento automático", @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Libros", "LabelButtonText": "Texto del botón", - "LabelByAuthor": "por", + "LabelByAuthor": "por {0}", "LabelChangePassword": "Cambiar contraseña", "LabelChannels": "Canales", "LabelChapterCount": "{0} capítulos", @@ -286,13 +286,13 @@ "LabelClickToUseCurrentValue": "Pulse para utilizar el valor actual", "LabelClosePlayer": "Cerrar reproductor", "LabelCodec": "Codec", - "LabelCollapseSeries": "Colapsar serie", + "LabelCollapseSeries": "Colapsar Series", "LabelCollapseSubSeries": "Contraer la subserie", "LabelCollection": "Colección", "LabelCollections": "Colecciones", "LabelComplete": "Completo", "LabelConfirmPassword": "Confirmar contraseña", - "LabelContinueListening": "Seguir escuchando", + "LabelContinueListening": "Seguir Escuchando", "LabelContinueReading": "Continuar leyendo", "LabelContinueSeries": "Continuar series", "LabelCorsAllowed": "Orígenes CORS Permitidos", @@ -332,7 +332,7 @@ "LabelEmailSettingsFromAddress": "Remitente", "LabelEmailSettingsRejectUnauthorized": "Rechazar certificados no autorizados", "LabelEmailSettingsRejectUnauthorizedHelp": "Desactivar la validación de certificados SSL puede exponer su conexión a riesgos de seguridad, como los ataques por intermediario. Desactive esta opción solo si conoce las implicaciones y confía en el servidor de correo al que se conecta.", - "LabelEmailSettingsSecure": "Seguridad", + "LabelEmailSettingsSecure": "Seguro", "LabelEmailSettingsSecureHelp": "Si está activado, se usará TLS para conectarse al servidor. Si está apagado, se usará TLS si su servidor tiene soporte para la extensión STARTTLS. En la mayoría de los casos, puede dejar esta opción activada si se está conectando al puerto 465. Apáguela en el caso de usar los puertos 587 o 25. (de nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Probar dirección", "LabelEmbeddedCover": "Cubierta incrustada", @@ -378,6 +378,7 @@ "LabelFilterByUser": "Filtrar por Usuario", "LabelFindEpisodes": "Buscar Episodio", "LabelFinished": "Terminado", + "LabelFinishedDate": "Finalizado {0}", "LabelFolder": "Carpeta", "LabelFolders": "Carpetas", "LabelFontBold": "Negrilla", @@ -422,6 +423,7 @@ "LabelLanguages": "Idiomas", "LabelLastBookAdded": "Último libro añadido", "LabelLastBookUpdated": "Último libro actualizado", + "LabelLastProgressDate": "Último progreso: {0}", "LabelLastSeen": "Última Vez Visto", "LabelLastTime": "Última Vez", "LabelLastUpdate": "Última Actualización", @@ -434,6 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Sin {0}", "LabelLibraryItem": "Elemento de Biblioteca", "LabelLibraryName": "Nombre de Biblioteca", + "LabelLibrarySortByProgress": "Progreso: Último actualizado", + "LabelLibrarySortByProgressFinished": "Progreso: Finalizado", + "LabelLibrarySortByProgressStarted": "Progreso: Iniciado", "LabelLimit": "Limites", "LabelLineSpacing": "Interlineado", "LabelListenAgain": "Volver a escuchar", @@ -442,6 +447,7 @@ "LabelLogLevelWarn": "Advertencia", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", "LabelLowestPriority": "Menor prioridad", + "LabelMatchConfidence": "Confidencia", "LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por", "LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO", "LabelMaxEpisodesToDownload": "Número máximo # de episodios para descargar. Usa 0 para descargar una cantidad ilimitada.", @@ -460,7 +466,7 @@ "LabelMissingEbook": "No tiene libro electrónico", "LabelMissingSupplementaryEbook": "No tiene libro electrónico suplementario", "LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos", - "LabelMobileRedirectURIsDescription": "Esta es una lista blanca de URI de redireccionamiento válidos para aplicaciones móviles. El predeterminado es audiobookshelf , que puede eliminar o complementar con URI adicionales para la integración de aplicaciones de terceros. Usando un asterisco ( * ) como única entrada que permite cualquier URI.", + "LabelMobileRedirectURIsDescription": "Esta es una lista en blanco de las URI de re‐direccionamiento válidos para aplicaciones móviles. El predeterminado es audiobookshelf , que puede retirar o sustituir con las URI adicionales para la integración de aplicaciones de terceros. Usando un asterisco (* ) como única entrada que permite cualquier URI.", "LabelMore": "Más", "LabelMoreInfo": "Más información", "LabelName": "Nombre", @@ -473,6 +479,7 @@ "LabelNextBackupDate": "Fecha del siguiente respaldo", "LabelNextChapters": "Los próximos capítulos serán:", "LabelNextScheduledRun": "Próxima ejecución programada", + "LabelNoApiKeys": "Sin claves API", "LabelNoCustomMetadataProviders": "Sin proveedores de metadatos personalizados", "LabelNoEpisodesSelected": "Ningún Episodio Seleccionado", "LabelNotFinished": "No terminado", @@ -563,6 +570,7 @@ "LabelSelectAll": "Seleccionar todo", "LabelSelectAllEpisodes": "Seleccionar todos los episodios", "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", + "LabelSelectUser": "Seleccionar usuario", "LabelSelectUsers": "Seleccionar usuarios", "LabelSendEbookToDevice": "Enviar libro electrónico a...", "LabelSequence": "Secuencia", @@ -580,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera", "LabelSettingsChromecastSupport": "Compatibilidad con Chromecast", "LabelSettingsDateFormat": "Formato de Fecha", - "LabelSettingsEnableWatcher": "Buscar cambios automáticamente en las bibliotecas", - "LabelSettingsEnableWatcherForLibrary": "Buscar cambios automáticamente en la biblioteca", + "LabelSettingsEnableWatcher": "Vigilar automáticamente los cambios en bibliotecas", + "LabelSettingsEnableWatcherForLibrary": "Vigilar automáticamente los cambios de biblioteca", "LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requiere reiniciar el servidor", "LabelSettingsEpubsAllowScriptedContent": "Permitir scripts en epubs", "LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que los archivos epub ejecuten scripts. Se recomienda mantener esta opción desactivada a menos que confíe en el origen de los archivos epub.", @@ -614,7 +622,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca", "LabelSettingsTimeFormat": "Formato de Tiempo", "LabelShare": "Compartir", - "LabelShareDownloadableHelp": "Permite a quienes posean el enlace de compartición descargar un archivo ZIP del elemento de la biblioteca.", + "LabelShareDownloadableHelp": "Permite a quienes posean el enlace de compartición descargar un archivo zip del elemento de la biblioteca.", "LabelShareOpen": "abrir un recurso compartido", "LabelShareURL": "Compartir la URL", "LabelShowAll": "Mostrar todo", @@ -630,6 +638,7 @@ "LabelStartTime": "Tiempo de Inicio", "LabelStarted": "Iniciado", "LabelStartedAt": "Iniciado En", + "LabelStartedDate": "Iniciado {0}", "LabelStatsAudioTracks": "Pistas de Audio", "LabelStatsAuthors": "Autores", "LabelStatsBestDay": "Mejor día", @@ -659,6 +668,7 @@ "LabelTheme": "Tema", "LabelThemeDark": "Oscuro", "LabelThemeLight": "Claro", + "LabelThemeSepia": "Sepia", "LabelTimeBase": "Tiempo Base", "LabelTimeDurationXHours": "{0} horas", "LabelTimeDurationXMinutes": "{0} minutos", @@ -727,8 +737,10 @@ "MessageAddToPlayerQueue": "Agregar a fila del Reproductor", "MessageAppriseDescription": "Para usar esta función deberás tener la API de Apprise corriendo o una API que maneje los mismos resultados.
La URL de la API de Apprise debe tener la misma ruta de archivos que donde se envían las notificaciones. Por ejemplo: si su API esta en http://192.168.1.1:8337 entonces pondría http://192.168.1.1:8337/notify.", "MessageAsinCheck": "Cerciórese de usar el ASIN de la región correcta de Audible, no de Amazon.", + "MessageAuthenticationLegacyTokenWarning": "Los vales de API heredados serán retirados en el futuro. Utilice las claves de API en su lugar.", "MessageAuthenticationOIDCChangesRestart": "Reinicie el servidor tras el guardado para aplicar los cambios de OIDC.", - "MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en /metadata/items y /metadata/authors. Los Respaldos NO incluyen ningún archivo guardado en la carpeta de tu biblioteca.", + "MessageAuthenticationSecurityMessage": "La autenticación ha sido mejorada para seguridad. Todos los usuarios requieren reiniciar sesión.", + "MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en /metadata/items y /metadata/authors. Los Respaldos no incluyen ningún archivo guardado en la carpeta de tu biblioteca.", "MessageBackupsLocationEditNote": "Nota: actualizar la ubicación de la copia de respaldo no moverá ni modificará los respaldos existentes", "MessageBackupsLocationNoEditNote": "Nota: la ubicación de la copia de respaldo se establece a través de una variable de entorno y no se puede cambiar aquí.", "MessageBackupsLocationPathEmpty": "La ruta de la copia de seguridad no puede estar vacía", @@ -750,6 +762,7 @@ "MessageChaptersNotFound": "Capítulos no encontrados", "MessageCheckingCron": "Revisando cron...", "MessageConfirmCloseFeed": "¿Confirma que quiere cerrar este suministro?", + "MessageConfirmDeleteApiKey": "¿Está seguro que desea eliminar la clave API «{0}»?", "MessageConfirmDeleteBackup": "¿Confirma que quiere eliminar el respaldo de {0}?", "MessageConfirmDeleteDevice": "¿Confirma que quiere eliminar el lector electrónico «{0}»?", "MessageConfirmDeleteFile": "Esto eliminará el archivo del sistema de archivos. ¿Quiere continuar?", @@ -768,8 +781,8 @@ "MessageConfirmMarkSeriesFinished": "¿Confirma que quiere marcar todos los libros de esta serie como terminados?", "MessageConfirmMarkSeriesNotFinished": "¿Confirma que quiere marcar todos los libros de esta serie como no terminados?", "MessageConfirmNotificationTestTrigger": "¿Activar esta notificación con datos de prueba?", - "MessageConfirmPurgeCache": "Purgar la antememoria eliminará el directorio completo ubicado en /metadata/cache.

¿Confirma que quiere eliminar el directorio de antememoria?", - "MessageConfirmPurgeItemsCache": "Purgar la antememoria de elementos eliminará el directorio completo ubicado en /metadata/cache/items.
¿Lo confirma?", + "MessageConfirmPurgeCache": "La purga del caché eliminará el directorio completo en /metadata/cache.

¿Confirma que desea quitar el directorio de caché?", + "MessageConfirmPurgeItemsCache": "Purgar el caché de elementos eliminará el directorio completo ubicado en /metadata/cache/items.
¿Lo confirma?", "MessageConfirmQuickEmbed": "Atención: la incrustación rápida no realiza copias de respaldo a ninguno de sus archivos de audio. Cerciórese de haber realizado una copia de los mismos previamente.

¿Quiere continuar?", "MessageConfirmQuickMatchEpisodes": "El reconocimiento rápido de extensiones sobrescribirá los detalles si se encuentra una coincidencia. Se actualizarán las extensiones no reconocidas. ¿Quiere continuar?", "MessageConfirmReScanLibraryItems": "¿Confirma que quiere volver a analizar {0} elementos?", @@ -795,14 +808,16 @@ "MessageDaysListenedInTheLastYear": "{0} días escuchados el año pasado", "MessageDownloadingEpisode": "Descargando episodio", "MessageDragFilesIntoTrackOrder": "Arrastre los archivos al orden correcto de las pistas", - "MessageEmbedFailed": "Falló la incrustación.", - "MessageEmbedFinished": "Finalizó la incrustación.", + "MessageEmbedFailed": "Incorporación incorrecta.", + "MessageEmbedFinished": "Incorporación finalizada.", "MessageEmbedQueue": "En cola para incrustar metadatos ({0} en cola)", "MessageEpisodesQueuedForDownload": "{0} episodio(s) en cola para descargar", "MessageEreaderDevices": "Para garantizar la entrega de libros electrónicos, es posible que tenga que agregar la dirección de correo electrónico anterior como remitente válido para cada dispositivo enumerado a continuación.", "MessageFeedURLWillBe": "El URL del suministro será {0}", "MessageFetching": "Recuperando...", "MessageForceReScanDescription": "Escaneará todos los archivos como un nuevo escaneo. Archivos de audio con etiquetas ID3, archivos OPF y archivos de texto serán escaneados como nuevos.", + "MessageHeatmapListeningTimeTooltip": "{0} escuchando en {1}", + "MessageHeatmapNoListeningSessions": "No enumera sesiones en {0}", "MessageImportantNotice": "¡Notificación importante!", "MessageInsertChapterBelow": "Insertar capítulo debajo", "MessageInvalidAsin": "ASIN no válido", @@ -873,7 +888,7 @@ "MessageResetChaptersConfirm": "¿Confirma que quiere deshacer los cambios y restablecer los capítulos a su estado original?", "MessageRestoreBackupConfirm": "¿Confirma que quiere restaurar el respaldo creado el", "MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.

El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.

Todos los clientes que usen su servidor se actualizarán automáticamente.", - "MessageScheduleLibraryScanNote": "Para la mayoría de los usuarios, se recomienda dejar esta función desactivada y mantener activada la configuración del observador de carpetas. El observador de carpetas detectará automáticamente los cambios en las carpetas de la biblioteca. El observador de carpetas no funciona para todos los sistemas de archivos (como NFS), por lo que se pueden utilizar exploraciones programadas de la biblioteca en su lugar.", + "MessageScheduleLibraryScanNote": "Para muchos usuarios, es recomendado dejar esta característica inhabilitada y mantener habilitados los ajustes de la «Vigía automática de cambio de biblioteca»: detectará automáticamente los cambios en sus carpetas de bibliotecas. Habilitar esta características si «Vigía automática de cambio de biblioteca» no funciona en su sistema de archivo (como NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Ejecutar cada {0} a las {1}", "MessageSearchResultsFor": "Resultados de la búsqueda de", "MessageSelected": "{0} seleccionado(s)", @@ -999,6 +1014,8 @@ "ToastBulkChapterInvalidCount": "Por favor ingrese un número válido entre 1 y 150", "ToastCachePurgeFailed": "No se pudo purgar la antememoria", "ToastCachePurgeSuccess": "Se purgó la antememoria correctamente", + "ToastChapterLocked": "El capítulo está bloqueado.", + "ToastChapterStartTimeAdjusted": "El capítulo inicia el tiempo ajustado en {0} segundos", "ToastChaptersAllLocked": "Todos los capítulos están bloqueados. Desbloquee algunos capítulos para cambiar sus tiempos.", "ToastChaptersHaveErrors": "Los capítulos tienen errores", "ToastChaptersInvalidShiftAmountLast": "Cantidad de desplazamiento no válida. La hora de inicio del último capítulo se extendería más allá de la duración de este audiolibro.", @@ -1009,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Artículo(s) añadido(s) a la colección fallido(s)", "ToastCollectionRemoveSuccess": "Colección quitada", "ToastCollectionUpdateSuccess": "Colección actualizada", + "ToastConnectionNotAvailable": "Conexión no disponible. Intenta de nuevo más tarde", + "ToastCoverSearchFailed": "Cobertura de búsqueda incorrecta", "ToastCoverUpdateFailed": "Error al actualizar la cubierta", "ToastDateTimeInvalidOrIncomplete": "Fecha y hora no válidas o incompletas", "ToastDeleteFileFailed": "Falló la eliminación del archivo", @@ -1024,6 +1043,8 @@ "ToastEpisodeDownloadQueueClearSuccess": "Se borró la cola de descargas de los episodios", "ToastEpisodeUpdateSuccess": "{0} episodio(s) actualizado(s)", "ToastErrorCannotShare": "No se puede compartir de forma nativa en este dispositivo", + "ToastFailedToCreate": "Ha fallado al crear", + "ToastFailedToDelete": "Ha fallado al eliminar", "ToastFailedToLoadData": "Error al cargar data", "ToastFailedToMatch": "Error al emparejar", "ToastFailedToShare": "Error al compartir", @@ -1031,6 +1052,7 @@ "ToastInvalidImageUrl": "URL de la imagen no válida", "ToastInvalidMaxEpisodesToDownload": "Número máximo de episodios para descargar no válidos", "ToastInvalidUrl": "URL no válida", + "ToastInvalidUrls": "Una o más URL son inválidas", "ToastItemCoverUpdateSuccess": "Cubierta del elemento actualizada", "ToastItemDeletedFailed": "Error al eliminar el elemento", "ToastItemDeletedSuccess": "Elemento borrado", @@ -1055,6 +1077,7 @@ "ToastMustHaveAtLeastOnePath": "Debe tener al menos una ruta", "ToastNameEmailRequired": "Son obligatorios el nombre y el correo electrónico", "ToastNameRequired": "Nombre obligatorio", + "ToastNewApiKeyUserError": "Debe seleccionar un usuario", "ToastNewEpisodesFound": "{0} nuevo(s) episodio(s) encontrado(s)", "ToastNewUserCreatedFailed": "No se pudo crear la cuenta: «{0}»", "ToastNewUserCreatedSuccess": "Nueva cuenta creada", @@ -1133,6 +1156,7 @@ "ToastUserRootRequireName": "Debe introducir un nombre de usuario administrativo", "TooltipAddChapters": "Añadir capítulo(s)", "TooltipAddOneSecond": "Añadir 1 segundo", + "TooltipAdjustChapterStart": "Pulse para ajustar la hora de inicio", "TooltipLockAllChapters": "Bloquear todos los capítulos", "TooltipLockChapter": "Bloquear capítulo (Mayús+clic para rango)", "TooltipSubtractOneSecond": "Restar 1 segundo", From e781ff5eaeaace14985573a6c757f9dc3831e30d Mon Sep 17 00:00:00 2001 From: Francisco Serrador Date: Wed, 18 Mar 2026 22:51:19 +0100 Subject: [PATCH 207/285] Translated using Weblate (Spanish) Currently translated at 99.7% (1160 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 80 +++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index 4569f04bd..9e64107e2 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -140,49 +140,49 @@ "HeaderDetails": "Detalles", "HeaderDownloadQueue": "Cola de descargas", "HeaderEbookFiles": "Archivos de libros digitales", - "HeaderEmail": "Correo electrónico", - "HeaderEmailSettings": "Configuración de correo electrónico", + "HeaderEmail": "Correo-e", + "HeaderEmailSettings": "Ajustes de correo-e", "HeaderEpisodes": "Episodios", - "HeaderEreaderDevices": "Dispositivos Ereader", - "HeaderEreaderSettings": "Ajustes del lector", + "HeaderEreaderDevices": "Dispositivos Lector-e", + "HeaderEreaderSettings": "Ajustes del Lector-e", "HeaderFiles": "Archivos", "HeaderFindChapters": "Buscar capítulos", "HeaderIgnoredFiles": "Archivos ignorados", - "HeaderItemFiles": "Archivos de elementos", - "HeaderItemMetadataUtils": "Utilidades de metadatos de elementos", + "HeaderItemFiles": "Archivos del elemento", + "HeaderItemMetadataUtils": "Utilidades de metadatos del elemento", "HeaderLastListeningSession": "Última sesión de escucha", "HeaderLatestEpisodes": "Episodios más recientes", "HeaderLibraries": "Bibliotecas", "HeaderLibraryFiles": "Archivos de biblioteca", "HeaderLibraryStats": "Estadísticas de biblioteca", - "HeaderListeningSessions": "Sesión", + "HeaderListeningSessions": "Sesiones Listadas", "HeaderListeningStats": "Estadísticas de Tiempo Escuchado", - "HeaderLogin": "Acceder", - "HeaderLogs": "Registros", + "HeaderLogin": "Inicio de Sesión", + "HeaderLogs": "Bitácoras", "HeaderManageGenres": "Gestionar géneros", "HeaderManageTags": "Gestionar etiquetas", "HeaderMapDetails": "Asignar Detalles", - "HeaderMatch": "Encontrar", + "HeaderMatch": "Coincidir", "HeaderMetadataOrderOfPrecedence": "Orden de precedencia de metadatos", - "HeaderMetadataToEmbed": "Metadatos para Insertar", - "HeaderNewAccount": "Cuenta nueva", + "HeaderMetadataToEmbed": "Metadatos para empotrar", + "HeaderNewAccount": "Crear Cuenta", "HeaderNewApiKey": "Nueva clave API", "HeaderNewLibrary": "Biblioteca nueva", - "HeaderNotificationCreate": "Crear notificación", - "HeaderNotificationUpdate": "Notificación de actualización", + "HeaderNotificationCreate": "Crear Notificación", + "HeaderNotificationUpdate": "Notificación de Actualización", "HeaderNotifications": "Notificaciones", "HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect", - "HeaderOpenListeningSessions": "Sesiones públicas de escucha", + "HeaderOpenListeningSessions": "Abrir escucha de sesiones", "HeaderOpenRSSFeed": "Abrir suministro RSS", "HeaderOtherFiles": "Otros archivos", "HeaderPasswordAuthentication": "Autenticación por contraseña", "HeaderPermissions": "Permisos", "HeaderPlayerQueue": "Cola del reproductor", - "HeaderPlayerSettings": "Configuración del reproductor", + "HeaderPlayerSettings": "Ajustes del reproductor", "HeaderPlaylist": "Lista de reproducción", "HeaderPlaylistItems": "Elementos de lista de reproducción", "HeaderPodcastsToAdd": "Pódcast para añadir", - "HeaderPresets": "Preconfiguraciones", + "HeaderPresets": "Preajustes", "HeaderPreviewCover": "Previsualizar cubierta", "HeaderRSSFeedGeneral": "Detalles de RSS", "HeaderRSSFeedIsOpen": "El suministro RSS está abierto", @@ -191,13 +191,13 @@ "HeaderRemoveEpisodes": "Quitar {0} episodios", "HeaderSavedMediaProgress": "Guardar Progreso de Multimedia", "HeaderSchedule": "Horario", - "HeaderScheduleEpisodeDownloads": "Programar descargas automáticas de episodios", - "HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca", + "HeaderScheduleEpisodeDownloads": "Planificador de auto‐descargas de episodios", + "HeaderScheduleLibraryScans": "Planificar Auto‐Escaneo de Biblioteca", "HeaderSession": "Sesión", - "HeaderSetBackupSchedule": "Programar Respaldo", + "HeaderSetBackupSchedule": "Establecer Planificación de Respaldo", "HeaderSettings": "Ajustes", "HeaderSettingsDisplay": "Interfaz", - "HeaderSettingsExperimental": "Funcionalidades experimentales", + "HeaderSettingsExperimental": "Características experimentales", "HeaderSettingsGeneral": "Generales", "HeaderSettingsScanner": "Escáner", "HeaderSettingsSecurity": "Seguridad", @@ -241,21 +241,21 @@ "LabelAllUsers": "Todos los usuarios", "LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados", "LabelAllUsersIncludingGuests": "Todos los usuarios e invitados", - "LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca", - "LabelApiKeyCreated": "La clave de API “{0}” se ha creado con éxito.", + "LabelAlreadyInYourLibrary": "Ya dentro de tu biblioteca", + "LabelApiKeyCreated": "La clave de API “{0}” se ha creado correctamente.", "LabelApiKeyCreatedDescription": "Asegúrate de copiar la clave de API ahora, no la volverás a ver otra vez.", "LabelApiKeyUser": "Actuar en nombre del usuario", "LabelApiKeyUserDescription": "Esta clave de API tendrá los mismos permisos que el usuario al que representa. En los registros se verá como si la solicitud la hubiera hecho el usuario directamente.", - "LabelApiToken": "Token de la API", + "LabelApiToken": "Vale del API", "LabelAppend": "Adjuntar", - "LabelAudioBitrate": "Tasa de bits del audio (por ejemplo, 128k)", + "LabelAudioBitrate": "Tasa de bit del audio (p.ej., 128k)", "LabelAudioChannels": "Canales de audio (1 o 2)", "LabelAudioCodec": "Códec de audio", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (Nombre Apellido)", "LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthors": "Autores", - "LabelAutoDownloadEpisodes": "Auto-Descargar episodios", + "LabelAutoDownloadEpisodes": "Auto‐Descargar episodios", "LabelAutoFetchMetadata": "Recuperar metadatos automáticamente", "LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.", "LabelAutoLaunch": "Lanzamiento automático", @@ -325,8 +325,8 @@ "LabelDurationComparisonLonger": "({0} más largo)", "LabelDurationComparisonShorter": "({0} más corto)", "LabelDurationFound": "Duración Comprobada:", - "LabelEbook": "Libro electrónico", - "LabelEbooks": "Libros electrónicos", + "LabelEbook": "Libro-e", + "LabelEbooks": "Libros-e", "LabelEdit": "Editar", "LabelEmail": "Correo electrónico", "LabelEmailSettingsFromAddress": "Remitente", @@ -377,12 +377,12 @@ "LabelFilename": "Nombre del archivo", "LabelFilterByUser": "Filtrar por Usuario", "LabelFindEpisodes": "Buscar Episodio", - "LabelFinished": "Terminado", + "LabelFinished": "Finalizado", "LabelFinishedDate": "Finalizado {0}", "LabelFolder": "Carpeta", "LabelFolders": "Carpetas", "LabelFontBold": "Negrilla", - "LabelFontBoldness": "Peso tipográfico", + "LabelFontBoldness": "Tipográfico sin Negrita", "LabelFontFamily": "Familia tipográfica", "LabelFontItalic": "Itálica", "LabelFontScale": "Escala de letra", @@ -392,8 +392,8 @@ "LabelGenre": "Género", "LabelGenres": "Géneros", "LabelHardDeleteFile": "Eliminar Definitivamente", - "LabelHasEbook": "Tiene un libro", - "LabelHasSupplementaryEbook": "Tiene un libro complementario", + "LabelHasEbook": "Tiene libro-e", + "LabelHasSupplementaryEbook": "Tiene un libro-e suplementario", "LabelHideSubtitles": "Ocultar subtítulos", "LabelHighestPriority": "Mayor prioridad", "LabelHost": "Anfitrión", @@ -482,7 +482,7 @@ "LabelNoApiKeys": "Sin claves API", "LabelNoCustomMetadataProviders": "Sin proveedores de metadatos personalizados", "LabelNoEpisodesSelected": "Ningún Episodio Seleccionado", - "LabelNotFinished": "No terminado", + "LabelNotFinished": "No finalizado", "LabelNotStarted": "Sin iniciar", "LabelNotes": "Notas", "LabelNotificationAppriseURL": "URL(s) de Apprise", @@ -496,7 +496,7 @@ "LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignoradas si llegan al numero máximo de cola para prevenir spam de eventos.", "LabelNumberOfBooks": "Número de libros", "LabelNumberOfChapters": "Número de capítulos:", - "LabelNumberOfEpisodes": "N.º de episodios", + "LabelNumberOfEpisodes": "Nº de episodios", "LabelOpenIDAdvancedPermsClaimDescription": "Nombre de la notificación de OpenID que contiene permisos avanzados para acciones de usuario dentro de la aplicación que se aplicarán a roles que no sean de administrador (si están configurados). Si el reclamo no aparece en la respuesta, se denegará el acceso a ABS. Si falta una sola opción, se tratará como falsa. Asegúrese de que la notificación del proveedor de identidades coincida con la estructura esperada:", "LabelOpenIDClaims": "Deje las siguientes opciones vacías para desactivar la asignación avanzada de grupos y permisos, lo que asignaría de manera automática al grupo «Usuario».", "LabelOpenIDGroupClaimDescription": "Nombre de la declaración OpenID que contiene una lista de grupos del usuario. Comúnmente conocidos como grupos. Si se configura, la aplicación asignará automáticamente roles en función de la pertenencia a grupos del usuario, siempre que estos grupos se denominen \"admin\", \"user\" o \"guest\" en la notificación. La solicitud debe contener una lista, y si un usuario pertenece a varios grupos, la aplicación asignará el rol correspondiente al mayor nivel de acceso. Si ningún grupo coincide, se denegará el acceso.", @@ -526,7 +526,7 @@ "LabelPodcasts": "Pódcast", "LabelPort": "Puerto", "LabelPrefixesToIgnore": "Prefijos para ignorar (no distingue entre mayúsculas y minúsculas)", - "LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indicen su suministro", + "LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indexen su suministro", "LabelPrimaryEbook": "Libro electrónico principal", "LabelProgress": "Progreso", "LabelProvider": "Proveedor", @@ -538,11 +538,11 @@ "LabelPublishedDecades": "Décadas publicadas", "LabelPublisher": "Editor", "LabelPublishers": "Editores", - "LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", - "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", + "LabelRSSFeedCustomOwnerEmail": "Correo-e de propietario personalizado", + "LabelRSSFeedCustomOwnerName": "Nombre de propietario personalizado", "LabelRSSFeedOpen": "Fuente RSS Abierta", "LabelRSSFeedPreventIndexing": "Evitar indización", - "LabelRSSFeedSlug": "«Slug» de suministro RSS", + "LabelRSSFeedSlug": "Ficha de suministro RSS", "LabelRSSFeedURL": "URL de suministro RSS", "LabelRandomly": "Aleatorio", "LabelReAddSeriesToContinueListening": "Volver a agregar la serie para continuar escuchándola", @@ -575,7 +575,7 @@ "LabelSendEbookToDevice": "Enviar libro electrónico a...", "LabelSequence": "Secuencia", "LabelSerial": "En serie", - "LabelSeries": "Serie", + "LabelSeries": "Series", "LabelSeriesName": "Nombre de la serie", "LabelSeriesProgress": "Progreso de la serie", "LabelServerLogLevel": "Nivel de registro del servidor", @@ -629,7 +629,7 @@ "LabelShowSeconds": "Mostrar segundos", "LabelShowSubtitles": "Mostrar subtítulos", "LabelSize": "Tamaño", - "LabelSleepTimer": "Temporizador de apagado", + "LabelSleepTimer": "Temporizador de dormida", "LabelSlug": "Slug", "LabelSortAscending": "Ascendente", "LabelSortDescending": "Descendente", From 5f8db24b961bd59a3069d9c86140eafa7cf65d03 Mon Sep 17 00:00:00 2001 From: Francisco Serrador Date: Wed, 18 Mar 2026 22:52:38 +0100 Subject: [PATCH 208/285] Translated using Weblate (Spanish) 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/es/ --- client/strings/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/es.json b/client/strings/es.json index 9e64107e2..c3b14742e 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -65,7 +65,7 @@ "ButtonPlaylists": "Listas de reproducción", "ButtonPrevious": "Anterior", "ButtonPreviousChapter": "Capítulo anterior", - "ButtonProbeAudioFile": "Examinar archivo de audio", + "ButtonProbeAudioFile": "Sonda del archivo de audio", "ButtonPurgeAllCache": "Purgar toda la caché", "ButtonPurgeItemsCache": "Purgar caché de elementos", "ButtonQueueAddItem": "Añadir a cola", From 9634c46bc593074f0a38e2daefafa044fc0a19b0 Mon Sep 17 00:00:00 2001 From: Francisco Serrador Date: Wed, 18 Mar 2026 22:56:54 +0100 Subject: [PATCH 209/285] Translated using Weblate (Spanish) 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/es/ --- client/strings/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/es.json b/client/strings/es.json index c3b14742e..eae32bd9f 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -212,7 +212,7 @@ "HeaderTableOfContents": "Sumario", "HeaderTools": "Herramientas", "HeaderUpdateAccount": "Actualizar cuenta", - "HeaderUpdateApiKey": "Actualizar Clave API", + "HeaderUpdateApiKey": "Actualizar clave API", "HeaderUpdateAuthor": "Actualizar autor", "HeaderUpdateDetails": "Actualizar detalles", "HeaderUpdateLibrary": "Actualizar biblioteca", From 9821c31f8eb49aea9d6507c9381066e56a59d40a Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 19 Mar 2026 16:53:21 -0500 Subject: [PATCH 210/285] Update collection create/update endpoints to strip html tags from collection name --- .../modals/collections/AddCreateModal.vue | 2 +- server/controllers/CollectionController.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/client/components/modals/collections/AddCreateModal.vue b/client/components/modals/collections/AddCreateModal.vue index 24e8695d6..f0d43c14f 100644 --- a/client/components/modals/collections/AddCreateModal.vue +++ b/client/components/modals/collections/AddCreateModal.vue @@ -227,7 +227,7 @@ export default { .catch((error) => { console.error('Failed to create collection', error) var errMsg = error.response ? error.response.data || '' : '' - this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg) + this.$toast.error(errMsg) this.processing = false }) } diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 475adfe0f..1476b0f81 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -3,6 +3,7 @@ const Sequelize = require('sequelize') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +const htmlSanitizer = require('../utils/htmlSanitizer') const RssFeedManager = require('../managers/RssFeedManager') @@ -31,8 +32,10 @@ class CollectionController { async create(req, res) { const reqBody = req.body || {} + const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name) + // Validation - if (!reqBody.name || !reqBody.libraryId) { + if (!nameCleaned || !reqBody.libraryId) { return res.status(400).send('Invalid collection data') } if (reqBody.description && typeof reqBody.description !== 'string') { @@ -65,7 +68,7 @@ class CollectionController { newCollection = await Database.collectionModel.create( { libraryId: reqBody.libraryId, - name: reqBody.name, + name: nameCleaned, description: reqBody.description || null }, { transaction } @@ -145,9 +148,12 @@ class CollectionController { collectionUpdatePayload.description = req.body.description wasUpdated = true } - if (req.body.name !== undefined && req.body.name !== req.collection.name) { - collectionUpdatePayload.name = req.body.name - wasUpdated = true + if (req.body.name !== undefined && typeof req.body.name === 'string') { + const nameCleaned = htmlSanitizer.stripAllTags(req.body.name) + if (nameCleaned !== req.collection.name) { + collectionUpdatePayload.name = nameCleaned + wasUpdated = true + } } if (wasUpdated) { From 3faa6f3e7d1ad52101a9355fa07efd26f13ea60d Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 19 Mar 2026 16:57:22 -0500 Subject: [PATCH 211/285] Update playlist create/update endpoint to strip all html tags --- server/controllers/PlaylistController.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 972c352a4..bc1a7a455 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -2,6 +2,7 @@ const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +const htmlSanitizer = require('../utils/htmlSanitizer') /** * @typedef RequestUserObject @@ -29,7 +30,8 @@ class PlaylistController { const reqBody = req.body || {} // Validation - if (!reqBody.name || !reqBody.libraryId) { + const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name) + if (!nameCleaned || !reqBody.libraryId) { return res.status(400).send('Invalid playlist data') } if (reqBody.description && typeof reqBody.description !== 'string') { @@ -84,7 +86,7 @@ class PlaylistController { { libraryId: reqBody.libraryId, userId: req.user.id, - name: reqBody.name, + name: nameCleaned, description: reqBody.description || null }, { transaction } @@ -174,7 +176,11 @@ class PlaylistController { } const playlistUpdatePayload = {} - if (reqBody.name) playlistUpdatePayload.name = reqBody.name + + const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name) + if (nameCleaned) { + playlistUpdatePayload.name = nameCleaned + } if (reqBody.description) playlistUpdatePayload.description = reqBody.description // Update name and description From 8b89b276544be16bdc353f793604d6d8a066463c Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 19 Mar 2026 17:09:14 -0500 Subject: [PATCH 212/285] Version bump v2.33.1 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 299741bd5..71bcf26a8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.33.0", + "version": "2.33.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.33.0", + "version": "2.33.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index a1503a506..782f411bd 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.33.0", + "version": "2.33.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index e07fba51d..d8cc43bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.33.0", + "version": "2.33.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.33.0", + "version": "2.33.1", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 3108b5170..d44df1165 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.33.0", + "version": "2.33.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 5de92d08f9018ed6e0b3af686f00c54b43d0fa27 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 29 Mar 2026 15:36:07 -0500 Subject: [PATCH 213/285] Fix share playback session not including coverAspectRatio --- server/objects/PlaybackSession.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js index ba031b665..ace0256e8 100644 --- a/server/objects/PlaybackSession.js +++ b/server/objects/PlaybackSession.js @@ -110,7 +110,8 @@ class PlaybackSession { startedAt: this.startedAt, updatedAt: this.updatedAt, audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }), - libraryItem: libraryItem?.toOldJSONExpanded() || null + libraryItem: libraryItem?.toOldJSONExpanded() || null, + coverAspectRatio: this.coverAspectRatio !== null ? this.coverAspectRatio : undefined // Used for share sessions } } From 093124aac694fbcb18184f84ecbb8dc8aaca7554 Mon Sep 17 00:00:00 2001 From: mikiher Date: Mon, 30 Mar 2026 22:02:56 +0300 Subject: [PATCH 214/285] Emit proper author_updated/added events when updating book media --- server/models/Author.js | 5 +++-- server/models/Book.js | 13 ++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/server/models/Author.js b/server/models/Author.js index 287b66976..d83eef15f 100644 --- a/server/models/Author.js +++ b/server/models/Author.js @@ -115,12 +115,13 @@ class Author extends Model { */ static async findOrCreateByNameAndLibrary(name, libraryId) { const author = await this.getByNameAndLibrary(name, libraryId) - if (author) return author - return this.create({ + if (author) return { author, created: false } + const newAuthor = await this.create({ name, lastFirst: this.getLastFirst(name), libraryId }) + return { author: newAuthor, created: true } } /** diff --git a/server/models/Book.js b/server/models/Book.js index 96371f3a2..d9f2ff132 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -4,6 +4,7 @@ const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') const parseNameString = require('../utils/parsers/parseNameString') const htmlSanitizer = require('../utils/htmlSanitizer') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') +const SocketAuthority = require('../SocketAuthority') /** * @typedef EBookFileObject @@ -470,13 +471,23 @@ class Book extends Model { for (const author of authorsRemoved) { await bookAuthorModel.removeByIds(author.id, this.id) + const numBooks = await bookAuthorModel.getCountForAuthor(author.id) + if (numBooks > 0) { + SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks)) + } Logger.debug(`[Book] "${this.title}" Removed author "${author.name}"`) this.authors = this.authors.filter((au) => au.id !== author.id) } const authorsAdded = [] for (const authorName of newAuthorNames) { - const author = await authorModel.findOrCreateByNameAndLibrary(authorName, libraryId) + const { author, created } = await authorModel.findOrCreateByNameAndLibrary(authorName, libraryId) await bookAuthorModel.create({ bookId: this.id, authorId: author.id }) + if (created) { + SocketAuthority.emitter('author_added', author.toOldJSON()) + } else { + const numBooks = await bookAuthorModel.getCountForAuthor(author.id) + SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks)) + } Logger.debug(`[Book] "${this.title}" Added author "${author.name}"`) this.authors.push(author) authorsAdded.push(author) From ab3bd6f4a170d7ddf6e50dbeb4e5794f1a011e41 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 30 Mar 2026 16:22:27 -0500 Subject: [PATCH 215/285] Update JS docs --- server/models/Author.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/models/Author.js b/server/models/Author.js index d83eef15f..65561e211 100644 --- a/server/models/Author.js +++ b/server/models/Author.js @@ -111,7 +111,7 @@ class Author extends Model { * * @param {string} name * @param {string} libraryId - * @returns {Promise} + * @returns {Promise<{ author: Author, created: boolean }>} */ static async findOrCreateByNameAndLibrary(name, libraryId) { const author = await this.getByNameAndLibrary(name, libraryId) From fda1a6ea9bac122b3b84e742f244e2c6eb6c660b Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 31 Mar 2026 22:02:52 +0300 Subject: [PATCH 216/285] Fix item_removed payload to include libraryId --- server/controllers/LibraryController.js | 6 +++--- server/controllers/LibraryItemController.js | 4 ++-- server/routers/ApiRouter.js | 6 ++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 55ef45690..be9d03328 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -462,7 +462,7 @@ class LibraryController { } } Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`) - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id) } if (authorIds.length) { @@ -563,7 +563,7 @@ class LibraryController { mediaItemIds.push(libraryItem.mediaId) } Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`) - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id) } // Set PlaybackSessions libraryId to null @@ -714,7 +714,7 @@ class LibraryController { } } Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" with issue`) - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id) } if (authorIds.length) { diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 5247dbb06..5f7bd9736 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -111,7 +111,7 @@ class LibraryItemController { } } - await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds, req.libraryItem.libraryId) if (hardDelete) { Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) await fs.remove(libraryItemPath).catch((error) => { @@ -565,7 +565,7 @@ class LibraryItemController { authorIds.push(...libraryItem.media.authors.map((au) => au.id)) } } - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, libraryItem.libraryId) if (hardDelete) { Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) await fs.remove(libraryItemPath).catch((error) => { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index db04bf5ec..e89b364f3 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -363,8 +363,9 @@ class ApiRouter { * Remove library item and associated entities * @param {string} libraryItemId * @param {string[]} mediaItemIds array of bookId or podcastEpisodeId + * @param {string} libraryId */ - async handleDeleteLibraryItem(libraryItemId, mediaItemIds) { + async handleDeleteLibraryItem(libraryItemId, mediaItemIds, libraryId) { const numProgressRemoved = await Database.mediaProgressModel.destroy({ where: { mediaItemId: mediaItemIds @@ -395,7 +396,8 @@ class ApiRouter { await Database.libraryItemModel.removeById(libraryItemId) SocketAuthority.emitter('item_removed', { - id: libraryItemId + id: libraryItemId, + libraryId }) } From 5a6b3d8e6158730cd7dede97234884d9137c1285 Mon Sep 17 00:00:00 2001 From: "peter.kottke" Date: Wed, 1 Apr 2026 21:05:48 -0400 Subject: [PATCH 217/285] updates to allow share t argument to over-ride server stored position --- client/pages/share/_slug.vue | 4 +++- server/controllers/ShareController.js | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/pages/share/_slug.vue b/client/pages/share/_slug.vue index 64c099632..15748b48f 100644 --- a/client/pages/share/_slug.vue +++ b/client/pages/share/_slug.vue @@ -363,7 +363,9 @@ export default { console.log('Loaded media item share', this.mediaItemShare) } - const startTime = this.playbackSession.currentTime || 0 + const startTime = this.$route.query.t && !isNaN(this.$route.query.t) + ? parseFloat(this.$route.query.t) + : (this.playbackSession.currentTime || 0) this.localAudioPlayer.set(null, this.audioTracks, false, startTime, false) this.localAudioPlayer.on('stateChange', this.playerStateChange.bind(this)) this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this)) diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 3e7ea1deb..f7dd36f83 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -20,7 +20,7 @@ const ShareManager = require('../managers/ShareManager') */ class ShareController { - constructor() {} + constructor() { } /** * Public route @@ -53,6 +53,10 @@ class ShareController { if (playbackSession) { if (mediaItemShare.id === playbackSession.mediaItemShareId) { Logger.debug(`[ShareController] Found share playback session ${req.cookies.share_session_id}`) + // If ?t was provided, override the cached currentTime + if (startTime > 0) { + playbackSession.currentTime = startTime + } mediaItemShare.playbackSession = playbackSession.toJSONForClient() return res.json(mediaItemShare) } else { From 522b9735e22dabc111eb47cb5d5e814bc1c1c351 Mon Sep 17 00:00:00 2001 From: Oliver Marriott Date: Thu, 9 Apr 2026 20:49:48 +1000 Subject: [PATCH 218/285] Add `audio/(x-)matroska` to client player MIME types to avoid transcode Firefox, at least, supports playing `matroska/audio` containers natively but the client was not checking for support. Clients that do not support playing `matroska/audio` containers will fallback to transcoding. --- client/players/LocalAudioPlayer.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/players/LocalAudioPlayer.js b/client/players/LocalAudioPlayer.js index 7fc17e7aa..377818089 100644 --- a/client/players/LocalAudioPlayer.js +++ b/client/players/LocalAudioPlayer.js @@ -46,7 +46,14 @@ export default class LocalAudioPlayer extends EventEmitter { this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this)) this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this)) - var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm'] + var mimeTypes = [ + 'audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', + 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm', + // `audio/matroska` is the correct mimetype, but at least as of 2026-04-09, + // the detected mimetype for matroska files by the server is `audio/x-matroska`. + // ref: https://www.iana.org/assignments/media-types/media-types.xhtml + 'audio/matroska', 'audio/x-matroska' + ] var mimeTypeCanPlayMap = {} mimeTypes.forEach((mt) => { var canPlay = this.player.canPlayType(mt) From 94c426bd971a40771e8a3503af3eafd01cc89c73 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 10 Apr 2026 16:42:39 -0500 Subject: [PATCH 219/285] Update comments on matroska --- client/players/LocalAudioPlayer.js | 16 +++++++++++----- server/utils/constants.js | 2 ++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/client/players/LocalAudioPlayer.js b/client/players/LocalAudioPlayer.js index 377818089..a0384d54d 100644 --- a/client/players/LocalAudioPlayer.js +++ b/client/players/LocalAudioPlayer.js @@ -47,12 +47,18 @@ export default class LocalAudioPlayer extends EventEmitter { this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this)) var mimeTypes = [ - 'audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', - 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm', - // `audio/matroska` is the correct mimetype, but at least as of 2026-04-09, - // the detected mimetype for matroska files by the server is `audio/x-matroska`. + 'audio/flac', + 'audio/mpeg', + 'audio/mp4', + 'audio/ogg', + 'audio/aac', + 'audio/x-ms-wma', + 'audio/x-aiff', + 'audio/webm', + // `audio/matroska` is the correct mimetype, but the server still uses `audio/x-matroska` // ref: https://www.iana.org/assignments/media-types/media-types.xhtml - 'audio/matroska', 'audio/x-matroska' + 'audio/matroska', + 'audio/x-matroska' ] var mimeTypeCanPlayMap = {} mimeTypes.forEach((mt) => { diff --git a/server/utils/constants.js b/server/utils/constants.js index cc5217f41..925035e17 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -48,6 +48,8 @@ module.exports.AudioMimeType = { AIF: 'audio/x-aiff', WEBM: 'audio/webm', WEBMA: 'audio/webm', + // TODO: Switch to `audio/matroska`? marked as deprecated in IANA registry + // ref: https://datatracker.ietf.org/doc/html/rfc9559 MKA: 'audio/x-matroska', AWB: 'audio/amr-wb', CAF: 'audio/x-caf', From 455e6051624316856c8ccaf8bcdb83bd289e2112 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 17 Apr 2026 16:30:08 -0500 Subject: [PATCH 220/285] Update author & library item image endpoints to clamp width/height query params --- server/controllers/AuthorController.js | 6 +++--- server/controllers/LibraryItemController.js | 6 +++--- server/utils/index.js | 10 ++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 82ed3e50a..80471ec47 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -10,7 +10,7 @@ const CacheManager = require('../managers/CacheManager') const CoverManager = require('../managers/CoverManager') const AuthorFinder = require('../finders/AuthorFinder') -const { reqSupportsWebp, isValidASIN } = require('../utils/index') +const { reqSupportsWebp, isValidASIN, clampPositiveInt } = require('../utils/index') const naturalSort = createNewSortInstance({ comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare @@ -412,8 +412,8 @@ class AuthorController { const options = { format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'), - height: height ? parseInt(height) : null, - width: width ? parseInt(width) : null + height: clampPositiveInt(height ? parseInt(height) : null, 4096), + width: clampPositiveInt(width ? parseInt(width) : null, 4096) } return CacheManager.handleAuthorCache(res, authorId, options) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 5f7bd9736..1a6b8ac11 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -7,7 +7,7 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const zipHelpers = require('../utils/zipHelpers') -const { reqSupportsWebp } = require('../utils/index') +const { reqSupportsWebp, clampPositiveInt } = require('../utils/index') const { ScanResult, AudioMimeType } = require('../utils/constants') const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils') const LibraryItemScanner = require('../scanner/LibraryItemScanner') @@ -398,8 +398,8 @@ class LibraryItemController { const options = { format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'), - height: height ? parseInt(height) : null, - width: width ? parseInt(width) : null + height: clampPositiveInt(height ? parseInt(height) : null, 4096), + width: clampPositiveInt(width ? parseInt(width) : null, 4096) } return CacheManager.handleCoverCache(res, libraryItemId, options) } diff --git a/server/utils/index.js b/server/utils/index.js index c7700a783..49a7c8e67 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -54,6 +54,16 @@ module.exports.isNullOrNaN = (num) => { return num === null || isNaN(num) } +/** + * @param {number|null|undefined} value + * @param {number} max + * @returns {number|null} + */ +module.exports.clampPositiveInt = (value, max) => { + if (value == null || !Number.isFinite(value) || value <= 0) return null + return Math.min(Math.floor(value), max) +} + const xmlToJSON = (xml) => { return new Promise((resolve, reject) => { parseString(xml, (err, results) => { From 09fa0b38f5af5a9bc8d07b1541dc21718585e697 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 17 Apr 2026 16:51:22 -0500 Subject: [PATCH 221/285] Update podcast create path validation & fix relPath --- server/controllers/PodcastController.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index c70287600..f099d05ed 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -7,7 +7,7 @@ const Database = require('../Database') const fs = require('../libs/fsExtra') const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils') -const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils') +const { getFileTimestampsWithIno, filePathToPOSIX, isSameOrSubPath } = require('../utils/fileUtils') const { validateUrl } = require('../utils/index') const htmlSanitizer = require('../utils/htmlSanitizer') @@ -58,8 +58,18 @@ class PodcastController { return res.status(404).send('Folder not found') } + if (typeof payload.path !== 'string' || !payload.path.trim()) { + return res.status(400).send('Invalid request body. "path" must be a non-empty string') + } + + const libraryFolderPath = filePathToPOSIX(folder.path) const podcastPath = filePathToPOSIX(payload.path) + if (!isSameOrSubPath(libraryFolderPath, podcastPath)) { + Logger.error(`[PodcastController] Create: Podcast path is outside library folder "${libraryFolderPath}": "${podcastPath}"`) + return res.status(400).send('Podcast path must be inside the selected library folder') + } + // Check if a library item with this podcast folder exists already const existingLibraryItem = (await Database.libraryItemModel.count({ @@ -83,7 +93,7 @@ class PodcastController { const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath) - let relPath = payload.path.replace(folder.fullPath, '') + let relPath = podcastPath.replace(libraryFolderPath, '') if (relPath.startsWith('/')) relPath = relPath.slice(1) let newLibraryItem = null From b27f21fd95b413c5ecd74f5b14d8f2c0284ba6d7 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 17 Apr 2026 16:59:22 -0500 Subject: [PATCH 222/285] Update podcastUtils to sanitize episode subtitle from rss feed --- server/utils/podcastUtils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 2042a8e39..1cb0c4cb4 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -217,6 +217,10 @@ function extractEpisodeData(item) { episode[cleanKey] = extractFirstArrayItemString(item, key) }) + if (episode.subtitle) { + episode.subtitle = htmlSanitizer.sanitize(episode.subtitle.trim()) + } + // Extract psc:chapters if duration is set episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null From 24cab79c66984b86652161b51290df641eb656f1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 18 Apr 2026 16:24:48 -0500 Subject: [PATCH 223/285] Update filesystem/pathexists endpoint to use existing isSameOrSubPath func --- server/controllers/FileSystemController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index 4b0a94b39..41e082fd4 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -117,7 +117,7 @@ class FileSystemController { filepath = fileUtils.filePathToPOSIX(filepath) // Ensure filepath is inside library folder (prevents directory traversal) - if (!filepath.startsWith(libraryFolder.path)) { + if (!fileUtils.isSameOrSubPath(libraryFolder.path, filepath)) { Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`) return res.sendStatus(400) } From 39adefb63281fb4d1dd0c20bd8b706b85f104a89 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 18 Apr 2026 17:03:37 -0500 Subject: [PATCH 224/285] Update backup load & upload to remove tempfile on failed backups, validate details filesize & close zip --- server/managers/BackupManager.js | 39 +++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index 2697f94ea..a7b531e62 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -126,13 +126,31 @@ class BackupManager { } catch (error) { // Not a valid zip file Logger.error('[BackupManager] Failed to read backup file - backup might not be a valid .zip file', tempPath, error) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(400).send('Failed to read backup file - backup might not be a valid .zip file') } - if (!Object.keys(entries).includes('absdatabase.sqlite')) { + if (!entries['absdatabase.sqlite']) { Logger.error(`[BackupManager] Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(500).send('Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.') } + const detailsEntry = entries['details'] + if (!detailsEntry) { + Logger.error('[BackupManager] Invalid backup - missing details entry') + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) + return res.status(400).send('Invalid backup file - missing details entry') + } + if (detailsEntry.size > 1024 * 1024) { + Logger.error(`[BackupManager] Backup details entry too large: ${detailsEntry.size} bytes`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) + return res.status(400).send('Invalid backup file - details entry too large') + } + const data = await zip.entryData('details') const details = data.toString('utf8').split('\n') @@ -140,9 +158,13 @@ class BackupManager { if (!backup.serverVersion) { Logger.error(`[BackupManager] Invalid backup with no server version - might be a backup created before version 2.0.0`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(500).send('Invalid backup. Might be a backup created before version 2.0.0.') } + await zip.close().catch(() => {}) + backup.fileSize = await getFileSize(backup.fullPath) const existingBackupIndex = this.backups.findIndex((b) => b.id === backup.id) @@ -257,9 +279,24 @@ class BackupManager { let data = null try { zip = new StreamZip.async({ file: fullFilePath }) + const entries = await zip.entries() + + const detailsEntry = entries['details'] + if (!detailsEntry) { + Logger.error(`[BackupManager] Backup "${fullFilePath}" missing details entry - skipping`) + await zip.close().catch(() => {}) + continue + } + if (detailsEntry.size > 1024 * 1024) { + Logger.error(`[BackupManager] Backup "${fullFilePath}" details entry too large (${detailsEntry.size} bytes) - skipping`) + await zip.close().catch(() => {}) + continue + } + data = await zip.entryData('details') } catch (error) { Logger.error(`[BackupManager] Failed to unzip backup "${fullFilePath}"`, error) + if (zip) await zip.close().catch(() => {}) continue } From b7e8a0474a9df3b175b587405133898a4b1f7c1c Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 19 Apr 2026 16:20:31 -0500 Subject: [PATCH 225/285] Update bulk download endpoint ensure items are from the same library requested --- server/controllers/LibraryController.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index be9d03328..73b3d5c60 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -1435,10 +1435,15 @@ class LibraryController { const libraryItems = await Database.libraryItemModel.findAll({ attributes: ['id', 'libraryId', 'path', 'isFile'], where: { - id: itemIds + id: itemIds, + libraryId: req.library.id } }) + if (libraryItems.length < itemIds.length) { + Logger.warn(`[LibraryController] User "${req.user.username}" requested ${itemIds.length} items but only ${libraryItems.length} are in library "${req.library.id}"`) + } + Logger.info(`[LibraryController] User "${req.user.username}" requested download for items "${itemIds}"`) const filename = `LibraryItems-${Date.now()}.zip` From d73b64a19c42ccd0e4416991ce23ce6b9993a171 Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Fri, 20 Mar 2026 10:41:58 +0100 Subject: [PATCH 226/285] Translated using Weblate (Belarusian) 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/be/ --- client/strings/be.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index d598ef7bf..018694130 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -81,7 +81,7 @@ "ButtonRemove": "Выдаліць", "ButtonRemoveAll": "Выдаліць усе", "ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі", - "ButtonRemoveFromContinueListening": "Выдаліць з Працягнуць праслухоўванне", + "ButtonRemoveFromContinueListening": "Выдаліць з Працяг праслухоўвання", "ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне", "ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю", "ButtonReset": "Скінуць", @@ -292,7 +292,7 @@ "LabelCollections": "Калекцыі", "LabelComplete": "Завяршыць", "LabelConfirmPassword": "Пацвердзіце пароль", - "LabelContinueListening": "Працягнуць праслухоўванне", + "LabelContinueListening": "Працяг праслухоўвання", "LabelContinueReading": "Працягнуць чытанне", "LabelContinueSeries": "Працягнуць серыі", "LabelCorsAllowed": "Дазволеныя крыніцы CORS", @@ -545,7 +545,7 @@ "LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі", "LabelRSSFeedURL": "URL RSS-стужкі", "LabelRandomly": "Выпадкова", - "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягнуць праслухоўванне", + "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працяг праслухоўвання", "LabelRead": "Чытаць", "LabelReadAgain": "Чытаць зноў", "LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу", @@ -634,12 +634,12 @@ "LabelSortAscending": "Па ўзрастанні", "LabelSortDescending": "Па ўбыванні", "LabelSortPubDate": "Сартаваць па даце публікацыі", - "LabelStart": "Пачаць", + "LabelStart": "Пачатак", "LabelStartTime": "Час пачатку", "LabelStarted": "Пачата", "LabelStartedAt": "Пачата ў", "LabelStartedDate": "Пачата {0}", - "LabelStatsAudioTracks": "Аўдыятрэкаў", + "LabelStatsAudioTracks": "Аўдыятрэкі", "LabelStatsAuthors": "Аўтараў", "LabelStatsBestDay": "Найлепшы дзень", "LabelStatsDailyAverage": "У сярэднім за дзень", From 2d4df273f0ca5c46455ba5e1ce9e65ba4bace950 Mon Sep 17 00:00:00 2001 From: Francisco Serrador Date: Thu, 19 Mar 2026 18:28:11 +0100 Subject: [PATCH 227/285] Translated using Weblate (Spanish) 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/es/ --- client/strings/es.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index eae32bd9f..e9655563d 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -40,7 +40,7 @@ "ButtonFullPath": "Ruta completa", "ButtonHide": "Ocultar", "ButtonHome": "Inicio", - "ButtonIssues": "Cuestiones", + "ButtonIssues": "Incidencias", "ButtonJumpBackward": "Retroceder", "ButtonJumpForward": "Adelantar", "ButtonLatest": "Más recientes", @@ -850,7 +850,7 @@ "MessageNoEpisodes": "Ningún episodio", "MessageNoFoldersAvailable": "Ninguna carpeta disponible", "MessageNoGenres": "Ningún género", - "MessageNoIssues": "Ningún número", + "MessageNoIssues": "Sin incidencias", "MessageNoItems": "Ningún elemento", "MessageNoItemsFound": "Ningún elemento encontrado", "MessageNoListeningSessions": "Ninguna sesión de escucha", @@ -1116,8 +1116,8 @@ "ToastRemoveFailed": "Error al eliminar", "ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección", "ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección", - "ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca incorrectos", - "ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca incorrectos", + "ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca con incidencias", + "ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca con incidencias", "ToastRenameFailed": "Error al cambiar el nombre", "ToastRescanFailed": "Error al volver a escanear para {0}", "ToastRescanRemoved": "Se eliminó el elemento reescaneado", From 2755204168d7f49a9bef25fe66431e89b502ab74 Mon Sep 17 00:00:00 2001 From: Vadzim Kurdzesau Date: Sun, 29 Mar 2026 15:06:51 +0200 Subject: [PATCH 228/285] 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/ --- client/strings/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/ru.json b/client/strings/ru.json index c84fe9dcf..4375fe056 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -437,8 +437,8 @@ "LabelLibraryItem": "Элемент библиотеки", "LabelLibraryName": "Имя библиотеки", "LabelLibrarySortByProgress": "Прогресс: Последнее обновление", - "LabelLibrarySortByProgressFinished": "Прогресс: Завершено", - "LabelLibrarySortByProgressStarted": "Прогресс: Начато", + "LabelLibrarySortByProgressFinished": "Прогресс: Закончена", + "LabelLibrarySortByProgressStarted": "Прогресс: Начата", "LabelLimit": "Лимит", "LabelLineSpacing": "Межстрочный интервал", "LabelListenAgain": "Послушать снова", From bc6bfbe80408282528dbb296ae1fc74b8b4e30d4 Mon Sep 17 00:00:00 2001 From: tfr tint Date: Sun, 29 Mar 2026 10:52:39 +0200 Subject: [PATCH 229/285] Translated using Weblate (Italian) 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/it/ --- client/strings/it.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/strings/it.json b/client/strings/it.json index d2178cbf6..b2b2b19a8 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -34,7 +34,7 @@ "ButtonEditChapters": "Modifica Capitoli", "ButtonEditPodcast": "Modifica Podcast", "ButtonEnable": "Abilita", - "ButtonFireAndFail": "Fire and Fail", + "ButtonFireAndFail": "Centro e fallimento", "ButtonFireOnTest": "Fire onTest event", "ButtonForceReScan": "Forza Re-Scan", "ButtonFullPath": "Percorso Completo", @@ -182,7 +182,7 @@ "HeaderPlaylist": "Playlist", "HeaderPlaylistItems": "Elementi della playlist", "HeaderPodcastsToAdd": "Podcasts da Aggiungere", - "HeaderPresets": "Presets", + "HeaderPresets": "Preimpostazioni", "HeaderPreviewCover": "Anteprima Cover", "HeaderRSSFeedGeneral": "Dettagli RSS", "HeaderRSSFeedIsOpen": "RSS Feed è aperto", @@ -306,7 +306,7 @@ "LabelCustomCronExpression": "Espressione Cron personalizzata:", "LabelDatetime": "Data & Ora", "LabelDays": "Giorni", - "LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)", + "LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (despunta per rimuoverla solo dal database)", "LabelDescription": "Descrizione", "LabelDeselectAll": "Deseleziona Tutto", "LabelDetectedPattern": "Trovato pattern:", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Nessuno {0}", "LabelLibraryItem": "Elementi della biblioteca", "LabelLibraryName": "Nome della biblioteca", - "LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti", - "LabelLibrarySortByProgressFinished": "Progressi: Completati", - "LabelLibrarySortByProgressStarted": "Progressi: Iniziati", + "LabelLibrarySortByProgress": "Progresso: ultimo aggiornamento", + "LabelLibrarySortByProgressFinished": "Progresso: finito", + "LabelLibrarySortByProgressStarted": "Progresso: iniziato", "LabelLimit": "Limiti", "LabelLineSpacing": "Interlinea", "LabelListenAgain": "Ascolta ancora", @@ -497,7 +497,7 @@ "LabelNumberOfBooks": "Numero di libri", "LabelNumberOfChapters": "Numero di capitoli:", "LabelNumberOfEpisodes": "Numero di episodi", - "LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministratori (se configurato). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata comefalsa. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:", + "LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministrativi (se configurato). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata come falso. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:", "LabelOpenIDClaims": "Lasciare vuote le seguenti opzioni per disabilitare l'assegnazione avanzata di gruppi e autorizzazioni, assegnando quindi automaticamente il gruppo \"Utente\".", "LabelOpenIDGroupClaimDescription": "Nome dell'attestazione OpenID che contiene un elenco dei gruppi dell'utente. Comunemente indicato come gruppo. se configurato, l'applicazione assegnerà automaticamente i ruoli in base alle appartenenze ai gruppi dell'utente, a condizione che tali gruppi siano denominati \"admin\", \"utente\" o \"ospite\" senza distinzione tra maiuscole e minuscole nell'attestazione. L'attestazione deve contenere un elenco e, se un utente appartiene a più gruppi, l'applicazione assegnerà il ruolo corrispondente al livello di accesso più alto. Se nessun gruppo corrisponde, l'accesso verrà negato.", "LabelOpenRSSFeed": "Apri RSS Feed", @@ -530,7 +530,7 @@ "LabelPrimaryEbook": "Libro principale", "LabelProgress": "Cominciati", "LabelProvider": "Fornitore", - "LabelProviderAuthorizationValue": "Authorization Header Value", + "LabelProviderAuthorizationValue": "Valore intestazione di autorizzazione", "LabelPubDate": "Data di pubblicazione", "LabelPublishYear": "Anno di pubblicazione", "LabelPublishedDate": "Pubblicati {0}", @@ -682,7 +682,7 @@ "LabelTitle": "Titolo", "LabelToolsEmbedMetadata": "Incorpora Metadata", "LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.", - "LabelToolsM4bEncoder": "M4B Encoder", + "LabelToolsM4bEncoder": "Codificatore M4B", "LabelToolsMakeM4b": "Crea un file M4B", "LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.", "LabelToolsSplitM4b": "Converti M4B in MP3", @@ -854,7 +854,7 @@ "MessageNoItems": "Nessun oggetto", "MessageNoItemsFound": "Nessun oggetto trovato", "MessageNoListeningSessions": "Nessuna sessione di ascolto", - "MessageNoLogs": "Nessun Log", + "MessageNoLogs": "Nessun rapporto", "MessageNoMediaProgress": "Nessun progresso multimediale", "MessageNoNotifications": "Nessuna notifica", "MessageNoPodcastFeed": "Podcast non valido: nessun feed", @@ -1109,7 +1109,7 @@ "ToastProgressIsNotBeingSynced": "L'avanzamento non è sincronizzato, riavviare la riproduzione", "ToastProviderCreatedFailed": "Impossibile aggiungere il provider", "ToastProviderCreatedSuccess": "Aggiunto nuovo provider", - "ToastProviderNameAndUrlRequired": "Nome e URL richiesti", + "ToastProviderNameAndUrlRequired": "Nome e Url richiesti", "ToastProviderRemoveSuccess": "Provider rimosso", "ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS", "ToastRSSFeedCloseSuccess": "Flusso RSS chiuso", From 0e2cdde731cb86de7d5452add2ebd1af10967926 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 29 Mar 2026 15:01:47 +0200 Subject: [PATCH 230/285] Translated using Weblate (Slovak) 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/sk/ --- client/strings/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/sk.json b/client/strings/sk.json index 6101eba7d..23ed276e8 100644 --- a/client/strings/sk.json +++ b/client/strings/sk.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Knihy", "LabelButtonText": "Text tlačidla", - "LabelByAuthor": "od", + "LabelByAuthor": "od {0}", "LabelChangePassword": "Zmeniť heslo", "LabelChannels": "Kanály", "LabelChapterCount": "{0} kapitol", From 0bbf8bde5cfb0fa576a3f701de6d0d3d6a75e1ae Mon Sep 17 00:00:00 2001 From: A L Date: Mon, 30 Mar 2026 13:23:36 +0200 Subject: [PATCH 231/285] Translated using Weblate (Bulgarian) Currently translated at 91.2% (1061 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/ --- client/strings/bg.json | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/client/strings/bg.json b/client/strings/bg.json index dacd62087..460f0ff83 100644 --- a/client/strings/bg.json +++ b/client/strings/bg.json @@ -436,7 +436,7 @@ "LabelLibraryFilterSublistEmpty": "Не {0}", "LabelLibraryItem": "Елемент на Библиотека", "LabelLibraryName": "Име на Библиотека", - "LabelLibrarySortByProgress": "Прогрес: Последно Обновен", + "LabelLibrarySortByProgress": "Прогрес: Последно обновление", "LabelLibrarySortByProgressFinished": "Прогрес: Приключено", "LabelLibrarySortByProgressStarted": "Прогрес: Започнато", "LabelLimit": "Лимит", @@ -892,7 +892,7 @@ "MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}", "MessageSearchResultsFor": "Резултати от търсенето за", "MessageSelected": "{0} избрани", - "MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации.", + "MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации", "MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат", "MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла", "MessageShareExpirationWillBe": "Изтичането ще бъде на {0}", @@ -956,6 +956,8 @@ "NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод", "NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити", "NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод", + "NotificationOnTestDescription": "Event за тестване на системата за нотификации", + "PlaceholderBulkChapterInput": "Въведете име на глава или използвайте номериране (прим. 'Епизод 1', 'Глава 10', '1.')", "PlaceholderNewCollection": "Ново име на колекцията", "PlaceholderNewFolderPath": "Нов път на папката", "PlaceholderNewPlaylist": "Ново име на плейлиста", @@ -963,26 +965,58 @@ "PlaceholderSearchEpisode": "Търсене на Епизоди...", "StatsAuthorsAdded": "добаврени автори", "StatsBooksAdded": "добавени книги", + "StatsBooksAdditional": "Някой от вкючените добавки…", "StatsBooksFinished": "завършени книги", + "StatsBooksFinishedThisYear": "Някой от книгите приключени тази година…", + "StatsBooksListenedTo": "слушани книги", + "StatsCollectionGrewTo": "Твоята книжна колекция израсна до…", + "StatsSessions": "сесии", + "StatsSpentListening": "прекарано в слушане", + "StatsTopAuthor": "ТОП АВТОР", + "StatsTopAuthors": "ТОП АВТОРИ", + "StatsTopGenre": "ТОП ЖАНР", + "StatsTopGenres": "ТОП ЖАНРА", + "StatsTopMonth": "ТОП МЕСЕЦ", + "StatsTopNarrator": "ТОП РАЗКАЗВАЧ", + "StatsTopNarrators": "ТОП РАЗКАЗВАЧИ", + "StatsTotalDuration": "С пълно времетраене…", + "StatsYearInReview": "ГОДИНАТА В ПРЕГЛЕД", "ToastAccountUpdateSuccess": "Успешно обновяване на акаунта", + "ToastAppriseUrlRequired": "Трябва да въведете Apprise URL", + "ToastAsinRequired": "ASIN-а е задължителен", "ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната", + "ToastAuthorNotFound": "Автор \"{0}\" не е намерен", + "ToastAuthorRemoveSuccess": "Арторът е премахнат", + "ToastAuthorSearchNotFound": "Авторът не е намерен", "ToastAuthorUpdateMerged": "Обновяване на автора сливано", "ToastAuthorUpdateSuccess": "Автора обновен", "ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)", + "ToastBackupAppliedSuccess": "Архивът е приложен", "ToastBackupCreateFailed": "Неуспешно създаване на архив", "ToastBackupCreateSuccess": "Архивът е създаден", "ToastBackupDeleteFailed": "Неуспешно изтриване на архив", "ToastBackupDeleteSuccess": "Архивът е изтрит", + "ToastBackupInvalidMaxKeep": "Невалиден брой за архиви за запазване", + "ToastBackupInvalidMaxSize": "Невалиден максимален рамер на архив", "ToastBackupRestoreFailed": "Неуспешно възстановяване на архив", "ToastBackupUploadFailed": "Неуспешно качване на архив", "ToastBackupUploadSuccess": "Архивът е качен", + "ToastBatchApplyDetailsToItemsSuccess": "Детайли приложени на предмети", + "ToastBatchDeleteFailed": "Груповото изтриване се провали", + "ToastBatchDeleteSuccess": "Успешно групово изтриване", + "ToastBatchQuickMatchFailed": "Груповото Бързо Съвпадение се провали!", + "ToastBatchQuickMatchStarted": "Груповото Бързо Съвпадение на {0} книги започна!", "ToastBatchUpdateFailed": "Неуспешно групово актуализиране", "ToastBatchUpdateSuccess": "Успешно групово актуализиране", "ToastBookmarkCreateFailed": "Неуспешно създаване на отметка", "ToastBookmarkCreateSuccess": "Отметката е създадена", "ToastBookmarkRemoveSuccess": "Отметката е премахната", + "ToastBulkChapterInvalidCount": "Въведете число между 1 и 150", "ToastCachePurgeFailed": "Неуспешно изчистване на кеша", "ToastCachePurgeSuccess": "Успешно изчистване на кеша", + "ToastChapterLocked": "Главата е заключена.", + "ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди", + "ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.", "ToastChaptersHaveErrors": "Главите имат грешки", "ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия", "ToastCollectionRemoveSuccess": "Колекцията е премахната", From a30fe15b106c905b5e235326c268c96328895791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D0=B5=D0=B2?= Date: Thu, 9 Apr 2026 12:03:20 +0200 Subject: [PATCH 232/285] 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/ --- client/strings/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/ru.json b/client/strings/ru.json index 4375fe056..ed3f18b89 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -392,7 +392,7 @@ "LabelGenre": "Жанр", "LabelGenres": "Жанры", "LabelHardDeleteFile": "Жесткое удаление файла", - "LabelHasEbook": "Есть e-книга", + "LabelHasEbook": "Есть электронная книга", "LabelHasSupplementaryEbook": "Есть дополнительная e-книга", "LabelHideSubtitles": "Скрыть серии", "LabelHighestPriority": "Наивысший приоритет", From f558182d94f9c6a7c61319ced8f23592a6e43afb Mon Sep 17 00:00:00 2001 From: tizio04 Date: Wed, 8 Apr 2026 15:12:13 +0200 Subject: [PATCH 233/285] Translated using Weblate (Italian) Currently translated at 99.9% (1162 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/ --- client/strings/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/it.json b/client/strings/it.json index b2b2b19a8..4b3921624 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -674,7 +674,7 @@ "LabelTimeDurationXMinutes": "{0} minuti", "LabelTimeDurationXSeconds": "{0} secondi", "LabelTimeInMinutes": "Tempo in minuti", - "LabelTimeLeft": "{0} sinistra", + "LabelTimeLeft": "{0} rimasti", "LabelTimeListened": "Tempo di Ascolto", "LabelTimeListenedToday": "Tempo di Ascolto Oggi", "LabelTimeRemaining": "{0} rimanente", From 3e0099e8d9f240c468ac184aded63613dffb4d4b Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Fri, 10 Apr 2026 15:29:08 +0200 Subject: [PATCH 234/285] Translated using Weblate (Belarusian) 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/be/ --- client/strings/be.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index 018694130..43668acb5 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -16,7 +16,7 @@ "ButtonBrowseForFolder": "Агляд папак", "ButtonCancel": "Скасаваць", "ButtonCancelEncode": "Скасаваць кадзіраванне", - "ButtonChangeRootPassword": "Зменіце Root пароль", + "ButtonChangeRootPassword": "Змяніць пароль root", "ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя выпускі", "ButtonChooseAFolder": "Выбраць папку", "ButtonChooseFiles": "Выбраць файлы", @@ -252,8 +252,8 @@ "LabelAudioChannels": "Аўдыяканалы (1 або 2)", "LabelAudioCodec": "Аўдыякодэк", "LabelAuthor": "Аўтар", - "LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)", - "LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)", + "LabelAuthorFirstLast": "Аўтар (імя, прозвішча)", + "LabelAuthorLastFirst": "Аўтар (прозвішча, імя)", "LabelAuthors": "Аўтары", "LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі", "LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых", @@ -424,7 +424,7 @@ "LabelLastBookAdded": "Апошняя дададзеная кніга", "LabelLastBookUpdated": "Апошняя абноўленая кніга", "LabelLastProgressDate": "Апошні прагрэс: {0}", - "LabelLastSeen": "Апошні прагляд", + "LabelLastSeen": "Апошняя актыўнасць", "LabelLastTime": "Апошні раз", "LabelLastUpdate": "Апошняе абнаўленне", "LabelLayout": "Знешні выгляд", From 88879f140923aa6b92c111245539ea16dbea6a86 Mon Sep 17 00:00:00 2001 From: Mario Date: Thu, 16 Apr 2026 11:41:30 +0200 Subject: [PATCH 235/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index ed0cae52c..0e8cb051f 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -622,7 +622,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet", "LabelSettingsTimeFormat": "Zeitformat", "LabelShare": "Freigeben", - "LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link, die Dateien des Mediums als ZIP herunterzuladen.", + "LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link die Dateien des Mediums als ZIP herunterzuladen.", "LabelShareOpen": "Freigeben", "LabelShareURL": "Freigabe URL", "LabelShowAll": "Alles anzeigen", @@ -1103,7 +1103,7 @@ "ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden", "ToastPodcastCreateSuccess": "Podcast erstellt", "ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert", - "ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast-Feeds", + "ToastPodcastGetFeedFailed": "Fehler beim Abrufen des Podcast Feeds", "ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden", "ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed", "ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet", From e3388d4446d5d18a8fb99e1dd9a5fd86f7ba72d3 Mon Sep 17 00:00:00 2001 From: Laurin Sorgend Date: Thu, 16 Apr 2026 11:39:35 +0200 Subject: [PATCH 236/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 0e8cb051f..2ace31eba 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -816,7 +816,7 @@ "MessageFeedURLWillBe": "Feed-URL wird {0} sein", "MessageFetching": "Wird abgerufen …", "MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.", - "MessageHeatmapListeningTimeTooltip": "{0} auf {1} gehört", + "MessageHeatmapListeningTimeTooltip": "{0} gehört auf {1}", "MessageHeatmapNoListeningSessions": "Keine Hörsitzungen am {0}", "MessageImportantNotice": "Wichtiger Hinweis!", "MessageInsertChapterBelow": "Kapitel unten einfügen", From e431ea047245322ae5531257e7b331476efaecc4 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 16 Apr 2026 11:40:38 +0200 Subject: [PATCH 237/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 2ace31eba..2372d5a41 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -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", From aa4a191567ce0758b48d002f31644206ad3e20f0 Mon Sep 17 00:00:00 2001 From: Gernomaly Date: Thu, 16 Apr 2026 11:41:09 +0200 Subject: [PATCH 238/285] Translated using Weblate (German) 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/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index 2372d5a41..4d9ecd412 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -737,7 +737,7 @@ "MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen", "MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von Apprise API laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann.
Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter http://192.168.1.1:8337 läuft, würdest du http://192.168.1.1:8337/notify eingeben.", "MessageAsinCheck": "Stelle sicher, dass die ASIN aus der richtigen Audible Region verwendet wird, nicht Amazon.", - "MessageAuthenticationLegacyTokenWarning": "Alte API-Token werden in Zukunft entfernt. Benutze stattdessen API Keys.", + "MessageAuthenticationLegacyTokenWarning": "Nicht mehr unterstützte API tokens werden in der Zukunft entfernt. Nutze stattdessen API Schlüssel.", "MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.", "MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.", "MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in /metadata/items & /metadata/authors gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.", From b41db23994cb678241e0aeb19681f15ee6739c3a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 19 Apr 2026 16:46:10 -0500 Subject: [PATCH 239/285] Version bump v2.33.2 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 71bcf26a8..57d2da716 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.33.1", + "version": "2.33.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.33.1", + "version": "2.33.2", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 782f411bd..f747cf90f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.33.1", + "version": "2.33.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index d8cc43bc8..250390c67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.33.1", + "version": "2.33.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.33.1", + "version": "2.33.2", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index d44df1165..810863f97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.33.1", + "version": "2.33.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 80b39abaa2723cf9bc0065fec702f7353721f340 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 21 Apr 2026 17:13:06 -0500 Subject: [PATCH 240/285] Update library item batch api endpoints check users per-item access & return 403 --- server/controllers/LibraryItemController.js | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 1a6b8ac11..e524ebcfd 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -36,6 +36,24 @@ const ShareManager = require('../managers/ShareManager') * @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile */ +/** + * Enforce per-item access for batch item routes + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {import('../models/LibraryItem')[]} libraryItems + * @returns {boolean} true if the user may access every item; false if 403 was sent + */ +function ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems) { + for (const libraryItem of libraryItems) { + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + res.sendStatus(403) + return false + } + } + return true +} + class LibraryItemController { constructor() {} @@ -547,7 +565,13 @@ class LibraryItemController { return res.sendStatus(404) } + // Ensure user has permission to delete these library items + if (!ensureUserCanAccessLibraryItemsForBatch(req, res, itemsToDelete)) { + return + } + const libraryId = itemsToDelete[0].libraryId + for (const libraryItem of itemsToDelete) { const libraryItemPath = libraryItem.path Logger.info(`[LibraryItemController] (${hardDelete ? 'Hard' : 'Soft'}) deleting Library Item "${libraryItem.media.title}" with id "${libraryItem.id}"`) @@ -581,6 +605,7 @@ class LibraryItemController { } await Database.resetLibraryIssuesFilterData(libraryId) + res.sendStatus(200) } @@ -593,6 +618,11 @@ class LibraryItemController { * @param {Response} res */ async batchUpdate(req, res) { + if (!req.user.canUpdate) { + Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to batch update without permission`) + return res.sendStatus(403) + } + const updatePayloads = req.body if (!Array.isArray(updatePayloads) || !updatePayloads.length) { Logger.error(`[LibraryItemController] Batch update failed. Invalid payload`) @@ -615,6 +645,11 @@ class LibraryItemController { return res.sendStatus(404) } + // Ensure user has permission to update these library items + if (!ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems)) { + return + } + let itemsUpdated = 0 const seriesIdsRemoved = [] @@ -695,6 +730,10 @@ class LibraryItemController { const libraryItems = await Database.libraryItemModel.findAllExpandedWhere({ id: libraryItemIds }) + // Ensure user has permission to access these library items + if (!ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems)) { + return + } res.json({ libraryItems: libraryItems.map((li) => li.toOldJSONExpanded()) }) From 5b2a788cfc9afd8f38a2a21a2f5fe65a5a8a1006 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 21 Apr 2026 17:13:52 -0500 Subject: [PATCH 241/285] Update LibraryItemController test with 403 tests --- .../controllers/LibraryItemController.test.js | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/test/server/controllers/LibraryItemController.test.js b/test/server/controllers/LibraryItemController.test.js index 5a0422392..1d57219d3 100644 --- a/test/server/controllers/LibraryItemController.test.js +++ b/test/server/controllers/LibraryItemController.test.js @@ -123,7 +123,9 @@ describe('LibraryItemController', () => { const fakeReq = { query: {}, user: { - canDelete: true + username: 'test', + canDelete: true, + checkCanAccessLibraryItem: () => true }, body: { libraryItemIds: [libraryItem1Id] @@ -199,4 +201,102 @@ describe('LibraryItemController', () => { expect(series2Exists).to.be.true }) }) + + describe('batch item access control', () => { + let lib1Id + let itemLib1Id + let itemLib2Id + + beforeEach(async () => { + const lib1 = await Database.libraryModel.create({ name: 'Lib 1', mediaType: 'book' }) + const folder1 = await Database.libraryFolderModel.create({ path: '/l1', libraryId: lib1.id }) + const book1 = await Database.bookModel.create({ title: 'B1', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const li1 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book1.id, + mediaType: 'book', + libraryId: lib1.id, + libraryFolderId: folder1.id + }) + lib1Id = lib1.id + itemLib1Id = li1.id + + const lib2 = await Database.libraryModel.create({ name: 'Lib 2', mediaType: 'book' }) + const folder2 = await Database.libraryFolderModel.create({ path: '/l2', libraryId: lib2.id }) + const book2 = await Database.bookModel.create({ title: 'B2', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const li2 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book2.id, + mediaType: 'book', + libraryId: lib2.id, + libraryFolderId: folder2.id + }) + itemLib2Id = li2.id + }) + + const userLimitedToLib1 = () => ({ + username: 'limited', + canDelete: true, + canUpdate: true, + checkCanAccessLibraryItem(li) { + return li.libraryId === lib1Id + } + }) + + it('batchGet returns 403 for a library item the user cannot access', async () => { + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + body: { libraryItemIds: [itemLib2Id] }, + user: userLimitedToLib1() + } + await LibraryItemController.batchGet.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + + it('batchGet returns items when the user can access them', async () => { + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + body: { libraryItemIds: [itemLib1Id] }, + user: userLimitedToLib1() + } + await LibraryItemController.batchGet.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.json.calledOnce).to.be.true + const payload = fakeRes.json.firstCall.args[0] + expect(payload.libraryItems).to.have.length(1) + expect(payload.libraryItems[0].id).to.equal(itemLib1Id) + }) + + it('batchUpdate returns 403 for a library item the user cannot access', async () => { + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + user: userLimitedToLib1(), + body: [{ id: itemLib2Id, mediaPayload: {} }] + } + await LibraryItemController.batchUpdate.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + + it('batchUpdate returns 403 when the user lacks canUpdate', async () => { + const u = userLimitedToLib1() + u.canUpdate = false + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + user: u, + body: [{ id: itemLib1Id, mediaPayload: {} }] + } + await LibraryItemController.batchUpdate.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + + it('batchDelete returns 403 for a library item the user cannot access', async () => { + const fakeRes = { sendStatus: sinon.spy() } + const fakeReq = { + query: {}, + user: userLimitedToLib1(), + body: { libraryItemIds: [itemLib2Id] } + } + await LibraryItemController.batchDelete.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + }) }) From 79cc9765cf343e30515b6d4609cb741aad4a017d Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 22 Apr 2026 16:29:47 -0500 Subject: [PATCH 242/285] Update collection endpoints to check user library access --- server/controllers/CollectionController.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 1476b0f81..bb00ea346 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -41,6 +41,10 @@ class CollectionController { if (reqBody.description && typeof reqBody.description !== 'string') { return res.status(400).send('Invalid collection description') } + if (!req.user.checkCanAccessLibrary(reqBody.libraryId)) { + Logger.warn(`[CollectionController] User "${req.user.username}" attempted to create collection in inaccessible library ${reqBody.libraryId}`) + return res.sendStatus(403) + } const libraryItemIds = (reqBody.books || []).filter((b) => !!b && typeof b == 'string') if (!libraryItemIds.length) { return res.status(400).send('Invalid collection data. No books') @@ -109,8 +113,9 @@ class CollectionController { */ async findAll(req, res) { const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user) + const accessibleCollections = collectionsExpanded.filter((c) => req.user.checkCanAccessLibrary(c.libraryId)) res.json({ - collections: collectionsExpanded + collections: accessibleCollections }) } @@ -431,6 +436,10 @@ class CollectionController { if (!collection) { return res.status(404).send('Collection not found') } + if (!req.user.checkCanAccessLibrary(collection.libraryId)) { + Logger.warn(`[CollectionController] User "${req.user.username}" attempted to access collection ${collection.id} in inaccessible library ${collection.libraryId}`) + return res.status(404).send('Collection not found') + } req.collection = collection } From 9ab35ef418090fa5b8f9b36f366dbea1c078a660 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 22 Apr 2026 16:42:58 -0500 Subject: [PATCH 243/285] Update playlist endpoints to check user still has library access --- server/controllers/PlaylistController.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index bc1a7a455..6ad7cff9e 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -37,6 +37,10 @@ class PlaylistController { if (reqBody.description && typeof reqBody.description !== 'string') { return res.status(400).send('Invalid playlist description') } + if (!req.user.checkCanAccessLibrary(reqBody.libraryId)) { + Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to create playlist in inaccessible library ${reqBody.libraryId}`) + return res.sendStatus(403) + } const items = reqBody.items || [] const isPodcast = items.some((i) => i.episodeId) const libraryItemIds = new Set() @@ -133,8 +137,9 @@ class PlaylistController { */ async findAllForUser(req, res) { const playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id) + const accessiblePlaylists = playlistsForUser.filter((p) => req.user.checkCanAccessLibrary(p.libraryId)) res.json({ - playlists: playlistsForUser + playlists: accessiblePlaylists }) } @@ -508,6 +513,10 @@ class PlaylistController { if (!collection) { return res.status(404).send('Collection not found') } + if (!req.user.checkCanAccessLibrary(collection.libraryId)) { + Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to create playlist from collection ${collection.id} in inaccessible library ${collection.libraryId}`) + return res.status(404).send('Collection not found') + } // Expand collection to get library items const collectionExpanded = await collection.getOldJsonExpanded(req.user) if (!collectionExpanded) { @@ -573,6 +582,10 @@ class PlaylistController { Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`) return res.sendStatus(403) } + if (!req.user.checkCanAccessLibrary(playlist.libraryId)) { + Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to access playlist ${playlist.id} in inaccessible library ${playlist.libraryId}`) + return res.status(404).send('Playlist not found') + } req.playlist = playlist } From a5362de9cc0a6d69c96679bce94eb831d381ebba Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 23 Apr 2026 14:34:59 -0500 Subject: [PATCH 244/285] Update podcast createFromRequest to sanitize html description --- server/models/Podcast.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/models/Podcast.js b/server/models/Podcast.js index a96e1dd02..02f8981c8 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -89,6 +89,9 @@ class Podcast extends Model { } }) + const rawDescription = typeof payload.metadata.description === 'string' ? payload.metadata.description : null + const description = rawDescription ? htmlSanitizer.sanitize(rawDescription) : null + return this.create( { title, @@ -97,7 +100,7 @@ class Podcast extends Model { releaseDate: typeof payload.metadata.releaseDate === 'string' ? payload.metadata.releaseDate : null, feedURL: typeof payload.metadata.feedUrl === 'string' ? payload.metadata.feedUrl : null, imageURL: typeof payload.metadata.imageUrl === 'string' ? payload.metadata.imageUrl : null, - description: typeof payload.metadata.description === 'string' ? payload.metadata.description : null, + description, itunesPageURL: typeof payload.metadata.itunesPageUrl === 'string' ? payload.metadata.itunesPageUrl : null, itunesId: typeof payload.metadata.itunesId === 'string' ? payload.metadata.itunesId : null, itunesArtistId: typeof payload.metadata.itunesArtistId === 'string' ? payload.metadata.itunesArtistId : null, From d6a2e5596b70702f176f8fa0da2546993f049d13 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 24 Apr 2026 16:18:56 -0500 Subject: [PATCH 245/285] Fix undefined variable in error log for when podcast cron is invalid --- server/managers/CronManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index d3e652129..b000413c5 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -167,7 +167,7 @@ class CronManager { task }) } catch (error) { - Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error) + Logger.error(`[PodcastManager] Failed to schedule podcast cron ${expression}`, error) } } From 7c0ca44727cfa789a2e7147ae9424a95536f447c Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 24 Apr 2026 16:55:42 -0500 Subject: [PATCH 246/285] Update podcast create/update endpoints to validate autoDownloadSchedule cron expression, validate cron expression before starting in CronManager --- client/components/modals/item/tabs/Schedule.vue | 2 ++ server/controllers/LibraryItemController.js | 11 +++++++++++ server/controllers/MiscController.js | 12 +++++------- server/controllers/PodcastController.js | 6 ++++++ server/managers/CronManager.js | 5 +++++ server/models/Podcast.js | 2 ++ 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/client/components/modals/item/tabs/Schedule.vue b/client/components/modals/item/tabs/Schedule.vue index 2cd3af514..0faa29630 100644 --- a/client/components/modals/item/tabs/Schedule.vue +++ b/client/components/modals/item/tabs/Schedule.vue @@ -158,6 +158,8 @@ export default { this.isProcessing = true var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatePayload).catch((error) => { console.error('Failed to update', error) + const errorMessage = typeof error?.response?.data === 'string' ? error?.response?.data : null + this.$toast.error(errorMessage || this.$strings.ToastFailedToUpdate) return false }) this.isProcessing = false diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index e524ebcfd..07b4ee67f 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -1,6 +1,7 @@ const { Request, Response, NextFunction } = require('express') const Path = require('path') const fs = require('../libs/fsExtra') +const cron = require('../libs/nodeCron') const uaParserJs = require('../libs/uaParser') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') @@ -220,6 +221,11 @@ class LibraryItemController { } else if (mediaPayload.autoDownloadSchedule !== undefined && req.libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) { isPodcastAutoDownloadUpdated = true } + + if (mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) { + Logger.error(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${req.libraryItem.media.title}"`) + return res.status(400).send('Invalid auto download schedule cron expression') + } } let hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url @@ -659,6 +665,11 @@ class LibraryItemController { const mediaPayload = updatePayload.mediaPayload const libraryItem = libraryItems.find((li) => li.id === updatePayload.id) + if (libraryItem.isPodcast && mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) { + Logger.warn(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${libraryItem.media.title}" - skipping update`) + continue + } + let hasUpdates = await libraryItem.media.updateFromRequest(mediaPayload) if (libraryItem.isBook && Array.isArray(mediaPayload.metadata?.series)) { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 490cb27d2..591f8ccf2 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -8,7 +8,7 @@ const Database = require('../Database') const Watcher = require('../Watcher') const libraryItemFilters = require('../utils/queries/libraryItemFilters') -const patternValidation = require('../libs/nodeCron/pattern-validation') +const cron = require('../libs/nodeCron') const { isObject, getTitleIgnorePrefix } = require('../utils/index') const { sanitizeFilename } = require('../utils/fileUtils') @@ -605,13 +605,11 @@ class MiscController { return res.sendStatus(400) } - try { - patternValidation(expression) - res.sendStatus(200) - } catch (error) { - Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message) - res.status(400).send(error.message) + if (!cron.validate(expression)) { + Logger.warn(`[MiscController] Invalid cron expression ${expression}`) + return res.status(400).send('Invalid cron expression') } + res.sendStatus(200) } /** diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index f099d05ed..85173e622 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -5,6 +5,7 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const fs = require('../libs/fsExtra') +const cron = require('../libs/nodeCron') const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils') const { getFileTimestampsWithIno, filePathToPOSIX, isSameOrSubPath } = require('../utils/fileUtils') @@ -46,6 +47,11 @@ class PodcastController { return res.status(400).send('Invalid request body. "media" and "media.metadata" are required') } + if (payload.media.autoDownloadSchedule && !cron.validate(payload.media.autoDownloadSchedule)) { + Logger.error(`[PodcastController] Invalid auto download schedule cron expression "${payload.media.autoDownloadSchedule}"`) + return res.status(400).send('Invalid auto download schedule cron expression') + } + const library = await Database.libraryModel.findByIdWithFolders(payload.libraryId) if (!library) { Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index b000413c5..7138d26a8 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -153,6 +153,11 @@ class CronManager { startPodcastCron(expression, libraryItemIds) { try { + if (!cron.validate(expression)) { + Logger.error(`[CronManager] Invalid auto download schedule cron expression "${expression}" - not starting podcast episode check cron`) + return + } + Logger.debug(`[CronManager] Scheduling podcast episode check cron "${expression}" for ${libraryItemIds.length} item(s)`) const task = cron.schedule(expression, () => { if (this.podcastCronExpressionsExecuting.includes(expression)) { diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 02f8981c8..6dbe1cd19 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -78,6 +78,7 @@ class Podcast extends Model { */ static async createFromRequest(payload, transaction) { const title = typeof payload.metadata.title === 'string' ? payload.metadata.title : null + // cron expression validated in controller const autoDownloadSchedule = typeof payload.autoDownloadSchedule === 'string' ? payload.autoDownloadSchedule : null const genres = Array.isArray(payload.metadata.genres) && payload.metadata.genres.every((g) => typeof g === 'string' && g.length) ? payload.metadata.genres : [] const tags = Array.isArray(payload.tags) && payload.tags.every((t) => typeof t === 'string' && t.length) ? payload.tags : [] @@ -273,6 +274,7 @@ class Podcast extends Model { hasUpdates = true } if (typeof payload.autoDownloadSchedule === 'string' && payload.autoDownloadSchedule !== this.autoDownloadSchedule) { + // cron expression validated in controller this.autoDownloadSchedule = payload.autoDownloadSchedule hasUpdates = true } From f47bbc7886689ecbd8f0ed7c5be5cfa8104d0b2d Mon Sep 17 00:00:00 2001 From: na3shkw <48012204+na3shkw@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:56:16 +0000 Subject: [PATCH 247/285] Add Japanese language and Japan podcast search region --- client/plugins/i18n.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 0184ca9e5..a56fbd267 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -21,6 +21,7 @@ const languageCodeMap = { he: { label: 'עברית', dateFnsLocale: 'he' }, hr: { label: 'Hrvatski', dateFnsLocale: 'hr' }, it: { label: 'Italiano', dateFnsLocale: 'it' }, + ja: { label: '日本語', dateFnsLocale: 'ja' }, lt: { label: 'Lietuvių', dateFnsLocale: 'lt' }, hu: { label: 'Magyar', dateFnsLocale: 'hu' }, ko: { label: '한국어', dateFnsLocale: 'ko' }, @@ -60,6 +61,7 @@ const podcastSearchRegionMap = { hr: { label: 'Hrvatska' }, il: { label: 'ישראל / إسرائيل' }, it: { label: 'Italia' }, + jp: { label: '日本' }, lu: { label: 'Luxembourg / Luxemburg / Lëtezebuerg' }, hu: { label: 'Magyarország' }, nl: { label: 'Nederland' }, From 3ccdcaec1a1a035bbcb1140d1057f98dfef035ae Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 25 Apr 2026 16:46:54 -0500 Subject: [PATCH 248/285] Implement SSRF filter for podcast episode downloads --- server/utils/ffmpegHelpers.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 80832cc77..7ad2a3aee 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -1,4 +1,5 @@ const axios = require('axios') +const ssrfFilter = require('ssrf-req-filter') const Ffmpeg = require('../libs/fluentFfmpeg') const ffmpgegUtils = require('../libs/fluentFfmpeg/utils') const fs = require('../libs/fsExtra') @@ -97,6 +98,8 @@ async function resizeImage(filePath, outputPath, width, height) { module.exports.resizeImage = resizeImage /** + * Download podcast episode + * Uses SSRF filter to prevent internal URLs * * @param {import('../objects/PodcastEpisodeDownload')} podcastEpisodeDownload * @returns {Promise<{success: boolean, isRequestError?: boolean}>} @@ -121,7 +124,9 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { Accept: '*/*', 'User-Agent': userAgent }, - timeout: global.PodcastDownloadTimeout + timeout: global.PodcastDownloadTimeout, + httpAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : ssrfFilter(podcastEpisodeDownload.url), + httpsAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : ssrfFilter(podcastEpisodeDownload.url) }) Logger.debug(`[ffmpegHelpers] Successfully connected with User-Agent: ${userAgent}`) From 928051744aa419f8ac311153419a3756997f42d2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 25 Apr 2026 17:13:22 -0500 Subject: [PATCH 249/285] ShareController check ?t param is less than duration, revert frontend mounted usage of param --- client/pages/share/_slug.vue | 5 ++--- server/controllers/ShareController.js | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/pages/share/_slug.vue b/client/pages/share/_slug.vue index 15748b48f..fdcbbe500 100644 --- a/client/pages/share/_slug.vue +++ b/client/pages/share/_slug.vue @@ -363,9 +363,8 @@ export default { console.log('Loaded media item share', this.mediaItemShare) } - const startTime = this.$route.query.t && !isNaN(this.$route.query.t) - ? parseFloat(this.$route.query.t) - : (this.playbackSession.currentTime || 0) + const startTime = this.playbackSession.currentTime || 0 + this.localAudioPlayer.set(null, this.audioTracks, false, startTime, false) this.localAudioPlayer.on('stateChange', this.playerStateChange.bind(this)) this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this)) diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index f7dd36f83..73da84a8a 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -20,7 +20,7 @@ const ShareManager = require('../managers/ShareManager') */ class ShareController { - constructor() { } + constructor() {} /** * Public route @@ -54,7 +54,7 @@ class ShareController { if (mediaItemShare.id === playbackSession.mediaItemShareId) { Logger.debug(`[ShareController] Found share playback session ${req.cookies.share_session_id}`) // If ?t was provided, override the cached currentTime - if (startTime > 0) { + if (startTime > 0 && startTime < playbackSession.duration) { playbackSession.currentTime = startTime } mediaItemShare.playbackSession = playbackSession.toJSONForClient() From 92df92ec99fd8b53006ba24d53e887b044494aff Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Apr 2026 16:51:08 -0500 Subject: [PATCH 250/285] Fix recent episodes endpoint cache not being cleared when updating media progress #5159 --- server/managers/ApiCacheManager.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index 8a67e4898..e34e4c055 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -42,11 +42,14 @@ class ApiCacheManager { } clearUserProgressSlices(modelName, hook) { - const removedPersonalized = this.modelsInvalidatingPersonalized.has(modelName) ? this.clearByUrlPattern(/^\/libraries\/[^/]+\/personalized/) : 0 + let removedPersonalized = 0 + let removedRecentEpisodes = 0 + if (this.modelsInvalidatingPersonalized.has(modelName)) { + removedPersonalized = this.clearByUrlPattern(/^\/libraries\/[^/]+\/personalized/) + removedRecentEpisodes = this.clearByUrlPattern(/^\/libraries\/[^/]+\/recent-episodes/) + } const removedMe = this.modelsInvalidatingMe.has(modelName) ? this.clearByUrlPattern(/^\/me(\/|\?|$)/) : 0 - Logger.debug( - `[ApiCacheManager] ${modelName}.${hook}: cleared user-progress cache slices (personalized=${removedPersonalized}, me=${removedMe})` - ) + Logger.debug(`[ApiCacheManager] ${modelName}.${hook}: cleared user-progress cache slices (personalized=${removedPersonalized}, recentEpisodes=${removedRecentEpisodes}, me=${removedMe})`) } clear(model, hook) { From 62d7097e23dbabb6da3def612622ed4b6a3123af Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Apr 2026 16:51:39 -0500 Subject: [PATCH 251/285] Add ApiCacheManager test for should remove recent-episodes cache entries --- test/server/managers/ApiCacheManager.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/server/managers/ApiCacheManager.test.js b/test/server/managers/ApiCacheManager.test.js index 19bbeecf6..2e9ad5b77 100644 --- a/test/server/managers/ApiCacheManager.test.js +++ b/test/server/managers/ApiCacheManager.test.js @@ -1,6 +1,7 @@ // Import dependencies and modules for testing const { expect } = require('chai') const sinon = require('sinon') +const { LRUCache } = require('lru-cache') const ApiCacheManager = require('../../../server/managers/ApiCacheManager') describe('ApiCacheManager', () => { @@ -94,4 +95,17 @@ describe('ApiCacheManager', () => { expect(res.originalSend.calledWith(body)).to.be.true }) }) + + describe('clear on mediaProgress', () => { + it('should remove recent-episodes cache entries', () => { + const key = JSON.stringify({ user: 'u', url: '/libraries/abc-123/recent-episodes?limit=50&page=0' }) + const cache = new LRUCache({ max: 10 }) + cache.set(key, { body: '[]', headers: {}, statusCode: 200 }) + const manager = new ApiCacheManager(cache) + + manager.clear({ name: 'mediaProgress' }, 'afterUpdate') + + expect(cache.get(key)).to.be.undefined + }) + }) }) From f8ef56c6bc3cfd8d45e72a0c3690a35273623d14 Mon Sep 17 00:00:00 2001 From: ugyes Date: Mon, 20 Apr 2026 18:22:36 +0200 Subject: [PATCH 252/285] 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/ --- client/strings/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/hu.json b/client/strings/hu.json index 90f58343a..4c36af869 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -16,7 +16,7 @@ "ButtonBrowseForFolder": "Mappa keresése", "ButtonCancel": "Mégse", "ButtonCancelEncode": "Kódolás megszakítása", - "ButtonChangeRootPassword": "Gyökérjelszó megváltoztatása", + "ButtonChangeRootPassword": "Root jelszó megváltoztatása", "ButtonCheckAndDownloadNewEpisodes": "Új epizódok ellenőrzése és letöltése", "ButtonChooseAFolder": "Válassz egy mappát", "ButtonChooseFiles": "Fájlok kiválasztása", From e79256d0fb00087a423a0e6f154fa8db867f88ea Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Wed, 22 Apr 2026 22:27:58 +0200 Subject: [PATCH 253/285] Translated using Weblate (Belarusian) 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/be/ --- client/strings/be.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index 43668acb5..b4e4df86a 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -1,6 +1,6 @@ { "ButtonAdd": "Дадаць", - "ButtonAddApiKey": "Дадаць API-ключ", + "ButtonAddApiKey": "Дадаць ключ API", "ButtonAddChapters": "Дадаць раздзелы", "ButtonAddDevice": "Дадаць прыладу", "ButtonAddLibrary": "Дадаць бібліятэку", @@ -121,7 +121,7 @@ "HeaderAccount": "Уліковы запіс", "HeaderAddCustomMetadataProvider": "Дадаванне карыстальніцкага пастаўшчыка метаданых", "HeaderAdvanced": "Дадаткова", - "HeaderApiKeys": "API-ключы", + "HeaderApiKeys": "Ключы API", "HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise", "HeaderAudioTracks": "Аўдыятрэкі", "HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг", @@ -166,7 +166,7 @@ "HeaderMetadataOrderOfPrecedence": "Парадак прыярытэту метаданых", "HeaderMetadataToEmbed": "Метаданыя для ўбудавання", "HeaderNewAccount": "Новы ўліковы запіс", - "HeaderNewApiKey": "Новы API-ключ", + "HeaderNewApiKey": "Новы ключ API", "HeaderNewLibrary": "Новая бібліятэка", "HeaderNotificationCreate": "Стварыць апавяшчэнне", "HeaderNotificationUpdate": "Абнавіць апавяшчэнне", @@ -212,7 +212,7 @@ "HeaderTableOfContents": "Змест", "HeaderTools": "Інструменты", "HeaderUpdateAccount": "Абнавіць уліковы запіс", - "HeaderUpdateApiKey": "Абнавіць API-ключ", + "HeaderUpdateApiKey": "Абнавіць ключ API", "HeaderUpdateAuthor": "Абнавіць аўтара", "HeaderUpdateDetails": "Абнавіць падрабязнасці", "HeaderUpdateLibrary": "Абнавіць бібліятэку", @@ -242,10 +242,10 @@ "LabelAllUsersExcludingGuests": "Усіх карыстальнікаў, акрамя гасцей", "LabelAllUsersIncludingGuests": "Усіх карыстальнікаў, уключаючы гасцей", "LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы", - "LabelApiKeyCreated": "API-ключ \"{0}\" паспяхова створаны.", - "LabelApiKeyCreatedDescription": "Пераканайцеся, што вы скапіявалі API-ключ зараз, бо паўторна яго ўбачыць не атрымаецца.", + "LabelApiKeyCreated": "Ключ API \"{0}\" паспяхова створаны.", + "LabelApiKeyCreatedDescription": "Абавязкова скапіюйце ключ API зараз, бо паўторна яго ўбачыць не атрымаецца.", "LabelApiKeyUser": "Дзейнічаць ад імя карыстальніка", - "LabelApiKeyUserDescription": "Гэты API-ключ будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.", + "LabelApiKeyUserDescription": "Гэты ключ API будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.", "LabelApiToken": "Токен API", "LabelAppend": "Дадаць", "LabelAudioBitrate": "Бітрэйт аўдыя (напрыклад, 128к)", @@ -884,7 +884,7 @@ "MessageRemoveEpisodes": "Выдаліць выпускі ({0})", "MessageRemoveFromPlayerQueue": "Выдаліць з чаргі прагравання", "MessageRemoveUserWarning": "Вы ўпэўнены, што хочаце назаўжды выдаліць карыстальніка \"{0}\"?", - "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на", + "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце функцыі і ўносьце свой уклад на", "MessageResetChaptersConfirm": "Вы ўпэўнены, што хочаце скінуць раздзелы і адрабіць зробленыя вамі змены?", "MessageRestoreBackupConfirm": "Вы ўпэўнены, што хочаце аднавіць рэзервовую копію, створаную", "MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама відарысы вокладкі ў /metadata/items і /metadata/authors.

Рэзервовыя копіі не змяняюць файлы ў папках бібліятэкі. Калі вы ўключылі налады сервера для захоўвання воклак і метаданых у папках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца.

Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.", From cbc103cf05956be3aff0ed5927039170d1f88dc3 Mon Sep 17 00:00:00 2001 From: Naoto Ishikawa <48012204+na3shkw@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:33:49 +0200 Subject: [PATCH 254/285] Translated using Weblate (Japanese) 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/ja/ --- client/strings/ja.json | 966 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 930 insertions(+), 36 deletions(-) diff --git a/client/strings/ja.json b/client/strings/ja.json index 0621614f4..95cfba2b8 100644 --- a/client/strings/ja.json +++ b/client/strings/ja.json @@ -16,7 +16,7 @@ "ButtonBrowseForFolder": "フォルダーを選択する", "ButtonCancel": "キャンセル", "ButtonCancelEncode": "エンコードを取り消す", - "ButtonChangeRootPassword": "Rootのパスワードを変更する", + "ButtonChangeRootPassword": "ルートのパスワードを変更する", "ButtonCheckAndDownloadNewEpisodes": "新しいエピソードを確認してダウンロード", "ButtonChooseAFolder": "フォルダーを選ぶ", "ButtonChooseFiles": "ファイルを選ぶ", @@ -29,7 +29,7 @@ "ButtonCreate": "作成", "ButtonCreateBackup": "バックアップを作成する", "ButtonDelete": "削除", - "ButtonDownloadQueue": "次に再生", + "ButtonDownloadQueue": "ダウンロードキュー", "ButtonEdit": "編集", "ButtonEditChapters": "チャプターの編集", "ButtonEditPodcast": "ポッドキャストの編集", @@ -52,9 +52,9 @@ "ButtonMatchAllAuthors": "すべての作者と紐付け", "ButtonMatchBooks": "本と紐付け", "ButtonNevermind": "中止", - "ButtonNext": "次", + "ButtonNext": "次へ", "ButtonNextChapter": "次のチャプター", - "ButtonNextItemInQueue": "キューの中の次のアイテム", + "ButtonNextItemInQueue": "キューにある次のアイテム", "ButtonOk": "はい", "ButtonOpenFeed": "フィードを開く", "ButtonOpenManager": "管理画面を開く", @@ -63,19 +63,19 @@ "ButtonPlayAll": "全て再生", "ButtonPlaying": "プレイ中", "ButtonPlaylists": "プレイリスト", - "ButtonPrevious": "先", + "ButtonPrevious": "前へ", "ButtonPreviousChapter": "前のチャプター", "ButtonProbeAudioFile": "オーディオファイルを解析", "ButtonPurgeAllCache": "全てのキャッシュを削除", "ButtonPurgeItemsCache": "項目のキャッシュを削除", "ButtonQueueAddItem": "次に再生する", - "ButtonQueueRemoveItem": "次に再生から削除", + "ButtonQueueRemoveItem": "「次に再生」から削除", "ButtonQuickEmbed": "クイック埋め込み", "ButtonQuickEmbedMetadata": "メタデータの埋め込み", "ButtonQuickMatch": "クイックマッチ", "ButtonReScan": "再スキャン", "ButtonRead": "読む", - "ButtonReadLess": "閉じる", + "ButtonReadLess": "少なく表示", "ButtonReadMore": "もっと見る", "ButtonRefresh": "再読み込み", "ButtonRemove": "削除", @@ -83,7 +83,7 @@ "ButtonRemoveAllLibraryItems": "ライブラリーの項目を全て削除", "ButtonRemoveFromContinueListening": "「続きを聴く」から削除", "ButtonRemoveFromContinueReading": "「続きを読む」から削除", - "ButtonRemoveSeriesFromContinueSeries": "「シリーズを続く」からシリーズを削除", + "ButtonRemoveSeriesFromContinueSeries": "「シリーズを続ける」からシリーズを削除", "ButtonReset": "元に戻す", "ButtonResetToDefault": "デフォルトに戻す", "ButtonRestore": "復元", @@ -108,15 +108,15 @@ "ButtonTest": "テスト", "ButtonUnlinkOpenId": "OpenID 連携解除", "ButtonUpload": "アップロード", - "ButtonUploadBackup": "バックアップのアップロード", + "ButtonUploadBackup": "バックアップをアップロード", "ButtonUploadCover": "カバー画像をアップロード", "ButtonUploadOPMLFile": "OPMLファイルをアップロード", "ButtonUserDelete": "ユーザーを削除 {0}", - "ButtonUserEdit": "ユーザを編集 {0}", + "ButtonUserEdit": "ユーザーを編集 {0}", "ButtonViewAll": "すべて表示", "ButtonYes": "はい", "ErrorUploadFetchMetadataAPI": "メタデータの取得中にエラーが発生しました", - "ErrorUploadFetchMetadataNoResults": "メタデータ取得に失敗しました。タイトルや著者名を更新してください", + "ErrorUploadFetchMetadataNoResults": "メタデータの取得に失敗しました。タイトルや作者名を更新してください", "ErrorUploadLacksTitle": "タイトルは必須です", "HeaderAccount": "アカウント", "HeaderAddCustomMetadataProvider": "カスタムメタデータプロバイダーを追加", @@ -153,8 +153,8 @@ "HeaderLastListeningSession": "直近の再生セッション", "HeaderLatestEpisodes": "最新のエピソード", "HeaderLibraries": "ライブラリー", - "HeaderLibraryFiles": "ライブラリファイル", - "HeaderLibraryStats": "ライブラリ統計", + "HeaderLibraryFiles": "ライブラリーファイル", + "HeaderLibraryStats": "ライブラリー統計", "HeaderListeningSessions": "再生セッション", "HeaderListeningStats": "再生統計", "HeaderLogin": "ログイン", @@ -172,100 +172,994 @@ "HeaderNotificationUpdate": "通知を更新", "HeaderNotifications": "通知", "HeaderOpenIDConnectAuthentication": "OpenID Connect 認証", + "HeaderOpenListeningSessions": "再生中のセッション", "HeaderOpenRSSFeed": "RSS Feedを開く", + "HeaderOtherFiles": "その他のファイル", + "HeaderPasswordAuthentication": "パスワード認証", + "HeaderPermissions": "権限", + "HeaderPlayerQueue": "次に再生", "HeaderPlayerSettings": "プレーヤーの設定", "HeaderPlaylist": "プレイリスト", "HeaderPlaylistItems": "プレイリストアイテム", + "HeaderPodcastsToAdd": "追加するポッドキャスト", + "HeaderPresets": "プリセット", + "HeaderPreviewCover": "カバープレビュー", "HeaderRSSFeedGeneral": "RSS 詳細", "HeaderRSSFeedIsOpen": "RSSフィードが開いています", + "HeaderRSSFeeds": "RSSフィード", + "HeaderRemoveEpisode": "エピソードを削除", + "HeaderRemoveEpisodes": "{0}個のエピソードを削除", + "HeaderSavedMediaProgress": "保存済みメディア進捗", + "HeaderSchedule": "スケジュール", + "HeaderScheduleEpisodeDownloads": "エピソード自動ダウンロードスケジュール", + "HeaderScheduleLibraryScans": "ライブラリー自動スキャンスケジュール", + "HeaderSession": "セッション", + "HeaderSetBackupSchedule": "バックアップスケジュールの設定", "HeaderSettings": "設定", + "HeaderSettingsDisplay": "表示", + "HeaderSettingsExperimental": "実験的な機能", "HeaderSettingsGeneral": "一般", "HeaderSettingsScanner": "スキャナー", + "HeaderSettingsSecurity": "セキュリティ", + "HeaderSettingsWebClient": "Webクライアント", "HeaderSleepTimer": "スリープタイマー", - "HeaderStatsMinutesListeningChart": "過去7日間の視聴時間(分)", + "HeaderStatsLargestItems": "最大のアイテム", + "HeaderStatsLongestItems": "最長のアイテム(時間)", + "HeaderStatsMinutesListeningChart": "過去7日間のリスニング時間(分)", "HeaderStatsRecentSessions": "最近の再生履歴", + "HeaderStatsTop10Authors": "上位10作者", + "HeaderStatsTop5Genres": "上位5ジャンル", "HeaderTableOfContents": "目次", + "HeaderTools": "ツール", + "HeaderUpdateAccount": "アカウントの更新", + "HeaderUpdateApiKey": "APIキーの更新", + "HeaderUpdateAuthor": "作者の更新", + "HeaderUpdateDetails": "詳細の更新", + "HeaderUpdateLibrary": "ライブラリーの更新", + "HeaderUsers": "ユーザー", + "HeaderYearReview": "{0}年の振り返り", "HeaderYourStats": "再生統計", + "LabelAbridged": "要約版", + "LabelAbridgedChecked": "要約版(チェック済み)", + "LabelAbridgedUnchecked": "完全版(未チェック)", + "LabelAccessibleBy": "アクセス可能なユーザー", + "LabelAccountType": "アカウントの種類", + "LabelAccountTypeAdmin": "管理者", + "LabelAccountTypeGuest": "ゲスト", + "LabelAccountTypeUser": "ユーザー", + "LabelActivities": "アクティビティー", + "LabelActivity": "アクティビティー", + "LabelAddToCollection": "コレクションに追加", + "LabelAddToCollectionBatch": "コレクションに{0}冊追加", "LabelAddToPlaylist": "プレイリストの追加", + "LabelAddToPlaylistBatch": "プレイリストに{0}件追加", "LabelAddedAt": "追加日時", - "LabelAddedDate": "追加日時 ­­{0}", + "LabelAddedDate": "追加日時 {0}", + "LabelAdminUsersOnly": "管理者ユーザーのみ", "LabelAll": "すべて", - "LabelAuthor": "著者", - "LabelAuthorFirstLast": "著者(名 氏)", - "LabelAuthorLastFirst": "著者(氏 名)", - "LabelAuthors": "著者", + "LabelAllEpisodesDownloaded": "すべてのエピソードをダウンロード済み", + "LabelAllUsers": "すべてのユーザー", + "LabelAllUsersExcludingGuests": "ゲストを除くすべてのユーザー", + "LabelAllUsersIncludingGuests": "ゲストを含むすべてのユーザー", + "LabelAlreadyInYourLibrary": "すでにライブラリーにあります", + "LabelApiKeyCreated": "APIキー \"{0}\" が正常に作成されました。", + "LabelApiKeyCreatedDescription": "このAPIキーを今すぐコピーしてください。二度と確認できません。", + "LabelApiKeyUser": "ユーザーの代わりに動作", + "LabelApiKeyUserDescription": "このAPIキーは代わりに動作するユーザーと同じ権限を持ちます。ログではユーザーがリクエストを行っているのと同じように表示されます。", + "LabelApiToken": "APIトークン", + "LabelAppend": "追記", + "LabelAudioBitrate": "オーディオビットレート(例: 128k)", + "LabelAudioChannels": "オーディオチャンネル(1または2)", + "LabelAudioCodec": "オーディオコーデック", + "LabelAuthor": "作者", + "LabelAuthorFirstLast": "作者(名 氏)", + "LabelAuthorLastFirst": "作者(氏 名)", + "LabelAuthors": "作者", "LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード", - "LabelBooks": "ほん", - "LabelByAuthor": "著 {0}", + "LabelAutoFetchMetadata": "メタデータの自動取得", + "LabelAutoFetchMetadataHelp": "タイトル・作者・シリーズのメタデータを取得してアップロードを効率化します。アップロード後に追加のメタデータのマッチングが必要になることがあります。", + "LabelAutoLaunch": "自動起動", + "LabelAutoLaunchDescription": "ログインページにアクセスすると自動的に認証プロバイダーにリダイレクトします(手動上書きパス /login?autoLaunch=0)", + "LabelAutoRegister": "自動登録", + "LabelAutoRegisterDescription": "ログイン後に新しいユーザーを自動的に作成", + "LabelBackToUser": "ユーザーに戻る", + "LabelBackupAudioFiles": "オーディオファイルをバックアップ", + "LabelBackupLocation": "バックアップ場所", + "LabelBackupsEnableAutomaticBackups": "自動バックアップ", + "LabelBackupsEnableAutomaticBackupsHelp": "/metadata/backups に保存されたバックアップ", + "LabelBackupsMaxBackupSize": "最大バックアップサイズ(GB)(無制限は0)", + "LabelBackupsMaxBackupSizeHelp": "設定ミスを防ぐため、バックアップが設定サイズを超えると失敗します。", + "LabelBackupsNumberToKeep": "保持するバックアップ数", + "LabelBackupsNumberToKeepHelp": "一度に1つのバックアップのみ削除されます。すでにこれより多くのバックアップがある場合は手動で削除してください。", + "LabelBitrate": "ビットレート", + "LabelBonus": "ボーナス", + "LabelBooks": "本", + "LabelButtonText": "ボタンテキスト", + "LabelByAuthor": "by {0}", + "LabelChangePassword": "パスワードを変更", + "LabelChannels": "チャンネル", + "LabelChapterCount": "{0}チャプター", + "LabelChapterTitle": "チャプタータイトル", "LabelChapters": "チャプター", - "LabelClosePlayer": "プレイヤーを閉じる", + "LabelChaptersFound": "チャプターが見つかりました", + "LabelClickForMoreInfo": "クリックして詳細を表示", + "LabelClickToUseCurrentValue": "クリックして現在の値を使用", + "LabelClosePlayer": "プレーヤーを閉じる", + "LabelCodec": "コーデック", "LabelCollapseSeries": "シリーズを折りたたむ", + "LabelCollapseSubSeries": "サブシリーズを折りたたむ", + "LabelCollection": "コレクション", + "LabelCollections": "コレクション", "LabelComplete": "完了", - "LabelContinueListening": "続きから聞く", + "LabelConfirmPassword": "パスワード確認", + "LabelContinueListening": "続きから聴く", "LabelContinueReading": "続きを読む", - "LabelContinueSeries": "シリーズを続く", + "LabelContinueSeries": "シリーズを続ける", + "LabelCorsAllowed": "許可されたCORSオリジン", + "LabelCover": "カバー", + "LabelCoverImageURL": "カバー画像URL", + "LabelCoverProvider": "カバープロバイダー", + "LabelCreatedAt": "作成日時", + "LabelCronExpression": "Cron式", + "LabelCurrent": "現在", + "LabelCurrently": "現在:", + "LabelCustomCronExpression": "カスタムCron式:", + "LabelDatetime": "日時", + "LabelDays": "日", + "LabelDeleteFromFileSystemCheckbox": "ファイルシステムから削除(チェック解除するとデータベースからのみ削除)", "LabelDescription": "説明", + "LabelDeselectAll": "すべて解除", + "LabelDetectedPattern": "検出されたパターン:", + "LabelDevice": "デバイス", + "LabelDeviceInfo": "デバイス情報", + "LabelDeviceIsAvailableTo": "デバイスが使用可能な対象...", + "LabelDirectory": "ディレクトリ", + "LabelDiscFromFilename": "ファイル名からディスク", + "LabelDiscFromMetadata": "メタデータからディスク", "LabelDiscover": "おすすめ", "LabelDownload": "ダウンロード", + "LabelDownloadNEpisodes": "{0}エピソードをダウンロード", + "LabelDownloadable": "ダウンロード可能", "LabelDuration": "長さ", - "LabelEbook": "Eブック", - "LabelEbooks": "Eブック", + "LabelDurationComparisonExactMatch": "(完全一致)", + "LabelDurationComparisonLonger": "({0} 長い)", + "LabelDurationComparisonShorter": "({0} 短い)", + "LabelDurationFound": "見つかった長さ:", + "LabelEbook": "電子書籍", + "LabelEbooks": "電子書籍", + "LabelEdit": "編集", + "LabelEmail": "メール", + "LabelEmailSettingsFromAddress": "送信元アドレス", + "LabelEmailSettingsRejectUnauthorized": "未認証の証明書を拒否", + "LabelEmailSettingsRejectUnauthorizedHelp": "SSL証明書の検証を無効にすると、中間者攻撃などのセキュリティリスクにさらされる可能性があります。接続するメールサーバーを信頼している場合にのみ無効にしてください。", + "LabelEmailSettingsSecure": "セキュア", + "LabelEmailSettingsSecureHelp": "trueの場合、サーバーへの接続にTLSを使用します。falseの場合、サーバーがSTARTTLS拡張をサポートしていればTLSが使用されます。ポート465に接続する場合はtrueに設定してください。ポート587または25の場合はfalseのままにしてください。(nodemailer.com/smtp/#authentication より)", + "LabelEmailSettingsTestAddress": "テストアドレス", + "LabelEmbeddedCover": "埋め込みカバー", "LabelEnable": "有効", + "LabelEncodingBackupLocation": "元のオーディオファイルのバックアップは以下に保存されます:", + "LabelEncodingChaptersNotEmbedded": "マルチトラックのオーディオブックにはチャプターが埋め込まれません。", + "LabelEncodingClearItemCache": "定期的にアイテムキャッシュを削除してください。", + "LabelEncodingFinishedM4B": "完成したM4Bはオーディオブックフォルダーに配置されます:", + "LabelEncodingInfoEmbedded": "メタデータはオーディオブックフォルダー内のオーディオトラックに埋め込まれます。", + "LabelEncodingStartedNavigation": "タスクが開始されたら、このページから離れることができます。", + "LabelEncodingTimeWarning": "エンコードには最大30分かかる場合があります。", + "LabelEncodingWarningAdvancedSettings": "警告: ffmpegエンコードオプションに詳しくない場合はこれらの設定を変更しないでください。", + "LabelEncodingWatcherDisabled": "ウォッチャーを無効にしている場合は、後でこのオーディオブックを再スキャンする必要があります。", "LabelEnd": "終了", "LabelEndOfChapter": "チャプターの最後", "LabelEpisode": "エピソード", + "LabelEpisodeNotLinkedToRssFeed": "RSSフィードにリンクされていないエピソード", + "LabelEpisodeNumber": "エピソード#{0}", + "LabelEpisodeTitle": "エピソードタイトル", + "LabelEpisodeType": "エピソードタイプ", + "LabelEpisodeUrlFromRssFeed": "RSSフィードのエピソードURL", "LabelEpisodes": "エピソード", "LabelEpisodic": "エピソード", + "LabelExample": "例", + "LabelExpandSeries": "シリーズを展開", + "LabelExpandSubSeries": "サブシリーズを展開", + "LabelExpired": "期限切れ", + "LabelExpiresAt": "有効期限", + "LabelExpiresInSeconds": "有効期限(秒)", + "LabelExpiresNever": "なし", "LabelExplicit": "露骨な表現", - "LabelFeedURL": "Feed URL", + "LabelExplicitChecked": "露骨な表現あり(チェック済み)", + "LabelExplicitUnchecked": "露骨な表現なし(未チェック)", + "LabelExportOPML": "OPMLをエクスポート", + "LabelFeedURL": "フィードURL", + "LabelFetchingMetadata": "メタデータ取得中", "LabelFile": "ファイル", "LabelFileBirthtime": "ファイル作成日時", + "LabelFileBornDate": "{0}に生成", "LabelFileModified": "ファイル更新日時", + "LabelFileModifiedDate": "{0}に更新", "LabelFilename": "ファイル名", + "LabelFilterByUser": "ユーザーでフィルター", + "LabelFindEpisodes": "エピソードを検索", "LabelFinished": "完了", - "LabelFolder": "フォルダ", + "LabelFinishedDate": "{0}に完了", + "LabelFolder": "フォルダー", + "LabelFolders": "フォルダー", + "LabelFontBold": "太字", "LabelFontBoldness": "フォントの太さ", "LabelFontFamily": "フォントファミリー", + "LabelFontItalic": "斜体", "LabelFontScale": "フォントサイズ", + "LabelFontStrikethrough": "打消線", + "LabelFormat": "形式", + "LabelFull": "完全", "LabelGenre": "ジャンル", "LabelGenres": "ジャンル", - "LabelHasEbook": "eBookあり", - "LabelHasSupplementaryEbook": "付属eBookあり", + "LabelHardDeleteFile": "ファイルを完全削除", + "LabelHasEbook": "電子書籍あり", + "LabelHasSupplementaryEbook": "付属電子書籍あり", + "LabelHideSubtitles": "字幕を非表示", + "LabelHighestPriority": "最高優先度", "LabelHost": "ホスト", + "LabelHour": "時間", + "LabelHours": "時間", + "LabelIcon": "アイコン", + "LabelImageURLFromTheWeb": "ウェブからの画像URL", "LabelInProgress": "進行中", + "LabelIncludeInTracklist": "トラックリストに含める", "LabelIncomplete": "未完了", + "LabelInterval": "間隔", + "LabelIntervalCustomDailyWeekly": "カスタム(毎日/曜日指定)", + "LabelIntervalEvery12Hours": "12時間ごと", + "LabelIntervalEvery15Minutes": "15分ごと", + "LabelIntervalEvery2Hours": "2時間ごと", + "LabelIntervalEvery30Minutes": "30分ごと", + "LabelIntervalEvery6Hours": "6時間ごと", + "LabelIntervalEveryDay": "毎日", + "LabelIntervalEveryHour": "1時間ごと", + "LabelIntervalEveryMinute": "1分ごと", + "LabelInvert": "反転", + "LabelItem": "アイテム", + "LabelJumpBackwardAmount": "巻き戻し量", + "LabelJumpForwardAmount": "早送り量", "LabelLanguage": "言語", + "LabelLanguageDefaultServer": "デフォルトサーバー言語", "LabelLanguages": "言語", + "LabelLastBookAdded": "最後に追加された本", + "LabelLastBookUpdated": "最後に更新された本", + "LabelLastProgressDate": "最終進捗: {0}", + "LabelLastSeen": "最終確認", + "LabelLastTime": "終了時刻", + "LabelLastUpdate": "最終更新", "LabelLayout": "レイアウト", "LabelLayoutSinglePage": "単ページ", + "LabelLayoutSplitPage": "ページを分割", + "LabelLess": "少ない", + "LabelLibrariesAccessibleToUser": "ユーザーがアクセス可能なライブラリー", + "LabelLibrary": "ライブラリー", + "LabelLibraryFilterSublistEmpty": "{0}がない", + "LabelLibraryItem": "ライブラリーアイテム", + "LabelLibraryName": "ライブラリー名", + "LabelLibrarySortByProgress": "進捗: 最終更新", + "LabelLibrarySortByProgressFinished": "進捗: 完了", + "LabelLibrarySortByProgressStarted": "進捗: 開始済み", + "LabelLimit": "制限", "LabelLineSpacing": "行間", - "LabelListenAgain": "再度視聴", + "LabelListenAgain": "もう一度聴く", + "LabelLogLevelDebug": "デバッグ", + "LabelLogLevelInfo": "情報", + "LabelLogLevelWarn": "警告", + "LabelLookForNewEpisodesAfterDate": "この日以降の新しいエピソードを検索", + "LabelLowestPriority": "最低優先度", + "LabelMatchConfidence": "信頼度", + "LabelMatchExistingUsersBy": "既存ユーザーの照合方法", + "LabelMatchExistingUsersByDescription": "既存ユーザーの接続に使用されます。接続後はSSOプロバイダーの一意のIDでユーザーが照合されます", + "LabelMaxEpisodesToDownload": "最大ダウンロードエピソード数(無制限は0)。", + "LabelMaxEpisodesToDownloadPerCheck": "1回のチェックでダウンロードする最大の新着エピソード数", + "LabelMaxEpisodesToKeep": "保持する最大エピソード数", + "LabelMaxEpisodesToKeepHelp": "0は上限なし。新しいエピソードの自動ダウンロード後、X件以上のエピソードがある場合は最も古いエピソードを削除します。1回のダウンロードにつき1件のみ削除されます。", + "LabelMediaPlayer": "メディアプレーヤー", "LabelMediaType": "メディアの種類", + "LabelMetaTag": "メタタグ", + "LabelMetaTags": "メタタグ", + "LabelMetadataOrderOfPrecedenceDescription": "優先度の高いメタデータソースが優先度の低いメタデータソースを上書きします", + "LabelMetadataProvider": "メタデータプロバイダー", + "LabelMinute": "分", + "LabelMinutes": "分", + "LabelMissing": "消失", + "LabelMissingEbook": "電子書籍なし", + "LabelMissingSupplementaryEbook": "付属電子書籍なし", + "LabelMobileRedirectURIs": "許可されたモバイルリダイレクトURI", + "LabelMobileRedirectURIsDescription": "これはモバイルアプリの有効なリダイレクトURIのホワイトリストです。デフォルトは audiobookshelf://oauth です。削除するか、サードパーティーアプリ統合用の追加URIで補足できます。アスタリスク(*)を唯一のエントリとして使用すると、任意のURIが許可されます。", + "LabelMore": "多い", "LabelMoreInfo": "追加情報", - "LabelName": "名", + "LabelName": "名前", "LabelNarrator": "ナレーター", "LabelNarrators": "ナレーター", "LabelNew": "新しい", - "LabelNewPassword": "新しいのパスワード", - "LabelNewestAuthors": "最新の著者", + "LabelNewPassword": "新しいパスワード", + "LabelNewestAuthors": "最新の作者", "LabelNewestEpisodes": "最新エピソード", + "LabelNextBackupDate": "次回バックアップ日", + "LabelNextChapters": "次のチャプター:", + "LabelNextScheduledRun": "次のスケジュール実行", + "LabelNoApiKeys": "APIキーなし", + "LabelNoCustomMetadataProviders": "カスタムメタデータプロバイダーなし", + "LabelNoEpisodesSelected": "エピソードが選択されていません", + "LabelNotFinished": "未完了", + "LabelNotStarted": "未開始", + "LabelNotes": "ノート", + "LabelNotificationAppriseURL": "Apprise URL", + "LabelNotificationAvailableVariables": "利用可能な変数", + "LabelNotificationBodyTemplate": "本文テンプレート", + "LabelNotificationEvent": "通知イベント", + "LabelNotificationTitleTemplate": "タイトルテンプレート", + "LabelNotificationsMaxFailedAttempts": "最大失敗試行回数", + "LabelNotificationsMaxFailedAttemptsHelp": "この回数失敗すると通知は無効になります", + "LabelNotificationsMaxQueueSize": "通知イベントの最大キューサイズ", + "LabelNotificationsMaxQueueSizeHelp": "イベントは1秒に1回の発火に制限されます。キューが最大サイズの場合、イベントは無視されます。これは通知スパムを防ぎます。", + "LabelNumberOfBooks": "本の冊数", + "LabelNumberOfChapters": "チャプター数:", + "LabelNumberOfEpisodes": "エピソード数", + "LabelOpenIDAdvancedPermsClaimDescription": "管理者以外のロールに適用されるアプリケーション内のユーザーアクション用の高度な権限を含むOpenIDクレーム名(設定されている場合)。クレームがレスポンスに含まれていない場合、ABSへのアクセスは拒否されます。単一のオプションがない場合は false として扱われます。IDプロバイダーのクレームが期待される構造と一致することを確認してください:", + "LabelOpenIDClaims": "以下のオプションを空のままにすると、高度なグループと権限の割り当てが無効になり、自動的に「User」グループが割り当てられます。", + "LabelOpenIDGroupClaimDescription": "ユーザーのグループリストを含むOpenIDクレーム名。一般的には groups と呼ばれます。設定されている場合、アプリケーションはユーザーのグループメンバーシップに基づいて自動的にロールを割り当てます。クレームのグループ名が大文字小文字を区別せずに「admin」「user」「guest」である場合、対応するロールが割り当てられます。グループが一致しない場合、アクセスは拒否されます。", + "LabelOpenRSSFeed": "RSSフィードを開く", + "LabelOverwrite": "上書き", + "LabelPaginationPageXOfY": "{0}/{1}ページ", "LabelPassword": "パスワード", "LabelPath": "パス", + "LabelPermanent": "無期限", + "LabelPermissionsAccessAllLibraries": "すべてのライブラリーにアクセス可能", + "LabelPermissionsAccessAllTags": "すべてのタグにアクセス可能", + "LabelPermissionsAccessExplicitContent": "露骨なコンテンツにアクセス可能", + "LabelPermissionsCreateEreader": "電子書籍リーダー作成可能", + "LabelPermissionsDelete": "削除可能", + "LabelPermissionsDownload": "ダウンロード可能", + "LabelPermissionsUpdate": "更新可能", + "LabelPermissionsUpload": "アップロード可能", + "LabelPersonalYearReview": "個人の年間振り返り({0})", + "LabelPhotoPathURL": "写真パス/URL", + "LabelPlayMethod": "再生方法", + "LabelPlaybackRateIncrementDecrement": "再生速度の増減量", + "LabelPlayerChapterNumberMarker": "{0}/{1}", "LabelPlaylists": "プレイリスト", "LabelPodcast": "ポッドキャスト", + "LabelPodcastSearchRegion": "ポッドキャスト検索地域", + "LabelPodcastType": "ポッドキャストタイプ", "LabelPodcasts": "ポッドキャスト", + "LabelPort": "ポート", + "LabelPrefixesToIgnore": "無視するプレフィックス(大文字小文字を区別しない)", "LabelPreventIndexing": "フィードがiTunesおよびGoogleのポッドキャストディレクトリにインデックス登録されるのを防ぎます", + "LabelPrimaryEbook": "メイン電子書籍", + "LabelProgress": "進捗", + "LabelProvider": "プロバイダー", + "LabelProviderAuthorizationValue": "認証ヘッダーの値", + "LabelPubDate": "公開日", "LabelPublishYear": "公開年", + "LabelPublishedDate": "{0}発行", + "LabelPublishedDecade": "発行年代", + "LabelPublishedDecades": "発行年代", + "LabelPublisher": "出版社", + "LabelPublishers": "出版社", + "LabelRSSFeedCustomOwnerEmail": "カスタムオーナーメール", + "LabelRSSFeedCustomOwnerName": "カスタムオーナー名", + "LabelRSSFeedOpen": "RSSフィードを公開", + "LabelRSSFeedPreventIndexing": "インデックス化を防止", + "LabelRSSFeedSlug": "RSSフィードスラグ", + "LabelRSSFeedURL": "RSSフィードURL", + "LabelRandomly": "ランダム", + "LabelReAddSeriesToContinueListening": "シリーズを再追加して「続きを聴く」に戻す", + "LabelRead": "読む", + "LabelReadAgain": "もう一度読む", + "LabelReadEbookWithoutProgress": "進捗を記録せずに電子書籍を読む", + "LabelRecentSeries": "最近のシリーズ", + "LabelRecentlyAdded": "最近追加", + "LabelRecommended": "おすすめ", + "LabelRedo": "やり直す", + "LabelRegion": "地域", + "LabelReleaseDate": "リリース日", + "LabelRemoveAllMetadataAbs": "すべてのmetadata.absファイルを削除", + "LabelRemoveAllMetadataJson": "すべてのmetadata.jsonファイルを削除", + "LabelRemoveAudibleBranding": "Audibleのイントロとアウトロをチャプターから削除", + "LabelRemoveCover": "カバーを削除", + "LabelRemoveMetadataFile": "ライブラリーアイテムフォルダー内のメタデータファイルを削除", + "LabelRemoveMetadataFileHelp": "{0}フォルダー内のすべてのmetadata.jsonおよびmetadata.absファイルを削除します。", + "LabelRowsPerPage": "1ページあたりの行数", + "LabelSearchTerm": "検索ワード", + "LabelSearchTitle": "タイトルを検索", + "LabelSearchTitleOrASIN": "タイトルまたはASINを検索", + "LabelSeason": "シーズン", + "LabelSeasonNumber": "シーズン#{0}", + "LabelSelectAll": "すべて選択", + "LabelSelectAllEpisodes": "すべてのエピソードを選択", + "LabelSelectEpisodesShowing": "表示中の{0}件のエピソードを選択", + "LabelSelectUser": "ユーザーを選択", + "LabelSelectUsers": "ユーザーを選択", + "LabelSendEbookToDevice": "電子書籍を送信...", + "LabelSequence": "シーケンス", + "LabelSerial": "シリアル", + "LabelSeries": "シリーズ", + "LabelSeriesName": "シリーズ名", + "LabelSeriesProgress": "シリーズ進捗", + "LabelServerLogLevel": "サーバーログレベル", + "LabelServerYearReview": "サーバーの年間振り返り({0})", + "LabelSetEbookAsPrimary": "メインとして設定", + "LabelSetEbookAsSupplementary": "付属として設定", + "LabelSettingsAllowIframe": "iframeへの埋め込みを許可", + "LabelSettingsAudiobooksOnly": "オーディオブックのみ", + "LabelSettingsAudiobooksOnlyHelp": "この設定を有効にすると電子書籍ファイルは無視されます。ただしオーディオブックフォルダー内の電子書籍は付属電子書籍として設定されます", + "LabelSettingsBookshelfViewHelp": "木製の棚を備えたスキューモーフィックデザイン", + "LabelSettingsChromecastSupport": "Chromecastサポート", + "LabelSettingsDateFormat": "日付フォーマット", + "LabelSettingsEnableWatcher": "ライブラリーの変更を自動的に監視する", + "LabelSettingsEnableWatcherForLibrary": "ライブラリーの変更を自動的に監視する", + "LabelSettingsEnableWatcherHelp": "ファイルの変更が検出されたときにアイテムの自動追加/更新を有効にします。*サーバーの再起動が必要です", + "LabelSettingsEpubsAllowScriptedContent": "epubのスクリプトコンテンツを許可", + "LabelSettingsEpubsAllowScriptedContentHelp": "epubファイルがスクリプトを実行することを許可します。epubファイルのソースを信頼しない限り、この設定を無効のままにすることをお勧めします。", + "LabelSettingsExperimentalFeatures": "実験的機能", + "LabelSettingsExperimentalFeaturesHelp": "開発中の機能でご意見とテストのご協力をお願いしています。クリックしてGitHubディスカッションを開く。", "LabelSettingsFindCovers": "表紙を探す", - "LabelSettingsFindCoversHelp": "もしオーディオブックに表紙が埋め込まれていない、もしくは表紙画像がフォルダー内に見つからなければ、スキャナーは表紙を探そうとします。
注記: これによってスキャン時間が長くなります", + "LabelSettingsFindCoversHelp": "もしオーディオブックに表紙が埋め込まれていない、もしくは表紙画像がフォルダー内に見つからなければ、スキャナーは表紙を探そうとします。
注: これによってスキャン時間が長くなります", + "LabelSettingsHideSingleBookSeries": "単一の本のシリーズを非表示", + "LabelSettingsHideSingleBookSeriesHelp": "単一の本を持つシリーズはシリーズページとホームページの棚から非表示になります。", + "LabelSettingsHomePageBookshelfView": "ホームページは本棚ビューを使用", + "LabelSettingsLibraryBookshelfView": "ライブラリーは本棚ビューを使用", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "完了率が次より大きい", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "残り時間が次より少ない(秒)", + "LabelSettingsLibraryMarkAsFinishedWhen": "メディアアイテムを完了としてマークする条件", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "「シリーズを続ける」で前の本をスキップ", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "「シリーズを続ける」のホームページ棚は、1冊以上完了していて進行中の本がないシリーズの最初の未開始の本を表示します。この設定を有効にすると、最初の未開始の本ではなく、最後に完了した本からシリーズが継続されます。", "LabelSettingsParseSubtitles": "サブタイトルを抽出する", "LabelSettingsParseSubtitlesHelp": "オーディオブックのフォルダー名からサブタイトルを抽出します。
サブタイトルは \"-\" で区切ってください
例: \"本のタイトル - ここにサブタイトル\" という名前だと \"ここにサブタイトル\" というサブタイトルになります", "LabelSettingsPreferMatchedMetadata": "一致したメタデータを優先する", "LabelSettingsPreferMatchedMetadataHelp": "クイックマッチを使用する時、一致したデータは書籍の詳細を上書きします。デフォルトでは、埋まっていない項目のみ入力されます。", + "LabelSettingsSkipMatchingBooksWithASIN": "ASINを持つ本のマッチングをスキップ", + "LabelSettingsSkipMatchingBooksWithISBN": "ISBNを持つ本のマッチングをスキップ", "LabelSettingsSortingIgnorePrefixes": "並び替えでプレフィックスを無視する", "LabelSettingsSortingIgnorePrefixesHelp": "例: プレフィックス \"the\" の付いた本のタイトル \"The Book Title\" は \"Book Title, The\" として並び替えられます", + "LabelSettingsSquareBookCovers": "正方形の本のカバーを使用", + "LabelSettingsSquareBookCoversHelp": "標準の1.6:1のカバーより正方形のカバーを優先", "LabelSettingsStoreCoversWithItem": "表紙を項目と一緒に保存する", "LabelSettingsStoreCoversWithItemHelp": "デフォルトでは表紙は /metadata/items に保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます。\"cover\" という名前のファイル一つのみが保持されます", "LabelSettingsStoreMetadataWithItem": "メタデータを項目と一緒に保存する", - "LabelSettingsStoreMetadataWithItemHelp": "デフォルトではメタデータは/metadata/itemsに保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます" + "LabelSettingsStoreMetadataWithItemHelp": "デフォルトではメタデータは/metadata/itemsに保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます", + "LabelSettingsTimeFormat": "時刻フォーマット", + "LabelShare": "共有", + "LabelShareDownloadableHelp": "共有リンクを持つユーザーがライブラリーアイテムのzipファイルをダウンロードできるようにします。", + "LabelShareOpen": "共有を公開", + "LabelShareURL": "共有URL", + "LabelShowAll": "すべて表示", + "LabelShowSeconds": "秒を表示", + "LabelShowSubtitles": "字幕を表示", + "LabelSize": "サイズ", + "LabelSleepTimer": "スリープタイマー", + "LabelSlug": "スラグ", + "LabelSortAscending": "昇順", + "LabelSortDescending": "降順", + "LabelSortPubDate": "公開日でソート", + "LabelStart": "開始", + "LabelStartTime": "開始時刻", + "LabelStarted": "開始済み", + "LabelStartedAt": "開始日時", + "LabelStartedDate": "{0}に開始", + "LabelStatsAudioTracks": "オーディオトラック", + "LabelStatsAuthors": "作者", + "LabelStatsBestDay": "日ごとの最長", + "LabelStatsDailyAverage": "日ごとの平均", + "LabelStatsDays": "日数", + "LabelStatsDaysListened": "聴いた日数", + "LabelStatsHours": "時間", + "LabelStatsInARow": "連続", + "LabelStatsItemsFinished": "完了したアイテム", + "LabelStatsItemsInLibrary": "ライブラリー内のアイテム", + "LabelStatsMinutes": "分", + "LabelStatsMinutesListening": "聴いた時間(分)", + "LabelStatsOverallDays": "総日数", + "LabelStatsOverallHours": "総時間", + "LabelStatsWeekListening": "週間リスニング", + "LabelSubtitle": "サブタイトル", + "LabelSupportedFileTypes": "サポートされているファイルタイプ", + "LabelTag": "タグ", + "LabelTags": "タグ", + "LabelTagsAccessibleToUser": "ユーザーがアクセス可能なタグ", + "LabelTagsNotAccessibleToUser": "ユーザーがアクセスできないタグ", + "LabelTasks": "実行中のタスク", + "LabelTextEditorBulletedList": "箇条書きリスト", + "LabelTextEditorLink": "リンク", + "LabelTextEditorNumberedList": "番号付きリスト", + "LabelTextEditorUnlink": "リンク解除", + "LabelTheme": "テーマ", + "LabelThemeDark": "ダーク", + "LabelThemeLight": "ライト", + "LabelThemeSepia": "セピア", + "LabelTimeBase": "タイムベース", + "LabelTimeDurationXHours": "{0}時間", + "LabelTimeDurationXMinutes": "{0}分", + "LabelTimeDurationXSeconds": "{0}秒", + "LabelTimeInMinutes": "時間(分単位)", + "LabelTimeLeft": "残り{0}", + "LabelTimeListened": "リスニング時間", + "LabelTimeListenedToday": "本日のリスニング時間", + "LabelTimeRemaining": "残り{0}", + "LabelTimeToShift": "シフトする時間(秒単位)", + "LabelTitle": "タイトル", + "LabelToolsEmbedMetadata": "メタデータの埋め込み", + "LabelToolsEmbedMetadataDescription": "カバー画像とチャプターを含むメタデータをオーディオファイルに埋め込みます。", + "LabelToolsM4bEncoder": "M4Bエンコーダー", + "LabelToolsMakeM4b": "M4Bオーディオブックファイルを作成", + "LabelToolsMakeM4bDescription": "メタデータ、カバー画像、チャプターを埋め込んだ.M4Bオーディオブックファイルを生成します。", + "LabelToolsSplitM4b": "M4BをMP3に分割", + "LabelToolsSplitM4bDescription": "メタデータ、カバー画像、チャプターを埋め込んだチャプター分割MP3ファイルをM4Bから作成します。", + "LabelTotalDuration": "総再生時間", + "LabelTotalTimeListened": "合計リスニング時間", + "LabelTrackFromFilename": "ファイル名からトラック", + "LabelTrackFromMetadata": "メタデータからトラック", + "LabelTracks": "トラック", + "LabelTracksMultiTrack": "マルチトラック", + "LabelTracksNone": "トラックなし", + "LabelTracksSingleTrack": "シングルトラック", + "LabelTrailer": "トレーラー", + "LabelType": "タイプ", + "LabelUnabridged": "完全版", + "LabelUndo": "元に戻す", + "LabelUnknown": "不明", + "LabelUnknownPublishDate": "公開日不明", + "LabelUpdateCover": "カバーを更新", + "LabelUpdateCoverHelp": "マッチが見つかった場合、選択した本の既存のカバーの上書きを許可", + "LabelUpdateDetails": "詳細を更新", + "LabelUpdateDetailsHelp": "マッチが見つかった場合、選択した本の既存の詳細の上書きを許可", + "LabelUpdatedAt": "更新日時", + "LabelUploaderDragAndDrop": "ファイルまたはフォルダーをドラッグ&ドロップ", + "LabelUploaderDragAndDropFilesOnly": "ファイルをドラッグ&ドロップ", + "LabelUploaderDropFiles": "ファイルをドロップ", + "LabelUploaderItemFetchMetadataHelp": "タイトル、作者、シリーズを自動取得", + "LabelUseAdvancedOptions": "詳細オプションを使用", + "LabelUseChapterTrack": "チャプタートラックを使用", + "LabelUseFullTrack": "フルトラックを使用", + "LabelUseZeroForUnlimited": "0で無制限", + "LabelUser": "ユーザー", + "LabelUsername": "ユーザー名", + "LabelValue": "値", + "LabelVersion": "バージョン", + "LabelViewBookmarks": "ブックマークを表示", + "LabelViewChapters": "チャプターを表示", + "LabelViewPlayerSettings": "プレーヤー設定を表示", + "LabelViewQueue": "「次に再生」を表示", + "LabelVolume": "ボリューム", + "LabelWebRedirectURLsDescription": "ログイン後にウェブアプリへのリダイレクトを許可するため、OAuthプロバイダーでこれらのURLを認可してください:", + "LabelWebRedirectURLsSubfolder": "リダイレクトURLのサブフォルダー", + "LabelWeekdaysToRun": "実行する曜日", + "LabelXBooks": "{0}冊の本", + "LabelXItems": "{0}件のアイテム", + "LabelYearReviewHide": "年間振り返りを非表示", + "LabelYearReviewShow": "年間振り返りを表示", + "LabelYourAudiobookDuration": "オーディオブックの再生時間", + "LabelYourBookmarks": "ブックマーク", + "LabelYourPlaylists": "プレイリスト", + "LabelYourProgress": "進捗状況", + "MessageAddToPlayerQueue": "「次に再生」に追加", + "MessageAppriseDescription": "この機能を使用するには、Apprise API のインスタンスを起動するか、同じリクエストを処理できるAPIが必要です。
Apprise API URLは通知を送信するための完全なURLパスである必要があります。例えば、APIインスタンスが http://192.168.1.1:8337 で起動している場合は http://192.168.1.1:8337/notify を入力してください。", + "MessageAsinCheck": "AmazonではなくAudibleの正しいリージョンのASINを使用していることを確認してください。", + "MessageAuthenticationLegacyTokenWarning": "レガシーAPIトークンは将来削除されます。代わりに APIキー を使用してください。", + "MessageAuthenticationOIDCChangesRestart": "OIDCの変更を適用するには、保存後にサーバーを再起動してください。", + "MessageAuthenticationSecurityMessage": "セキュリティのため認証が改善されました。すべてのユーザーは再ログインが必要です。", + "MessageBackupsDescription": "バックアップにはユーザー、ユーザーの進捗状況、ライブラリーアイテムの詳細、サーバー設定、および /metadata/items/metadata/authors に保存されている画像が含まれます。バックアップにはライブラリーフォルダーに保存されているファイルは含まれません。", + "MessageBackupsLocationEditNote": "注: バックアップ場所を更新しても、既存のバックアップは移動または変更されません", + "MessageBackupsLocationNoEditNote": "注: バックアップ場所は環境変数で設定されており、ここでは変更できません。", + "MessageBackupsLocationPathEmpty": "バックアップ場所のパスは空にできません", + "MessageBatchEditPopulateMapDetailsAllHelp": "すべてのアイテムのデータで有効なフィールドを設定します。複数の値があるフィールドはマージされます", + "MessageBatchEditPopulateMapDetailsItemHelp": "このアイテムのデータで有効なマップ詳細フィールドを設定します", + "MessageBatchQuickMatchDescription": "クイックマッチは選択したアイテムの不足しているカバーとメタデータを追加しようとします。クイックマッチが既存のカバーやメタデータを上書きできるようにするには、以下のオプションを有効にしてください。", + "MessageBookshelfNoCollections": "まだコレクションを作成していません", + "MessageBookshelfNoCollectionsHelp": "コレクションは公開されています。ライブラリーにアクセスできるすべてのユーザーが見ることができます。", + "MessageBookshelfNoRSSFeeds": "公開中のRSSフィードはありません", + "MessageBookshelfNoResultsForFilter": "フィルター \"{0}: {1}\" に一致する結果がありません", + "MessageBookshelfNoResultsForQuery": "クエリに一致する結果がありません", + "MessageBookshelfNoSeries": "シリーズがありません", + "MessageBulkChapterPattern": "この番号パターンでいくつのチャプターを追加しますか?", + "MessageChapterEndIsAfter": "チャプターの終了がオーディオブックの末尾を超えています", + "MessageChapterErrorFirstNotZero": "最初のチャプターは0から始まる必要があります", + "MessageChapterErrorStartGteDuration": "開始時間はオーディオブックの長さより短くなければなりません", + "MessageChapterErrorStartLtPrev": "開始時間は前のチャプターの開始時間以上でなければなりません", + "MessageChapterStartIsAfter": "チャプターの開始がオーディオブックの末尾を超えています", + "MessageChaptersNotFound": "チャプターが見つかりません", + "MessageCheckingCron": "cronを確認中...", + "MessageConfirmCloseFeed": "このフィードを閉じてもよろしいですか?", + "MessageConfirmDeleteApiKey": "APIキー \"{0}\" を削除してもよろしいですか?", + "MessageConfirmDeleteBackup": "{0} のバックアップを削除してもよろしいですか?", + "MessageConfirmDeleteDevice": "電子書籍リーダー端末 \"{0}\" を削除してもよろしいですか?", + "MessageConfirmDeleteFile": "ファイルシステムからファイルを削除します。よろしいですか?", + "MessageConfirmDeleteLibrary": "ライブラリー \"{0}\" を完全に削除してもよろしいですか?", + "MessageConfirmDeleteLibraryItem": "データベースとファイルシステムからライブラリーアイテムを削除します。よろしいですか?", + "MessageConfirmDeleteLibraryItems": "データベースとファイルシステムから {0}件のライブラリーアイテムを削除します。よろしいですか?", + "MessageConfirmDeleteMetadataProvider": "カスタムメタデータプロバイダー \"{0}\" を削除してもよろしいですか?", + "MessageConfirmDeleteNotification": "この通知を削除してもよろしいですか?", + "MessageConfirmDeleteSession": "このセッションを削除してもよろしいですか?", + "MessageConfirmEmbedMetadataInAudioFiles": "{0}件のオーディオファイルにメタデータを埋め込んでもよろしいですか?", + "MessageConfirmForceReScan": "強制的に再スキャンしてもよろしいですか?", + "MessageConfirmMarkAllEpisodesFinished": "すべてのエピソードを完了済みにしてもよろしいですか?", + "MessageConfirmMarkAllEpisodesNotFinished": "すべてのエピソードを未完了にしてもよろしいですか?", + "MessageConfirmMarkItemFinished": "\"{0}\" を完了済みにしてもよろしいですか?", + "MessageConfirmMarkItemNotFinished": "\"{0}\" を未完了にしてもよろしいですか?", + "MessageConfirmMarkSeriesFinished": "このシリーズのすべての本を完了済みにしてもよろしいですか?", + "MessageConfirmMarkSeriesNotFinished": "このシリーズのすべての本を未完了にしてもよろしいですか?", + "MessageConfirmNotificationTestTrigger": "テストデータでこの通知をトリガーしてもよろしいですか?", + "MessageConfirmPurgeCache": "キャッシュの削除により /metadata/cache ディレクトリ全体が削除されます。

キャッシュディレクトリを削除してもよろしいですか?", + "MessageConfirmPurgeItemsCache": "アイテムキャッシュの削除により /metadata/cache/items ディレクトリ全体が削除されます。
よろしいですか?", + "MessageConfirmQuickEmbed": "警告!クイック埋め込みはオーディオファイルをバックアップしません。オーディオファイルのバックアップがあることを確認してください。

続行しますか?", + "MessageConfirmQuickMatchEpisodes": "エピソードのクイックマッチは一致が見つかった場合に詳細を上書きします。未マッチのエピソードのみ更新されます。よろしいですか?", + "MessageConfirmReScanLibraryItems": "{0}件のアイテムを再スキャンしてもよろしいですか?", + "MessageConfirmRemoveAllChapters": "すべてのチャプターを削除してもよろしいですか?", + "MessageConfirmRemoveAuthor": "作者 \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemoveCollection": "コレクション \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemoveEpisode": "エピソード \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemoveEpisodeNote": "注: 「ファイルも削除する」を有効にしない限り、オーディオファイルは削除されません", + "MessageConfirmRemoveEpisodes": "{0}件のエピソードを削除してもよろしいですか?", + "MessageConfirmRemoveListeningSessions": "{0}件のリスニングセッションを削除してもよろしいですか?", + "MessageConfirmRemoveMetadataFiles": "ライブラリーアイテムフォルダー内のすべての metadata.{0} ファイルを削除してもよろしいですか?", + "MessageConfirmRemoveNarrator": "ナレーター \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemovePlaylist": "プレイリスト \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRenameGenre": "すべてのアイテムのジャンル \"{0}\" を \"{1}\" に変更してもよろしいですか?", + "MessageConfirmRenameGenreMergeNote": "注: このジャンルはすでに存在するためマージされます。", + "MessageConfirmRenameGenreWarning": "警告!大文字・小文字が異なる類似したジャンル \"{0}\" がすでに存在します。", + "MessageConfirmRenameTag": "すべてのアイテムのタグ \"{0}\" を \"{1}\" に変更してもよろしいですか?", + "MessageConfirmRenameTagMergeNote": "注: このタグはすでに存在するためマージされます。", + "MessageConfirmRenameTagWarning": "警告!大文字・小文字が異なる類似したタグ \"{0}\" がすでに存在します。", + "MessageConfirmResetProgress": "進捗状況をリセットしてもよろしいですか?", + "MessageConfirmSendEbookToDevice": "{0} の電子書籍 \"{1}\" を端末 \"{2}\" に送信してもよろしいですか?", + "MessageConfirmUnlinkOpenId": "このユーザーをOpenIDからリンク解除してもよろしいですか?", + "MessageDaysListenedInTheLastYear": "過去1年間に {0}日間聴きました", + "MessageDownloadingEpisode": "エピソードをダウンロード中", + "MessageDragFilesIntoTrackOrder": "ファイルを正しいトラック順にドラッグしてください", + "MessageEmbedFailed": "埋め込み失敗!", + "MessageEmbedFinished": "埋め込み完了!", + "MessageEmbedQueue": "メタデータ埋め込みキューに追加されました(キュー内: {0}件)", + "MessageEpisodesQueuedForDownload": "{0}件のエピソードがダウンロードキューに追加されました", + "MessageEreaderDevices": "電子書籍を確実に配信するため、以下の各端末で上記のメールアドレスを送信元として許可する設定が必要になることがあります。", + "MessageFeedURLWillBe": "フィードURLは {0} になります", + "MessageFetching": "取得中...", + "MessageForceReScanDescription": "すべてのファイルを新規スキャンのように再スキャンします。オーディオファイルのID3タグ、OPFファイル、テキストファイルが新規として読み込まれます。", + "MessageHeatmapListeningTimeTooltip": "{1} に {0} のリスニング", + "MessageHeatmapNoListeningSessions": "{0} にリスニングセッションはありません", + "MessageImportantNotice": "重要なお知らせ!", + "MessageInsertChapterBelow": "下にチャプターを挿入", + "MessageInvalidAsin": "無効なASIN", + "MessageItemsSelected": "{0}件選択中", + "MessageItemsUpdated": "{0}件更新されました", + "MessageJoinUsOn": "参加する:", + "MessageLoading": "読み込み中...", + "MessageLoadingFolders": "フォルダーを読み込み中...", + "MessageLogsDescription": "ログは /metadata/logs にJSONファイルとして保存されます。クラッシュログは /metadata/logs/crash_logs.txt に保存されます。", + "MessageM4BFailed": "M4B変換失敗!", + "MessageM4BFinished": "M4B変換完了!", + "MessageMapChapterTitles": "タイムスタンプを調整せずにチャプタータイトルを既存のオーディオブックのチャプターに割り当てます", + "MessageMarkAllEpisodesFinished": "すべてのエピソードを完了済みにする", + "MessageMarkAllEpisodesNotFinished": "すべてのエピソードを未完了にする", + "MessageMarkAsFinished": "完了済みにする", + "MessageMarkAsNotFinished": "未完了にする", + "MessageMatchBooksDescription": "ライブラリー内の本を選択した検索プロバイダーの本と照合し、空の詳細とカバーアートを補完しようとします。既存の詳細は上書きしません。", + "MessageNoAudioTracks": "オーディオトラックなし", + "MessageNoAuthors": "作者なし", + "MessageNoBackups": "バックアップなし", + "MessageNoBookmarks": "ブックマークなし", + "MessageNoChapters": "チャプターなし", + "MessageNoCollections": "コレクションなし", + "MessageNoCoversFound": "カバーが見つかりません", + "MessageNoDescription": "説明なし", + "MessageNoDevices": "端末なし", + "MessageNoDownloadsInProgress": "現在進行中のダウンロードはありません", + "MessageNoDownloadsQueued": "ダウンロードキューが空です", + "MessageNoEpisodeMatchesFound": "エピソードの一致が見つかりません", + "MessageNoEpisodes": "エピソードなし", + "MessageNoFoldersAvailable": "利用可能なフォルダーがありません", + "MessageNoGenres": "ジャンルなし", + "MessageNoIssues": "問題なし", + "MessageNoItems": "アイテムなし", + "MessageNoItemsFound": "アイテムが見つかりません", + "MessageNoListeningSessions": "リスニングセッションなし", + "MessageNoLogs": "ログなし", + "MessageNoMediaProgress": "メディアの進捗状況なし", + "MessageNoNotifications": "通知なし", + "MessageNoPodcastFeed": "無効なポッドキャスト: フィードなし", + "MessageNoPodcastsFound": "ポッドキャストが見つかりません", + "MessageNoResults": "結果なし", + "MessageNoSearchResultsFor": "\"{0}\" の検索結果はありません", + "MessageNoSeries": "シリーズなし", + "MessageNoTags": "タグなし", + "MessageNoTasksRunning": "実行中のタスクなし", + "MessageNoUpdatesWereNecessary": "更新は不要でした", + "MessageNoUserPlaylists": "プレイリストがありません", + "MessageNoUserPlaylistsHelp": "プレイリストは非公開です。作成したユーザーのみが見ることができます。", + "MessageNotYetImplemented": "未実装", + "MessageOpmlPreviewNote": "注: これは解析されたOPMLファイルのプレビューです。実際のポッドキャストタイトルはRSSフィードから取得されます。", + "MessageOr": "または", + "MessagePauseChapter": "チャプターの再生を一時停止", + "MessagePlayChapter": "チャプターの最初から聴く", + "MessagePlaylistCreateFromCollection": "コレクションからプレイリストを作成", + "MessagePleaseWait": "しばらくお待ちください...", + "MessagePodcastHasNoRSSFeedForMatching": "このポッドキャストには照合に使用できるRSSフィードURLがありません", + "MessagePodcastSearchField": "検索キーワードまたはRSSフィードURLを入力", + "MessageQuickEmbedInProgress": "クイック埋め込みを実行中", + "MessageQuickEmbedQueue": "クイック埋め込みキューに追加されました(キュー内: {0}件)", + "MessageQuickMatchAllEpisodes": "すべてのエピソードをクイックマッチ", + "MessageQuickMatchDescription": "'{0}' の最初の一致結果でアイテムの空の詳細とカバーを設定します。サーバー設定の「一致したメタデータを優先」が有効な場合を除き、既存の詳細は上書きされません。", + "MessageRemoveChapter": "チャプターを削除", + "MessageRemoveEpisodes": "{0}件のエピソードを削除", + "MessageRemoveFromPlayerQueue": "「次に再生」から削除", + "MessageRemoveUserWarning": "ユーザー \"{0}\" を完全に削除してもよろしいですか?", + "MessageReportBugsAndContribute": "バグ報告、機能リクエスト、コントリビューション:", + "MessageResetChaptersConfirm": "チャプターをリセットして変更を元に戻してもよろしいですか?", + "MessageRestoreBackupConfirm": "以下の日時に作成されたバックアップを復元してもよろしいですか:", + "MessageRestoreBackupWarning": "バックアップを復元すると、/config にあるデータベース全体と /metadata/items および /metadata/authors のカバー画像が上書きされます。

バックアップはライブラリーフォルダー内のファイルを変更しません。カバーアートとメタデータをライブラリーフォルダーに保存するサーバー設定を有効にしている場合、それらはバックアップされず上書きもされません。

サーバーを使用しているすべてのクライアントは自動的に更新されます。", + "MessageScheduleLibraryScanNote": "ほとんどのユーザーには、この機能を無効のままにして「ライブラリーの変更を自動的に監視する」設定を有効にしておくことをお勧めします。これによりライブラリーフォルダーの変更が自動的に検出されます。NFS などのファイルシステムで「ライブラリーの変更を自動的に監視する」が機能しない場合はこの機能を有効にしてください。", + "MessageScheduleRunEveryWeekdayAtTime": "毎{0} {1} に実行", + "MessageSearchResultsFor": "検索結果:", + "MessageSelected": "{0}件選択中", + "MessageSeriesSequenceCannotContainSpaces": "シリーズのシーケンスにスペースを含めることはできません", + "MessageServerCouldNotBeReached": "サーバーに接続できませんでした", + "MessageSetChaptersFromTracksDescription": "各オーディオファイルをチャプターとして設定し、チャプタータイトルにはファイル名を使用します", + "MessageShareExpirationWillBe": "有効期限は {0} になります", + "MessageShareExpiresIn": "{0} 後に期限切れ", + "MessageShareURLWillBe": "共有URLは {0} になります", + "MessageStartPlaybackAtTime": "\"{0}\" を {1} から再生しますか?", + "MessageTaskAudioFileNotWritable": "オーディオファイル \"{0}\" は書き込み不可です", + "MessageTaskCanceledByUser": "タスクがユーザーによりキャンセルされました", + "MessageTaskDownloadingEpisodeDescription": "エピソード \"{0}\" をダウンロード中", + "MessageTaskEmbeddingMetadata": "メタデータを埋め込み中", + "MessageTaskEmbeddingMetadataDescription": "オーディオブック \"{0}\" にメタデータを埋め込み中", + "MessageTaskEncodingM4b": "M4Bをエンコード中", + "MessageTaskEncodingM4bDescription": "オーディオブック \"{0}\" を単一のm4bファイルにエンコード中", + "MessageTaskFailed": "失敗", + "MessageTaskFailedToBackupAudioFile": "オーディオファイル \"{0}\" のバックアップに失敗しました", + "MessageTaskFailedToCreateCacheDirectory": "キャッシュディレクトリの作成に失敗しました", + "MessageTaskFailedToEmbedMetadataInFile": "ファイル \"{0}\" へのメタデータ埋め込みに失敗しました", + "MessageTaskFailedToMergeAudioFiles": "オーディオファイルのマージに失敗しました", + "MessageTaskFailedToMoveM4bFile": "m4bファイルの移動に失敗しました", + "MessageTaskFailedToWriteMetadataFile": "メタデータファイルの書き込みに失敗しました", + "MessageTaskMatchingBooksInLibrary": "ライブラリー \"{0}\" の本を照合中", + "MessageTaskNoFilesToScan": "スキャンするファイルがありません", + "MessageTaskOpmlImport": "OPMLインポート", + "MessageTaskOpmlImportDescription": "{0}件のRSSフィードからポッドキャストを作成中", + "MessageTaskOpmlImportFeed": "OPMLインポートフィード", + "MessageTaskOpmlImportFeedDescription": "RSSフィード \"{0}\" をインポート中", + "MessageTaskOpmlImportFeedFailed": "ポッドキャストフィードの取得に失敗しました", + "MessageTaskOpmlImportFeedPodcastDescription": "ポッドキャスト \"{0}\" を作成中", + "MessageTaskOpmlImportFeedPodcastExists": "このパスにポッドキャストがすでに存在します", + "MessageTaskOpmlImportFeedPodcastFailed": "ポッドキャストの作成に失敗しました", + "MessageTaskOpmlImportFinished": "{0}件のポッドキャストを追加しました", + "MessageTaskOpmlParseFailed": "OPMLファイルの解析に失敗しました", + "MessageTaskOpmlParseFastFail": "無効なOPMLファイル: タグが見つからないか タグが見つかりません", + "MessageTaskOpmlParseNoneFound": "OPMLファイルにフィードが見つかりません", + "MessageTaskScanItemsAdded": "{0}件追加", + "MessageTaskScanItemsMissing": "{0}件不明", + "MessageTaskScanItemsUpdated": "{0}件更新", + "MessageTaskScanNoChangesNeeded": "変更は不要でした", + "MessageTaskScanningFileChanges": "\"{0}\" のファイル変更をスキャン中", + "MessageTaskScanningLibrary": "\"{0}\" ライブラリーをスキャン中", + "MessageTaskTargetDirectoryNotWritable": "ターゲットディレクトリは書き込み不可です", + "MessageThinking": "処理中...", + "MessageUploaderItemFailed": "アップロード失敗", + "MessageUploaderItemSuccess": "アップロード成功!", + "MessageUploading": "アップロード中...", + "MessageValidCronExpression": "有効なCron式です", + "MessageWatcherIsDisabledGlobally": "ウォッチャーはサーバー設定でグローバルに無効化されています", + "MessageXLibraryIsEmpty": "{0} ライブラリーは空です!", + "MessageYourAudiobookDurationIsLonger": "オーディオブックの長さが見つかった長さより長くなっています", + "MessageYourAudiobookDurationIsShorter": "オーディオブックの長さが見つかった長さより短くなっています", + "NoteChangeRootPassword": "ルートユーザーのみ空のパスワードを設定できます", + "NoteChapterEditorTimes": "注: 最初のチャプターの開始時間は必ず0:00のままにしてください。また最後のチャプターの開始時間はオーディオブックの長さを超えることはできません。", + "NoteFolderPicker": "注: すでにマッピング済みのフォルダーは表示されません", + "NoteRSSFeedPodcastAppsHttps": "警告: ほとんどのポッドキャストアプリはHTTPSのRSSフィードURLが必要です", + "NoteRSSFeedPodcastAppsPubDate": "警告: 1件以上のエピソードに公開日がありません。一部のポッドキャストアプリではこれが必要です。", + "NoteUploaderFoldersWithMediaFiles": "メディアファイルを含むフォルダーは別々のライブラリーアイテムとして処理されます。", + "NoteUploaderOnlyAudioFiles": "オーディオファイルのみをアップロードする場合、各オーディオファイルは個別のオーディオブックとして処理されます。", + "NoteUploaderUnsupportedFiles": "サポートされていないファイルは無視されます。フォルダーを選択またはドロップする場合、アイテムフォルダー内にない他のファイルは無視されます。", + "NotificationOnBackupCompletedDescription": "バックアップが完了したときにトリガーされます", + "NotificationOnBackupFailedDescription": "バックアップが失敗したときにトリガーされます", + "NotificationOnEpisodeDownloadedDescription": "ポッドキャストエピソードが自動ダウンロードされたときにトリガーされます", + "NotificationOnRSSFeedDisabledDescription": "失敗が多すぎたため自動エピソードダウンロードが無効になったときにトリガーされます", + "NotificationOnRSSFeedFailedDescription": "自動エピソードダウンロードのRSSフィードリクエストが失敗したときにトリガーされます", + "NotificationOnTestDescription": "通知システムのテスト用イベント", + "PlaceholderBulkChapterInput": "チャプタータイトルを入力するか、番号付けを使用(例: 「エピソード 1」「チャプター 10」「1.」)", + "PlaceholderNewCollection": "新しいコレクション名", + "PlaceholderNewFolderPath": "新しいフォルダーパス", + "PlaceholderNewPlaylist": "新しいプレイリスト名", + "PlaceholderSearch": "検索...", + "PlaceholderSearchEpisode": "エピソードを検索...", + "StatsAuthorsAdded": "追加された作者", + "StatsBooksAdded": "追加された本", + "StatsBooksAdditional": "その他の追加内容…", + "StatsBooksFinished": "完了した本", + "StatsBooksFinishedThisYear": "今年完了した本…", + "StatsBooksListenedTo": "聴いた本", + "StatsCollectionGrewTo": "あなたの本のコレクションが増えました…", + "StatsSessions": "セッション", + "StatsSpentListening": "リスニング時間", + "StatsTopAuthor": "トップ作者", + "StatsTopAuthors": "トップ作者", + "StatsTopGenre": "トップジャンル", + "StatsTopGenres": "トップジャンル", + "StatsTopMonth": "トップ月", + "StatsTopNarrator": "トップナレーター", + "StatsTopNarrators": "トップナレーター", + "StatsTotalDuration": "合計再生時間…", + "StatsYearInReview": "年間振り返り", + "ToastAccountUpdateSuccess": "アカウントを更新しました", + "ToastAppriseUrlRequired": "Apprise URLを入力してください", + "ToastAsinRequired": "ASINを入力してください", + "ToastAuthorImageRemoveSuccess": "作者の画像を削除しました", + "ToastAuthorNotFound": "作者「{0}」が見つかりません", + "ToastAuthorRemoveSuccess": "作者を削除しました", + "ToastAuthorSearchNotFound": "作者が見つかりません", + "ToastAuthorUpdateMerged": "作者をマージしました", + "ToastAuthorUpdateSuccess": "作者を更新しました", + "ToastAuthorUpdateSuccessNoImageFound": "作者を更新しました(画像が見つかりません)", + "ToastBackupAppliedSuccess": "バックアップを適用しました", + "ToastBackupCreateFailed": "バックアップの作成に失敗しました", + "ToastBackupCreateSuccess": "バックアップを作成しました", + "ToastBackupDeleteFailed": "バックアップの削除に失敗しました", + "ToastBackupDeleteSuccess": "バックアップを削除しました", + "ToastBackupInvalidMaxKeep": "保持するバックアップ数が無効です", + "ToastBackupInvalidMaxSize": "バックアップの最大サイズが無効です", + "ToastBackupRestoreFailed": "バックアップの復元に失敗しました", + "ToastBackupUploadFailed": "バックアップのアップロードに失敗しました", + "ToastBackupUploadSuccess": "バックアップをアップロードしました", + "ToastBatchApplyDetailsToItemsSuccess": "詳細をアイテムに適用しました", + "ToastBatchDeleteFailed": "一括削除に失敗しました", + "ToastBatchDeleteSuccess": "一括削除に成功しました", + "ToastBatchQuickMatchFailed": "一括クイックマッチに失敗しました!", + "ToastBatchQuickMatchStarted": "{0}冊の一括クイックマッチを開始しました!", + "ToastBatchUpdateFailed": "一括更新に失敗しました", + "ToastBatchUpdateSuccess": "一括更新に成功しました", + "ToastBookmarkCreateFailed": "ブックマークの作成に失敗しました", + "ToastBookmarkCreateSuccess": "ブックマークを追加しました", + "ToastBookmarkRemoveSuccess": "ブックマークを削除しました", + "ToastBulkChapterInvalidCount": "1〜150の数値を入力してください", + "ToastCachePurgeFailed": "キャッシュのクリアに失敗しました", + "ToastCachePurgeSuccess": "キャッシュをクリアしました", + "ToastChapterLocked": "チャプターはロックされています。", + "ToastChapterStartTimeAdjusted": "チャプター開始時間を{0}秒調整しました", + "ToastChaptersAllLocked": "すべてのチャプターがロックされています。時間をシフトするにはいくつかのチャプターのロックを解除してください。", + "ToastChaptersHaveErrors": "チャプターにエラーがあります", + "ToastChaptersInvalidShiftAmountLast": "シフト量が無効です。最後のチャプターの開始時間がこのオーディオブックの長さを超えてしまいます。", + "ToastChaptersInvalidShiftAmountStart": "シフト量が無効です。最初のチャプターがゼロまたは負の長さになり、2番目のチャプターに上書きされます。2番目のチャプターの開始時間を長くしてください。", + "ToastChaptersMustHaveTitles": "チャプターにはタイトルが必要です", + "ToastChaptersRemoved": "チャプターを削除しました", + "ToastChaptersUpdated": "チャプターを更新しました", + "ToastCollectionItemsAddFailed": "アイテムのコレクションへの追加に失敗しました", + "ToastCollectionRemoveSuccess": "コレクションを削除しました", + "ToastCollectionUpdateSuccess": "コレクションを更新しました", + "ToastConnectionNotAvailable": "接続が利用できません。後でもう一度お試しください", + "ToastCoverSearchFailed": "カバー検索に失敗しました", + "ToastCoverUpdateFailed": "カバーの更新に失敗しました", + "ToastDateTimeInvalidOrIncomplete": "日時が無効または不完全です", + "ToastDeleteFileFailed": "ファイルの削除に失敗しました", + "ToastDeleteFileSuccess": "ファイルを削除しました", + "ToastDeviceAddFailed": "デバイスの追加に失敗しました", + "ToastDeviceNameAlreadyExists": "その名前の電子書籍リーダー端末はすでに存在します", + "ToastDeviceTestEmailFailed": "テストメールの送信に失敗しました", + "ToastDeviceTestEmailSuccess": "テストメールを送信しました", + "ToastEmailSettingsUpdateSuccess": "メール設定を更新しました", + "ToastEncodeCancelFailed": "エンコードのキャンセルに失敗しました", + "ToastEncodeCancelSucces": "エンコードをキャンセルしました", + "ToastEpisodeDownloadQueueClearFailed": "ダウンロードキューのクリアに失敗しました", + "ToastEpisodeDownloadQueueClearSuccess": "エピソードのダウンロードキューをクリアしました", + "ToastEpisodeUpdateSuccess": "{0}個のエピソードを更新しました", + "ToastErrorCannotShare": "このデバイスではネイティブ共有ができません", + "ToastFailedToCreate": "作成に失敗しました", + "ToastFailedToDelete": "削除に失敗しました", + "ToastFailedToLoadData": "データの読み込みに失敗しました", + "ToastFailedToMatch": "マッチングに失敗しました", + "ToastFailedToShare": "共有に失敗しました", + "ToastFailedToUpdate": "更新に失敗しました", + "ToastInvalidImageUrl": "画像のURLが無効です", + "ToastInvalidMaxEpisodesToDownload": "ダウンロード可能な最大エピソード数が無効です", + "ToastInvalidUrl": "URLが無効です", + "ToastInvalidUrls": "1つ以上のURLが無効です", + "ToastItemCoverUpdateSuccess": "アイテムのカバーを更新しました", + "ToastItemDeletedFailed": "アイテムの削除に失敗しました", + "ToastItemDeletedSuccess": "アイテムを削除しました", + "ToastItemDetailsUpdateSuccess": "アイテムの詳細を更新しました", + "ToastItemMarkedAsFinishedFailed": "完了としてマークするのに失敗しました", + "ToastItemMarkedAsFinishedSuccess": "アイテムを完了としてマークしました", + "ToastItemMarkedAsNotFinishedFailed": "未完了としてマークするのに失敗しました", + "ToastItemMarkedAsNotFinishedSuccess": "アイテムを未完了としてマークしました", + "ToastItemUpdateSuccess": "アイテムを更新しました", + "ToastLibraryCreateFailed": "ライブラリーの作成に失敗しました", + "ToastLibraryCreateSuccess": "ライブラリー「{0}」を作成しました", + "ToastLibraryDeleteFailed": "ライブラリーの削除に失敗しました", + "ToastLibraryDeleteSuccess": "ライブラリーを削除しました", + "ToastLibraryScanFailedToStart": "スキャンの開始に失敗しました", + "ToastLibraryScanStarted": "ライブラリースキャンを開始しました", + "ToastLibraryUpdateSuccess": "ライブラリー「{0}」を更新しました", + "ToastMatchAllAuthorsFailed": "すべての作者のマッチングに失敗しました", + "ToastMetadataFilesRemovedError": "metadata.{0}ファイルの削除中にエラーが発生しました", + "ToastMetadataFilesRemovedNoneFound": "ライブラリー内にmetadata.{0}ファイルが見つかりません", + "ToastMetadataFilesRemovedNoneRemoved": "削除されたmetadata.{0}ファイルはありません", + "ToastMetadataFilesRemovedSuccess": "{0}個のmetadata.{1}ファイルを削除しました", + "ToastMustHaveAtLeastOnePath": "少なくとも1つのパスが必要です", + "ToastNameEmailRequired": "名前とメールアドレスが必要です", + "ToastNameRequired": "名前を入力してください", + "ToastNewApiKeyUserError": "ユーザーを選択してください", + "ToastNewEpisodesFound": "{0}個の新しいエピソードが見つかりました", + "ToastNewUserCreatedFailed": "アカウントの作成に失敗しました:「{0}」", + "ToastNewUserCreatedSuccess": "新しいアカウントを作成しました", + "ToastNewUserLibraryError": "少なくとも1つのライブラリーを選択してください", + "ToastNewUserPasswordError": "パスワードが必要です。ルートユーザーのみ空のパスワードを使用できます", + "ToastNewUserTagError": "少なくとも1つのタグを選択してください", + "ToastNewUserUsernameError": "ユーザー名を入力してください", + "ToastNoNewEpisodesFound": "新しいエピソードは見つかりません", + "ToastNoRSSFeed": "ポッドキャストにはRSSフィードがありません", + "ToastNoUpdatesNecessary": "更新の必要はありません", + "ToastNotificationCreateFailed": "通知の作成に失敗しました", + "ToastNotificationDeleteFailed": "通知の削除に失敗しました", + "ToastNotificationFailedMaximum": "最大失敗試行回数は0以上である必要があります", + "ToastNotificationQueueMaximum": "最大通知キューは0以上である必要があります", + "ToastNotificationSettingsUpdateSuccess": "通知設定を更新しました", + "ToastNotificationTestTriggerFailed": "テスト通知のトリガーに失敗しました", + "ToastNotificationTestTriggerSuccess": "テスト通知をトリガーしました", + "ToastNotificationUpdateSuccess": "通知を更新しました", + "ToastPlaylistCreateFailed": "プレイリストの作成に失敗しました", + "ToastPlaylistCreateSuccess": "プレイリストを作成しました", + "ToastPlaylistRemoveSuccess": "プレイリストを削除しました", + "ToastPlaylistUpdateSuccess": "プレイリストを更新しました", + "ToastPodcastCreateFailed": "ポッドキャストの作成に失敗しました", + "ToastPodcastCreateSuccess": "ポッドキャストを作成しました", + "ToastPodcastEpisodeUpdated": "エピソードを更新しました", + "ToastPodcastGetFeedFailed": "ポッドキャストフィードの取得に失敗しました", + "ToastPodcastNoEpisodesInFeed": "RSSフィードにエピソードが見つかりません", + "ToastPodcastNoRssFeed": "ポッドキャストにはRSSフィードがありません", + "ToastProgressIsNotBeingSynced": "進捗が同期されていません。再生を再開してください", + "ToastProviderCreatedFailed": "プロバイダーの追加に失敗しました", + "ToastProviderCreatedSuccess": "新しいプロバイダーを追加しました", + "ToastProviderNameAndUrlRequired": "名前とURLが必要です", + "ToastProviderRemoveSuccess": "プロバイダーを削除しました", + "ToastRSSFeedCloseFailed": "RSSフィードのクローズに失敗しました", + "ToastRSSFeedCloseSuccess": "RSSフィードをクローズしました", + "ToastRemoveFailed": "削除に失敗しました", + "ToastRemoveItemFromCollectionFailed": "コレクションからのアイテム削除に失敗しました", + "ToastRemoveItemFromCollectionSuccess": "アイテムをコレクションから削除しました", + "ToastRemoveItemsWithIssuesFailed": "問題のあるライブラリーアイテムの削除に失敗しました", + "ToastRemoveItemsWithIssuesSuccess": "問題のあるライブラリーアイテムを削除しました", + "ToastRenameFailed": "名前変更に失敗しました", + "ToastRescanFailed": "{0}の再スキャンに失敗しました", + "ToastRescanRemoved": "再スキャンが完了しました。アイテムが削除されました", + "ToastRescanUpToDate": "再スキャンが完了しました。アイテムは最新です", + "ToastRescanUpdated": "再スキャンが完了しました。アイテムが更新されました", + "ToastScanFailed": "ライブラリーアイテムのスキャンに失敗しました", + "ToastSelectAtLeastOneUser": "少なくとも1人のユーザーを選択してください", + "ToastSendEbookToDeviceFailed": "電子書籍のデバイスへの送信に失敗しました", + "ToastSendEbookToDeviceSuccess": "電子書籍をデバイス「{0}」に送信しました", + "ToastSeriesSubmitFailedSameName": "同じ名前のシリーズを2つ追加することはできません", + "ToastSeriesUpdateFailed": "シリーズの更新に失敗しました", + "ToastSeriesUpdateSuccess": "シリーズを更新しました", + "ToastServerSettingsUpdateSuccess": "サーバー設定を更新しました", + "ToastSessionCloseFailed": "セッションのクローズに失敗しました", + "ToastSessionDeleteFailed": "セッションの削除に失敗しました", + "ToastSessionDeleteSuccess": "セッションを削除しました", + "ToastSleepTimerDone": "スリープタイマーが終了しました... zZzzZz", + "ToastSlugMustChange": "スラグに無効な文字が含まれています", + "ToastSlugRequired": "スラグを入力してください", + "ToastSocketConnected": "ソケットが接続されました", + "ToastSocketDisconnected": "ソケットが切断されました", + "ToastSocketFailedToConnect": "ソケット接続に失敗しました", + "ToastSortingPrefixesEmptyError": "少なくとも1つのソート接頭辞が必要です", + "ToastSortingPrefixesUpdateSuccess": "ソート接頭辞を更新しました({0}アイテム)", + "ToastTitleRequired": "タイトルを入力してください", + "ToastUnknownError": "不明なエラー", + "ToastUnlinkOpenIdFailed": "OpenIDからのユーザーのリンク解除に失敗しました", + "ToastUnlinkOpenIdSuccess": "ユーザーをOpenIDからリンク解除しました", + "ToastUploaderFilepathExistsError": "ファイルパス「{0}」はサーバーにすでに存在しています", + "ToastUploaderItemExistsInSubdirectoryError": "アイテム「{0}」はアップロードパスのサブディレクトリーを使用しています。", + "ToastUserDeleteFailed": "ユーザーの削除に失敗しました", + "ToastUserDeleteSuccess": "ユーザーを削除しました", + "ToastUserPasswordChangeSuccess": "パスワードを変更しました", + "ToastUserPasswordMismatch": "パスワードが一致しません", + "ToastUserPasswordMustChange": "新しいパスワードは古いパスワードと同じにすることはできません", + "ToastUserRootRequireName": "ルートユーザー名を入力してください", + "TooltipAddChapters": "チャプターを追加", + "TooltipAddOneSecond": "1秒追加", + "TooltipAdjustChapterStart": "クリックして開始時刻を調整", + "TooltipLockAllChapters": "すべてのチャプターをロック", + "TooltipLockChapter": "チャプターをロック(Shift+クリックで範囲選択)", + "TooltipSubtractOneSecond": "1秒減算", + "TooltipUnlockAllChapters": "すべてのチャプターをロック解除", + "TooltipUnlockChapter": "チャプターをロック解除(Shift+クリックで範囲選択)" } From 5dc01261c13ad35f58f77de6c2218a9faf105050 Mon Sep 17 00:00:00 2001 From: LvanAlphen Date: Sun, 26 Apr 2026 20:18:36 +0200 Subject: [PATCH 255/285] Translated using Weblate (Dutch) 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/nl/ --- client/strings/nl.json | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/client/strings/nl.json b/client/strings/nl.json index 708ea7a97..41a5a814b 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -2,7 +2,7 @@ "ButtonAdd": "Toevoegen", "ButtonAddApiKey": "API Key toevoegen", "ButtonAddChapters": "Hoofdstukken toevoegen", - "ButtonAddDevice": "Toestel toevoegen", + "ButtonAddDevice": "Apparaat toevoegen", "ButtonAddLibrary": "Bibliotheek toevoegen", "ButtonAddPodcasts": "Podcasts toevoegen", "ButtonAddUser": "Gebruiker toevoegen", @@ -139,7 +139,7 @@ "HeaderCustomMetadataProviders": "Aangepaste Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download-wachtrij", - "HeaderEbookFiles": "Ebook bestanden", + "HeaderEbookFiles": "E-book bestanden", "HeaderEmail": "E-mail", "HeaderEmailSettings": "E-mail instellingen", "HeaderEpisodes": "Afleveringen", @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Boeken", "LabelButtonText": "Knop Tekst", - "LabelByAuthor": "Door {0}", + "LabelByAuthor": "door {0}", "LabelChangePassword": "Wachtwoord wijzigen", "LabelChannels": "Kanalen", "LabelChapterCount": "{0} Hoofdstukken", @@ -383,7 +383,7 @@ "LabelFolders": "Mappen", "LabelFontBold": "Vetgedrukt", "LabelFontBoldness": "Lettertype Dikte", - "LabelFontFamily": "Lettertypefamilie", + "LabelFontFamily": "Letterfamilie", "LabelFontItalic": "Cursief", "LabelFontScale": "Lettertype schaal", "LabelFontStrikethrough": "Doorgestreept", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Nee {0}", "LabelLibraryItem": "Bibliotheekonderdeel", "LabelLibraryName": "Bibliotheeknaam", - "LabelLibrarySortByProgress": "Voortuigang geüpdatet", - "LabelLibrarySortByProgressFinished": "Datum voltooid", - "LabelLibrarySortByProgressStarted": "Datum gestart", + "LabelLibrarySortByProgress": "Voortgang: Laatst geüpdatet", + "LabelLibrarySortByProgressFinished": "Voortgang: Voltooid", + "LabelLibrarySortByProgressStarted": "Voortgang: Gestart", "LabelLimit": "Limiet", "LabelLineSpacing": "Regelruimte", "LabelListenAgain": "Opnieuw Beluisteren", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken", "LabelSettingsChromecastSupport": "Chromecast ondersteuning", "LabelSettingsDateFormat": "Datumnotatie", - "LabelSettingsEnableWatcher": "Bibliotheken automatisch scannen op wijzigingen", - "LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch scannen op wijzigingen", + "LabelSettingsEnableWatcher": "Bibliotheken automatisch monitoren op wijzigingen", + "LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch monitoren op wijzigingen", "LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server", "LabelSettingsEpubsAllowScriptedContent": "Sta scripted content toe in epubs", "LabelSettingsEpubsAllowScriptedContentHelp": "Sta toe dat epub-bestanden scripts uitvoeren. Het wordt aanbevolen om deze instelling uitgeschakeld te houden, tenzij u de bron van de epub-bestanden vertrouwt.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Weet je zeker dat je de hoofdstukken wil resetten en de wijzigingen die je gemaakt hebt ongedaan wil maken?", "MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op", "MessageRestoreBackupWarning": "Een back-up herstellen zal de volledige database in /config en de omslagen in /metadata/items & /metadata/authors overschrijven.

Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om omslagen en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.

Alle apparaten die je server gebruiken, worden automatisch ververst.", - "MessageScheduleLibraryScanNote": "Voor de meeste gebruikers is het raadzaam om deze functie uitgeschakeld te laten en de folder watcher-instelling ingeschakeld te houden. De folder watcher detecteert automatisch wijzigingen in uw bibliotheekmappen. De folder watcher werkt niet voor elk bestandssysteem (zoals NFS), dus geplande bibliotheekscans kunnen in plaats daarvan worden gebruikt.", + "MessageScheduleLibraryScanNote": "Voor de meeste gebruikers is het aangeraden om deze functie uitgeschakeld te laten en de \"Bibliotheek automatisch monitoren op wijzigingen\" instelling ingeschakeld te houden - deze detecteert automatisch wijzigingen in uw bibliotheekmappen. Activeer deze instelling als \"Bibliotheek automatisch monitoren op wijzigingen\" niet werkt voor uw bestandssysteem (zoals NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Elke {0} uitvoeren op {1}", "MessageSearchResultsFor": "Zoekresultaten voor", "MessageSelected": "{0} geselecteerd", @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Item(s) toegevoegd aan collectie mislukt", "ToastCollectionRemoveSuccess": "Collectie verwijderd", "ToastCollectionUpdateSuccess": "Collectie bijgewerkt", + "ToastConnectionNotAvailable": "Verbinding niet beschikbaar. Gelieve later opnieuw te proberen", + "ToastCoverSearchFailed": "Omslag zoeken mislukt", "ToastCoverUpdateFailed": "Omslag bijwerken mislukt", "ToastDateTimeInvalidOrIncomplete": "Datum en tijd ongeldig of onvolledig", "ToastDeleteFileFailed": "Bestand verwijderen mislukt", From 47457ee1e76a3e073230dd95a0faafdb1b17508b Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 27 Apr 2026 16:51:34 -0500 Subject: [PATCH 256/285] Version bump v2.34.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 57d2da716..79ac53250 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.33.2", + "version": "2.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.33.2", + "version": "2.34.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index f747cf90f..dd0f3a0c9 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.33.2", + "version": "2.34.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 250390c67..8950b4903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.33.2", + "version": "2.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.33.2", + "version": "2.34.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 810863f97..10ba26f65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.33.2", + "version": "2.34.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From dc446862c1090cc3d22db554d42464f6b4b1c1ec Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 30 Apr 2026 16:08:24 -0500 Subject: [PATCH 257/285] Rename migration to v2.35.0 & merge master --- ...-last-refresh-token.js => v2.35.0-add-last-refresh-token.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename server/migrations/{v2.33.0-add-last-refresh-token.js => v2.35.0-add-last-refresh-token.js} (99%) diff --git a/server/migrations/v2.33.0-add-last-refresh-token.js b/server/migrations/v2.35.0-add-last-refresh-token.js similarity index 99% rename from server/migrations/v2.33.0-add-last-refresh-token.js rename to server/migrations/v2.35.0-add-last-refresh-token.js index 5b53c749a..0ad190e9a 100644 --- a/server/migrations/v2.33.0-add-last-refresh-token.js +++ b/server/migrations/v2.35.0-add-last-refresh-token.js @@ -7,7 +7,7 @@ * @property {MigrationContext} context - an object containing the migration context. */ -const migrationVersion = '2.33.0' +const migrationVersion = '2.35.0' const migrationName = `${migrationVersion}-add-last-refresh-token` const loggerPrefix = `[${migrationVersion} migration]` From 394280512941ec83a95e6a1429a8973d13454136 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 30 Apr 2026 16:25:43 -0500 Subject: [PATCH 258/285] Cleanup rotateTokensForSession --- server/auth/TokenManager.js | 46 +++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 7306411ef..57c57cac7 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -187,32 +187,35 @@ class TokenManager { * @returns {Promise<{ accessToken:string, refreshToken:string }>} */ async rotateTokensForSession(session, user, req, res, gracePeriod = true) { - // Generate new tokens + const previousRefreshToken = session.refreshToken const newAccessToken = this.generateTempAccessToken(user) let newRefreshToken = this.generateRefreshToken(user) + const newExpiresAt = new Date(Date.now() + this.RefreshTokenExpiry * 1000) + let lastRefreshToken = null + let lastRefreshTokenExpiresAt = null if (gracePeriod) { // Set grace period of old refresh token in case of race condition in token rotation. // This grace period may need to be longer if fetching the user data takes longer due to large progress objects - session.lastRefreshToken = session.refreshToken - session.lastRefreshTokenExpiresAt = new Date(Date.now() + 60 * 1000) // 1 minute grace period - } else { - // Do not set grace period of old refresh token, such as when specifically invalidating sessions for a user - session.lastRefreshToken = null - session.lastRefreshTokenExpiresAt = null + lastRefreshToken = previousRefreshToken + lastRefreshTokenExpiresAt = new Date(Date.now() + 60 * 1000) // 1 minute grace period } - // Update the session with the new refresh token and expiration - session.refreshToken = newRefreshToken - session.expiresAt = new Date(Date.now() + this.RefreshTokenExpiry * 1000) - - // Only update the session if the refresh token hasn't changed since we originally read it - const [numUpdated] = await Database.sessionModel.update(session, { - where: { - id: session.id, - refreshToken: session.lastRefreshToken + // Only update if this session row still has the refresh token we read + const [numUpdated] = await Database.sessionModel.update( + { + refreshToken: newRefreshToken, + expiresAt: newExpiresAt, + lastRefreshToken, + lastRefreshTokenExpiresAt + }, + { + where: { + id: session.id, + refreshToken: previousRefreshToken + } } - }) + ) if (numUpdated === 0) { Logger.debug(`[TokenManager] Race condition in rotateTokensForSession for user ${user.id}, getting new token`) @@ -220,6 +223,15 @@ class TokenManager { const updatedSession = await Database.sessionModel.findOne({ where: { id: session.id } }) newRefreshToken = updatedSession.refreshToken + session.refreshToken = updatedSession.refreshToken + session.expiresAt = updatedSession.expiresAt + session.lastRefreshToken = updatedSession.lastRefreshToken + session.lastRefreshTokenExpiresAt = updatedSession.lastRefreshTokenExpiresAt + } else { + session.refreshToken = newRefreshToken + session.expiresAt = newExpiresAt + session.lastRefreshToken = lastRefreshToken + session.lastRefreshTokenExpiresAt = lastRefreshTokenExpiresAt } // Set new refresh token cookie From 40869bcf39f5894281c501bc059932d15ee6f6ed Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 1 May 2026 22:22:09 -0400 Subject: [PATCH 259/285] fix: set correct Content-Type for RSS feed audio files Express's mime package does not recognize .m4b, causing it to fall back to application/octet-stream. This reuses the existing getAudioMimeTypeFromExtname utility (already applied to the download endpoint) to set the correct audio/mp4 header before sendFile. Fixes #5041 --- server/managers/RssFeedManager.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index c4681bdc2..a066a0d32 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -2,6 +2,7 @@ const { Request, Response } = require('express') const Path = require('path') const Logger = require('../Logger') +const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') @@ -216,6 +217,11 @@ class RssFeedManager { res.sendStatus(404) return } + // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available + const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(episodePath)) + if (audioMimeType) { + res.setHeader('Content-Type', audioMimeType) + } res.sendFile(episodePath) } From 47ea6b50922e310acf523dbfaa4abd2f43d61940 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 5 May 2026 17:18:49 -0500 Subject: [PATCH 260/285] Update book/podcast scanner to sanitize description pulled from metadata --- server/scanner/BookScanner.js | 5 +++++ server/scanner/PodcastScanner.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index a1e7ff507..ac93c6379 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -7,6 +7,7 @@ const parseNameString = require('../utils/parsers/parseNameString') const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata') const globals = require('../utils/globals') const { readTextFile, filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') +const htmlSanitizer = require('../utils/htmlSanitizer') const AudioFileScanner = require('./AudioFileScanner') const Database = require('../Database') @@ -688,6 +689,10 @@ class BookScanner { bookMetadata.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title) + if (typeof bookMetadata.description === 'string' && bookMetadata.description) { + bookMetadata.description = htmlSanitizer.sanitize(bookMetadata.description) + } + return bookMetadata } diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index c9569c3ad..6ab2d332c 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -11,6 +11,7 @@ const LibraryFile = require('../objects/files/LibraryFile') const fsExtra = require('../libs/fsExtra') const PodcastEpisode = require('../models/PodcastEpisode') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') +const htmlSanitizer = require('../utils/htmlSanitizer') /** * Metadata for podcasts pulled from files @@ -398,6 +399,10 @@ class PodcastScanner { podcastMetadata.titleIgnorePrefix = getTitleIgnorePrefix(podcastMetadata.title) + if (typeof podcastMetadata.description === 'string' && podcastMetadata.description) { + podcastMetadata.description = htmlSanitizer.sanitize(podcastMetadata.description) + } + return podcastMetadata } From b0aaa246603df147f2a81b91153c9ed1144e599d Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 12 May 2026 16:57:28 -0500 Subject: [PATCH 261/285] Update socket events to check client is admin & validate log level --- server/SocketAuthority.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index ad55f6605..a54231d75 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -3,6 +3,7 @@ const Logger = require('./Logger') const Database = require('./Database') const TokenManager = require('./auth/TokenManager') const CoverSearchManager = require('./managers/CoverSearchManager') +const { LogLevel } = require('./utils/constants') /** * @typedef SocketClient @@ -85,6 +86,14 @@ class SocketAuthority { } } + requireAdminSocket(socket, eventName) { + const client = this.clients[socket.id] + if (client?.user?.isAdminOrUp) return true + + Logger.warn(`[SocketAuthority] Unauthorized ${eventName} socket event from socket ${socket.id}`) + return false + } + /** * Emits event with library item to all clients that can access the library item * Note: Emits toOldJSONExpanded() @@ -179,14 +188,25 @@ class SocketAuthority { socket.on('auth', (token) => this.authenticateSocket(socket, token)) // Scanning - socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId)) + socket.on('cancel_scan', (libraryId) => { + if (!this.requireAdminSocket(socket, 'cancel_scan')) return + 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('set_log_listener', (level) => { + if (!this.requireAdminSocket(socket, 'set_log_listener')) return + + if (!Number.isInteger(level) || !Object.values(LogLevel).includes(level)) { + Logger.warn(`[SocketAuthority] Invalid set_log_listener level from socket ${socket.id}`) + return + } + Logger.addSocketListener(socket, level) + }) socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) // Sent automatically from socket.io clients From eee377e0817d99f66b1b2da08ee7c366bfd606ee Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 13 May 2026 16:23:26 -0500 Subject: [PATCH 262/285] Cleanup TokenManager logs --- server/auth/TokenManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 5efeb7a64..fc25140ac 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -299,7 +299,7 @@ class TokenManager { }) if (!session) { - Logger.error(`[TokenManager] Failed to refresh token. Session not found for refresh token: ${refreshToken}`) + Logger.error(`[TokenManager] Failed to refresh token. Session not found`) return { error: 'Invalid refresh token' } @@ -389,7 +389,7 @@ class TokenManager { return newTokens.accessToken } else { - Logger.error(`[TokenManager] No session found to rotate tokens for refresh token ${currentRefreshToken}`) + Logger.error(`[TokenManager] No session found to rotate tokens`) } } @@ -413,7 +413,7 @@ class TokenManager { try { const numDeleted = await Database.sessionModel.destroy({ where: { refreshToken: refreshToken } }) - Logger.info(`[TokenManager] Refresh token ${refreshToken} invalidated, ${numDeleted} sessions deleted`) + Logger.info(`[TokenManager] Refresh token invalidated, ${numDeleted} sessions deleted`) return true } catch (error) { Logger.error(`[TokenManager] Error invalidating refresh token: ${error.message}`) From c010f0e1ebb2cba5a743ca935c52160558cc74d5 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 15 May 2026 13:13:14 -0500 Subject: [PATCH 263/285] Fix android device sdkVersion not handling it using number type, causing android session device names to show as iOS --- server/objects/DeviceInfo.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/objects/DeviceInfo.js b/server/objects/DeviceInfo.js index 22ebfbea4..68237f0fb 100644 --- a/server/objects/DeviceInfo.js +++ b/server/objects/DeviceInfo.js @@ -96,7 +96,12 @@ class DeviceInfo { this.clientVersion = stripAllTags(clientDeviceInfo?.clientVersion) || serverVersion this.manufacturer = stripAllTags(clientDeviceInfo?.manufacturer) || null this.model = stripAllTags(clientDeviceInfo?.model) || null - this.sdkVersion = stripAllTags(clientDeviceInfo?.sdkVersion) || null + + if (typeof clientDeviceInfo?.sdkVersion === 'number') { + this.sdkVersion = clientDeviceInfo.sdkVersion.toString() + } else { + this.sdkVersion = stripAllTags(clientDeviceInfo?.sdkVersion) || null + } this.clientName = stripAllTags(clientDeviceInfo?.clientName) || null if (this.sdkVersion) { From 6d5d89429d2e4c796540adcd8ec7ff260235403d Mon Sep 17 00:00:00 2001 From: Mateusz Lesiak Date: Wed, 29 Apr 2026 13:42:37 +0200 Subject: [PATCH 264/285] Translated using Weblate (Polish) Currently translated at 99.8% (1161 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/ --- client/strings/pl.json | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/client/strings/pl.json b/client/strings/pl.json index d1bc6c062..f04e61c46 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -951,6 +951,11 @@ "NoteUploaderFoldersWithMediaFiles": "Foldery z plikami multimedialnymi będą traktowane jako osobne elementy w bibliotece.", "NoteUploaderOnlyAudioFiles": "Jeśli przesyłasz tylko pliki audio, każdy plik audio będzie traktowany jako osobny audiobook.", "NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.", + "NotificationOnBackupCompletedDescription": "Wyzwalane po zakończeniu tworzenia kopii zapasowej", + "NotificationOnBackupFailedDescription": "Wyzwalane w przypadku gdy stworzenie kopii zapasowej rzuci błąd", + "NotificationOnEpisodeDownloadedDescription": "Wyzwalane, gdy odcinek podcastu zostanie automatycznie pobrany", + "NotificationOnRSSFeedDisabledDescription": "Wyzwalane, gdy automatyczne pobieranie odcinków jest wyłączone z powodu zbyt wielu nieudanych prób", + "NotificationOnRSSFeedFailedDescription": "Wyzwalane, gdy żądanie kanału RSS dotyczące automatycznego pobrania odcinka nie powiedzie się", "NotificationOnTestDescription": "Zdarzenie używane do testowania systemu powiadomień", "PlaceholderBulkChapterInput": "Wpisz tytuł rozdziału lub użyj numeracji (np. „Odcinek 1”, „Rozdział 10”, „1.”)", "PlaceholderNewCollection": "Nowa nazwa kolekcji", @@ -960,6 +965,7 @@ "PlaceholderSearchEpisode": "Szukanie odcinka..", "StatsAuthorsAdded": "dodano autorów", "StatsBooksAdded": "dodano książki", + "StatsBooksAdditional": "Niektóre dodatki obejmują…", "StatsBooksFinished": "ukończone książki", "StatsBooksFinishedThisYear": "Wybrane książki ukończone w tym roku…", "StatsBooksListenedTo": "książki wysłuchane", @@ -976,6 +982,7 @@ "StatsTotalDuration": "O sumarycznej długości…", "StatsYearInReview": "PRZEGLĄD ROKU", "ToastAccountUpdateSuccess": "Zaktualizowano konto", + "ToastAppriseUrlRequired": "Należy wprowadzić adres URL Apprise", "ToastAsinRequired": "ASIN jest wymagany", "ToastAuthorImageRemoveSuccess": "Zdjęcie autora usunięte", "ToastAuthorNotFound": "Autor \"{0}\" nie został znaleziony", @@ -994,8 +1001,11 @@ "ToastBackupRestoreFailed": "Nie udało się przywrócić kopii zapasowej", "ToastBackupUploadFailed": "Nie udało się przesłać kopii zapasowej", "ToastBackupUploadSuccess": "Kopia zapasowa została przesłana", + "ToastBatchApplyDetailsToItemsSuccess": "Szczegóły zastosowane do elementów", "ToastBatchDeleteFailed": "Usuwanie zbiorcze nie powiodło się", "ToastBatchDeleteSuccess": "Usuwanie zbiorcze powiodło się", + "ToastBatchQuickMatchFailed": "Szybkie dopasowanie partii nie powiodło się!", + "ToastBatchQuickMatchStarted": "Rozpoczęto partię szybkiego dopasowania {0} książek!", "ToastBatchUpdateFailed": "Aktualizacja zbiorcza nie powiodła się", "ToastBatchUpdateSuccess": "Aktualizacja zbiorcza powiodła się", "ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki", @@ -1033,7 +1043,14 @@ "ToastEpisodeDownloadQueueClearSuccess": "Wyczyszczono kolejkę epizodów do ściągnięcia", "ToastEpisodeUpdateSuccess": "Zaktualizowano {0} odcinków", "ToastErrorCannotShare": "Nie można udostępniać natywnie na tym urządzeniu.", + "ToastFailedToCreate": "Nie udało się utworzyć", + "ToastFailedToDelete": "Nie udało się usunąć", + "ToastFailedToLoadData": "Nie udało się załadować danych", + "ToastFailedToMatch": "Nie udało się dopasować", + "ToastFailedToShare": "Nie udało się udostępnić", + "ToastFailedToUpdate": "Nie udało się zaktualizować", "ToastInvalidImageUrl": "Nieprawidłowy URL obrazu", + "ToastInvalidMaxEpisodesToDownload": "Nieprawidłowa maksymalna liczba odcinków do pobrania", "ToastInvalidUrl": "Nieprawidłowy URL", "ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe", "ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę", @@ -1044,6 +1061,7 @@ "ToastItemMarkedAsFinishedSuccess": "Pozycja oznaczona jako ukończona", "ToastItemMarkedAsNotFinishedFailed": "Oznaczenie pozycji jako ukończonej nie powiodło się", "ToastItemMarkedAsNotFinishedSuccess": "Pozycja oznaczona jako nieukończona", + "ToastItemUpdateSuccess": "Element zaktualizowany", "ToastLibraryCreateFailed": "Nie udało się utworzyć biblioteki", "ToastLibraryCreateSuccess": "Biblioteka \"{0}\" stworzona", "ToastLibraryDeleteFailed": "Nie udało się usunąć biblioteki", @@ -1052,6 +1070,10 @@ "ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki", "ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji", "ToastMatchAllAuthorsFailed": "Nie udało się dopasować wszystkich autorów", + "ToastMetadataFilesRemovedError": "Błąd podczas usuwania metadata.{0} plików", + "ToastMetadataFilesRemovedNoneFound": "Nie znaleziono metadata.{0} plików w bibliotece", + "ToastMetadataFilesRemovedNoneRemoved": "Nie usunięto żadnego metadata.{0} pliku", + "ToastMetadataFilesRemovedSuccess": "{0} metadata.{0} plików usunięto", "ToastMustHaveAtLeastOnePath": "Musi mieć przynajmniej jedną ścieżkę", "ToastNameEmailRequired": "Nazwa i email są wymagane", "ToastNameRequired": "Imię jest wymagane", @@ -1065,7 +1087,15 @@ "ToastNewUserUsernameError": "Wprowadź nazwę użytkownika", "ToastNoNewEpisodesFound": "Nie znaleziono nowych odcinków", "ToastNoRSSFeed": "Podcast nie posiada RSS Feed", + "ToastNoUpdatesNecessary": "Brak konieczności aktualizacji", + "ToastNotificationCreateFailed": "Nie udało się utworzyć powiadomienia", + "ToastNotificationDeleteFailed": "Nie udało się usunąć powiadomienia", "ToastNotificationFailedMaximum": "Maks. ilość nieudanych prób musi być >= 0", + "ToastNotificationQueueMaximum": "Maksymalna liczba powiadomień w kolejce musi być >= 0", + "ToastNotificationSettingsUpdateSuccess": "Zaktualizowano ustawienia powiadomień", + "ToastNotificationTestTriggerFailed": "Nie udało się wywołać powiadomienia testowego", + "ToastNotificationTestTriggerSuccess": "Wyzwolono powiadomienie testowe", + "ToastNotificationUpdateSuccess": "Powiadomienie zaktualizowane", "ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty", "ToastPlaylistCreateSuccess": "Playlista utworzona", "ToastPlaylistRemoveSuccess": "Playlista usunięta", @@ -1073,8 +1103,17 @@ "ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu", "ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony", "ToastPodcastEpisodeUpdated": "Zaktualizowano odcinki", + "ToastPodcastGetFeedFailed": "Nie udało się pobrać kanału podcastu", + "ToastPodcastNoEpisodesInFeed": "Nie znaleziono żadnych odcinków w kanale RSS", + "ToastPodcastNoRssFeed": "Podcast nie ma kanału RSS", + "ToastProgressIsNotBeingSynced": "Postęp nie jest synchronizowany, uruchom ponownie odtwarzanie", + "ToastProviderCreatedFailed": "Nie udało się dodać dostawcy", + "ToastProviderCreatedSuccess": "Dodano nowego dostawcę", + "ToastProviderNameAndUrlRequired": "Wymagane jest podanie nazwy i adresu URL", + "ToastProviderRemoveSuccess": "Dostawca usunięty", "ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się", "ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się", + "ToastRemoveFailed": "Nie udało się usunąć", "ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji", "ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji", "ToastRemoveItemsWithIssuesFailed": "Nie udało się usunąć wadliwych elementów z biblioteki", @@ -1096,16 +1135,25 @@ "ToastSessionDeleteFailed": "Nie udało się usunąć sesji", "ToastSessionDeleteSuccess": "Sesja usunięta", "ToastSleepTimerDone": "Słodkich snów... zZzzZz", + "ToastSlugMustChange": "Slug zawiera nieprawidłowe znaki", + "ToastSlugRequired": "Slug jest wymagany", "ToastSocketConnected": "Nawiązano połączenie z serwerem", "ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte", "ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się", + "ToastSortingPrefixesEmptyError": "Musi mieć co najmniej 1 prefiks sortowania", + "ToastSortingPrefixesUpdateSuccess": "Zaktualizowano prefiksy sortowania ({0} elementów)", "ToastTitleRequired": "Tytuł jest wymagany", "ToastUnknownError": "Nieznany błąd", "ToastUnlinkOpenIdFailed": "Nie udało się odpiąć użytkownika z OpenID", "ToastUnlinkOpenIdSuccess": "Użytkownik odpięty z OpenID", "ToastUploaderFilepathExistsError": "Ścieżka \"{0}\" już istnieje na serwerze", + "ToastUploaderItemExistsInSubdirectoryError": "Element \"{0}\" używa podkatalogu ścieżki przesyłania.", "ToastUserDeleteFailed": "Nie udało się usunąć użytkownika", "ToastUserDeleteSuccess": "Użytkownik usunięty", + "ToastUserPasswordChangeSuccess": "Hasło zostało pomyślnie zmienione", + "ToastUserPasswordMismatch": "Hasła nie są zgodne", + "ToastUserPasswordMustChange": "Nowe hasło nie może być takie samo jak stare hasło", + "ToastUserRootRequireName": "Należy wprowadzić nazwę użytkownika root", "TooltipAddChapters": "Dodaj rozdział(y)", "TooltipAddOneSecond": "Dodaj sekundę", "TooltipAdjustChapterStart": "Kliknij, aby skorygować czas początkowy", From 3b025076e8c692dda3f9740592b2512be7e6111a Mon Sep 17 00:00:00 2001 From: d0nizam Date: Wed, 29 Apr 2026 17:36:49 +0200 Subject: [PATCH 265/285] Translated using Weblate (Bulgarian) 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/bg/ --- client/strings/bg.json | 105 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/client/strings/bg.json b/client/strings/bg.json index 460f0ff83..c34188a54 100644 --- a/client/strings/bg.json +++ b/client/strings/bg.json @@ -752,7 +752,7 @@ "MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове", "MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Няма резултати от заявката", - "MessageBookshelfNoSeries": "Нямаш сеЗЙ", + "MessageBookshelfNoSeries": "Нямате поредица", "MessageBulkChapterPattern": "Колко глави искате да добавите, използвайки тази схема за номериране?", "MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига", "MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0", @@ -1018,18 +1018,50 @@ "ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди", "ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.", "ToastChaptersHaveErrors": "Главите имат грешки", + "ToastChaptersInvalidShiftAmountLast": "Невалидно време за преместване. Началният час на последната глава ще превиши общата продължителност на аудиокнигата.", + "ToastChaptersInvalidShiftAmountStart": "Невалидно време за преместване. Първата глава ще има нулева или отрицателна дължина и ще бъде презаписана от втората глава. Увеличете началното време на втората глава.", "ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия", + "ToastChaptersRemoved": "Главите са премахнати", + "ToastChaptersUpdated": "Главите са актуализирани", + "ToastCollectionItemsAddFailed": "Неуспешно добавяне на елемент(и) към колекцията", "ToastCollectionRemoveSuccess": "Колекцията е премахната", "ToastCollectionUpdateSuccess": "Колекцията е обновена", + "ToastConnectionNotAvailable": "Няма връзка. Моля, опитайте отново по-късно", + "ToastCoverSearchFailed": "Търсенето на корица е неуспешно", + "ToastCoverUpdateFailed": "Обновяването на корицата е неуспешно", + "ToastDateTimeInvalidOrIncomplete": "Датата и часът са невалидни или непълни", "ToastDeleteFileFailed": "Неуспешно изтриване на файла", "ToastDeleteFileSuccess": "Успешно изтриване на файла", + "ToastDeviceAddFailed": "Неуспешно добавяне на устройство", + "ToastDeviceNameAlreadyExists": "Вече съществува четец с това име", + "ToastDeviceTestEmailFailed": "Неуспешно изпращане на тестов имейл", + "ToastDeviceTestEmailSuccess": "Тестовият имейл е изпратен", + "ToastEmailSettingsUpdateSuccess": "Имейл настройките са актуализирани", + "ToastEncodeCancelFailed": "Неуспешно отменяне на кодирането", + "ToastEncodeCancelSucces": "Кодирането е отменено", + "ToastEpisodeDownloadQueueClearFailed": "Неуспешно изчистване на опашката", + "ToastEpisodeDownloadQueueClearSuccess": "Опашката за изтегляне на епизоди е изчистена", + "ToastEpisodeUpdateSuccess": "{0} епизода са актуализирани", + "ToastErrorCannotShare": "Не може да се споделя директно от това устройство", + "ToastFailedToCreate": "Неуспешно създаване", + "ToastFailedToDelete": "Неуспешно изтриване", "ToastFailedToLoadData": "Неуспешно зареждане на данни", + "ToastFailedToMatch": "Неуспешно съвпадение", + "ToastFailedToShare": "Неуспешно споделяне", + "ToastFailedToUpdate": "Неуспешно актуализиране", + "ToastInvalidImageUrl": "Невалиден URL адрес на изображение", + "ToastInvalidMaxEpisodesToDownload": "Невалиден максимален брой епизоди за изтегляне", + "ToastInvalidUrl": "Невалиден URL адрес", + "ToastInvalidUrls": "Един или повече URL адреси са невалидни", "ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена", + "ToastItemDeletedFailed": "Неуспешно изтриване на елемента", + "ToastItemDeletedSuccess": "Елементът е изтрит", "ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени", "ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като Завършено", "ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен", "ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като Незавършено", "ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен", + "ToastItemUpdateSuccess": "Елементът е актуализиран", "ToastLibraryCreateFailed": "Неуспешно създаване на библиотека", "ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена", "ToastLibraryDeleteFailed": "Неуспешно изтриване на библиотека", @@ -1037,28 +1069,97 @@ "ToastLibraryScanFailedToStart": "Неуспешно стартиране на сканиране", "ToastLibraryScanStarted": "Сканирането на библиотеката е стартирано", "ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена", + "ToastMatchAllAuthorsFailed": "Неуспешно съвпадение на всички автори", + "ToastMetadataFilesRemovedError": "Грешка при премахване на metadata.{0} файлове", + "ToastMetadataFilesRemovedNoneFound": "Не са намерени metadata.{0} файлове в библиотеката", + "ToastMetadataFilesRemovedNoneRemoved": "Не са премахнати metadata.{0} файлове", + "ToastMetadataFilesRemovedSuccess": "Премахнати са {0} файла metadata.{1}", + "ToastMustHaveAtLeastOnePath": "Трябва да има поне един път", + "ToastNameEmailRequired": "Изискват се име и имейл", + "ToastNameRequired": "Изисква се име", + "ToastNewApiKeyUserError": "Трябва да изберете потребител", + "ToastNewEpisodesFound": "Намерени са {0} нови епизода", + "ToastNewUserCreatedFailed": "Неуспешно създаване на акаунт: „{0}“", + "ToastNewUserCreatedSuccess": "Създаден е нов акаунт", + "ToastNewUserLibraryError": "Трябва да изберете поне една библиотека", + "ToastNewUserPasswordError": "Трябва да има парола; само root потребителят може да бъде с празна парола", + "ToastNewUserTagError": "Трябва да изберете поне един етикет", + "ToastNewUserUsernameError": "Въведете потребителско име", + "ToastNoNewEpisodesFound": "Не са намерени нови епизоди", + "ToastNoRSSFeed": "Подкастът няма RSS емисия", + "ToastNoUpdatesNecessary": "Не са необходими актуализации", + "ToastNotificationCreateFailed": "Неуспешно създаване на известие", + "ToastNotificationDeleteFailed": "Неуспешно изтриване на известието", + "ToastNotificationFailedMaximum": "Максималният брой неуспешни опити трябва да бъде >= 0", + "ToastNotificationQueueMaximum": "Максималната опашка за известия трябва да бъде >= 0", + "ToastNotificationSettingsUpdateSuccess": "Настройките за известия са актуализирани", + "ToastNotificationTestTriggerFailed": "Неуспешно задействане на тестово известие", + "ToastNotificationTestTriggerSuccess": "Тестовото известие е задействано", + "ToastNotificationUpdateSuccess": "Известието е актуализирано", "ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист", "ToastPlaylistCreateSuccess": "Плейлистът е създаден", "ToastPlaylistRemoveSuccess": "Плейлистът е премахнат", "ToastPlaylistUpdateSuccess": "Плейлистът е обновен", "ToastPodcastCreateFailed": "Неуспешно създаване на подкаст", "ToastPodcastCreateSuccess": "Подкаст успешно създаден", + "ToastPodcastEpisodeUpdated": "Епизодът е актуализиран", + "ToastPodcastGetFeedFailed": "Неуспешно извличане на емисията на подкаста", + "ToastPodcastNoEpisodesInFeed": "Не са намерени епизоди в RSS емисията", + "ToastPodcastNoRssFeed": "Подкастът няма RSS емисия", + "ToastProgressIsNotBeingSynced": "Напредъкът не се синхронизира, рестартирайте възпроизвеждането", + "ToastProviderCreatedFailed": "Неуспешно добавяне на доставчик", + "ToastProviderCreatedSuccess": "Добавен е нов доставчик", + "ToastProviderNameAndUrlRequired": "Изискват се име и URL адрес", + "ToastProviderRemoveSuccess": "Доставчикът е премахнат", "ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията", "ToastRSSFeedCloseSuccess": "RSS емисията е затворена", + "ToastRemoveFailed": "Неуспешно премахване", "ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция", "ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция", + "ToastRemoveItemsWithIssuesFailed": "Неуспешно премахване на елементите от библиотеката с проблеми", + "ToastRemoveItemsWithIssuesSuccess": "Елементите от библиотеката с проблеми са премахнати", + "ToastRenameFailed": "Неуспешно преименуване", + "ToastRescanFailed": "Повторното сканиране е неуспешно за {0}", + "ToastRescanRemoved": "Повторното сканиране завърши: елементът е премахнат", + "ToastRescanUpToDate": "Повторното сканиране завърши: елементът вече е актуален", + "ToastRescanUpdated": "Повторното сканиране завърши: елементът е актуализиран", + "ToastScanFailed": "Неуспешно сканиране на елемент от библиотеката", + "ToastSelectAtLeastOneUser": "Изберете поне един потребител", "ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство", "ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Не могат да бъдат добавени два сериала с едно и също име", "ToastSeriesUpdateFailed": "Неуспешно обновяване на серия", "ToastSeriesUpdateSuccess": "Серията е обновена", "ToastServerSettingsUpdateSuccess": "Настройките на сървъра са актуализирани", + "ToastSessionCloseFailed": "Неуспешно затваряне на сесията", "ToastSessionDeleteFailed": "Неуспешно изтриване на сесия", "ToastSessionDeleteSuccess": "Сесията е изтрита", + "ToastSleepTimerDone": "Таймерът за заспиване приключи... zZzzZz", + "ToastSlugMustChange": "Краткият URL (slug) съдържа невалидни символи", + "ToastSlugRequired": "Изисква се кратък URL (slug)", "ToastSocketConnected": "Свързан сокет", "ToastSocketDisconnected": "Сокетът е прекъснат", "ToastSocketFailedToConnect": "Неуспешно свързване на сокет", "ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране", "ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)", + "ToastTitleRequired": "Изисква се заглавие", + "ToastUnknownError": "Неизвестна грешка", + "ToastUnlinkOpenIdFailed": "Неуспешно прекъсване на връзката на потребителя с OpenID", + "ToastUnlinkOpenIdSuccess": "Връзката на потребителя с OpenID е прекъсната", + "ToastUploaderFilepathExistsError": "Файловият път „{0}“ вече съществува на сървъра", + "ToastUploaderItemExistsInSubdirectoryError": "Елементът „{0}“ използва поддиректория на пътя за качване.", "ToastUserDeleteFailed": "Неуспешно изтриване на потребител", - "ToastUserDeleteSuccess": "Потребителят е изтрит" + "ToastUserDeleteSuccess": "Потребителят е изтрит", + "ToastUserPasswordChangeSuccess": "Паролата е променена успешно", + "ToastUserPasswordMismatch": "Паролите не съвпадат", + "ToastUserPasswordMustChange": "Новата парола не може да бъде същата като старата", + "ToastUserRootRequireName": "Трябва да въведете root потребителско име", + "TooltipAddChapters": "Добавяне на глава(и)", + "TooltipAddOneSecond": "Добавяне на 1 секунда", + "TooltipAdjustChapterStart": "Кликнете за коригиране на началния час", + "TooltipLockAllChapters": "Заключване на всички глави", + "TooltipLockChapter": "Заключване на глава (Shift+клик за диапазон)", + "TooltipSubtractOneSecond": "Изваждане на 1 секунда", + "TooltipUnlockAllChapters": "Отключване на всички глави", + "TooltipUnlockChapter": "Отключване на глава (Shift+клик за диапазон)" } From 52a485d1359daf79832fa6d3987bea8793c3fe87 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 9 May 2026 16:09:49 +0200 Subject: [PATCH 266/285] Added translation using Weblate (Latvian) --- client/strings/lv.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 client/strings/lv.json diff --git a/client/strings/lv.json b/client/strings/lv.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/client/strings/lv.json @@ -0,0 +1 @@ +{} From 4f21fc023c1c227949955ed6aa76d9f5b1f9f722 Mon Sep 17 00:00:00 2001 From: EteranlK Date: Sun, 10 May 2026 19:36:36 +0200 Subject: [PATCH 267/285] Translated using Weblate (Arabic) Currently translated at 96.3% (1120 of 1163 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/ --- client/strings/ar.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/strings/ar.json b/client/strings/ar.json index a176b9d8b..dc5ef1762 100644 --- a/client/strings/ar.json +++ b/client/strings/ar.json @@ -244,6 +244,8 @@ "LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك", "LabelApiKeyCreated": "تم إنشاء مفتاح API \"{0}\" بنجاح.", "LabelApiKeyCreatedDescription": "تأكد من نسخ مفتاح API الآن، لن تتمكن من رؤيته مرة أخرى.", + "LabelApiKeyUser": "التصرف بالنيابة عن مستخدم", + "LabelApiKeyUserDescription": "مفتاح API سيمتلك نفس صلاحيات المستخدم الذي ينوب عنه ، سيظهر بالسجلات وكأن المستخدم قام بالطلب.", "LabelApiToken": "رمز API", "LabelAppend": "إلحاق", "LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)", @@ -293,6 +295,7 @@ "LabelContinueListening": "استمرار الاستماع", "LabelContinueReading": "استمرار القراءة", "LabelContinueSeries": "استمرار المسلسلات", + "LabelCorsAllowed": "CORS Origins مسموح", "LabelCover": "الغلاف", "LabelCoverImageURL": "رابط صورة الغلاف", "LabelCoverProvider": "مزود الغلاف", @@ -426,6 +429,9 @@ "LabelLibraryFilterSublistEmpty": "لا يوجد {0}", "LabelLibraryItem": "عنصر المكتبة", "LabelLibraryName": "اسم المكتبة", + "LabelLibrarySortByProgress": "المرحلة: الأحدث", + "LabelLibrarySortByProgressFinished": "المرحلة: تم الانتهاء", + "LabelLibrarySortByProgressStarted": "المرحلة: تم البدء", "LabelLimit": "حد", "LabelLineSpacing": "تباعد الأسطر", "LabelListenAgain": "الاستماع مجدداً", From 50eeca2e0fcd7136beef1bdd04db368faa2619c3 Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Wed, 13 May 2026 13:47:32 +0200 Subject: [PATCH 268/285] Translated using Weblate (Belarusian) 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/be/ --- client/strings/be.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index b4e4df86a..d3aa42cf1 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -88,7 +88,7 @@ "ButtonResetToDefault": "Скінуць да прадвызначаных", "ButtonRestore": "Аднавіць", "ButtonSave": "Захаваць", - "ButtonSaveAndClose": "Захаваць і зачыніць", + "ButtonSaveAndClose": "Захаваць і закрыць", "ButtonSaveTracklist": "Захаваць спіс трэкаў", "ButtonScan": "Сканаваць", "ButtonScanLibrary": "Сканіраваць бібліятэку", @@ -284,7 +284,7 @@ "LabelChaptersFound": "раздзелаў знойдзена", "LabelClickForMoreInfo": "Націсніце для больш падрабязнай інфармацыі", "LabelClickToUseCurrentValue": "Націсніце, каб выкарыстоўваць бягучае значэнне", - "LabelClosePlayer": "Зачыніць прайгравальнік", + "LabelClosePlayer": "Закрыць прайгравальнік", "LabelCodec": "Кодэк", "LabelCollapseSeries": "Згарнуць серыі", "LabelCollapseSubSeries": "Згарнуць падсерыі", From 72dc75482f73c766681b5fe20dc1895897bfef44 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 May 2026 14:31:41 -0500 Subject: [PATCH 269/285] Version bump v2.35.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 79ac53250..ba071852b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.34.0", + "version": "2.35.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.34.0", + "version": "2.35.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index dd0f3a0c9..71d64f60a 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.34.0", + "version": "2.35.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 8950b4903..312675d71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.34.0", + "version": "2.35.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.34.0", + "version": "2.35.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 10ba26f65..a0b340244 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.34.0", + "version": "2.35.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From e944b2a2f5d2792cb2d9412708d87815f0e8088c Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 21 May 2026 17:08:39 -0700 Subject: [PATCH 270/285] Add unique UUID to access and refresh tokens --- server/auth/TokenManager.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 42ca262bb..5933209c7 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -1,4 +1,5 @@ const { Op } = require('sequelize') +const uuid = require('uuid') const Database = require('../Database') const Logger = require('../Logger') @@ -115,6 +116,7 @@ class TokenManager { const payload = { userId: user.id, username: user.username, + jti: uuid.v4(), type: 'access' } const options = { @@ -138,6 +140,7 @@ class TokenManager { const payload = { userId: user.id, username: user.username, + jti: uuid.v4(), type: 'refresh' } const options = { From c97b36e11c51230c4ec3537990b3fbdad72ad44d Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 21 May 2026 21:06:17 -0700 Subject: [PATCH 271/285] Add `ignoreDuplicates` for bookAuthor when renaming to respect unique index --- server/controllers/AuthorController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 80471ec47..8c2e80aec 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -149,7 +149,7 @@ class AuthorController { }) if (libraryItems.length) { await Database.bookAuthorModel.removeByIds(req.author.id) // Remove all old BookAuthor - await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate) // Create all new BookAuthor + await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate, { ignoreDuplicates: true }) // Create all new unique BookAuthor for (const libraryItem of libraryItems) { await libraryItem.saveMetadataFile() } From 1bad2d907294d5eeacbc29e868e2e25613da1176 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 27 May 2026 17:33:14 -0500 Subject: [PATCH 272/285] Cleanup abmetadata file parsing & fix server crash #5268 #4287 #5142 --- server/scanner/AbsMetadataFileScanner.js | 29 ++-- server/scanner/BookScanner.js | 3 + server/scanner/PodcastScanner.js | 3 + .../utils/generators/abmetadataGenerator.js | 143 +++++++++++++++--- 4 files changed, 145 insertions(+), 33 deletions(-) diff --git a/server/scanner/AbsMetadataFileScanner.js b/server/scanner/AbsMetadataFileScanner.js index e554dfb44..0ea34b1bb 100644 --- a/server/scanner/AbsMetadataFileScanner.js +++ b/server/scanner/AbsMetadataFileScanner.js @@ -5,15 +5,15 @@ const { LogLevel } = require('../utils/constants') const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') class AbsMetadataFileScanner { - constructor() { } + constructor() {} /** * Check for metadata.json file and set book metadata - * - * @param {import('./LibraryScan')} libraryScan - * @param {import('./LibraryItemScanData')} libraryItemData - * @param {Object} bookMetadata - * @param {string} [existingLibraryItemId] + * + * @param {import('./LibraryScan')} libraryScan + * @param {import('./LibraryItemScanData')} libraryItemData + * @param {Object} bookMetadata + * @param {string} [existingLibraryItemId] */ async scanBookMetadataFile(libraryScan, libraryItemData, bookMetadata, existingLibraryItemId = null) { const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile @@ -32,7 +32,8 @@ class AbsMetadataFileScanner { if (metadataText) { libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`) - const abMetadata = abmetadataGenerator.parseJson(metadataText) || {} + const abMetadata = abmetadataGenerator.parseJson(metadataText, 'book') || {} + for (const key in abMetadata) { // TODO: When to override with null or empty arrays? if (abMetadata[key] === undefined || abMetadata[key] === null) continue @@ -48,11 +49,11 @@ class AbsMetadataFileScanner { /** * Check for metadata.json file and set podcast metadata - * - * @param {import('./LibraryScan')} libraryScan - * @param {import('./LibraryItemScanData')} libraryItemData - * @param {Object} podcastMetadata - * @param {string} [existingLibraryItemId] + * + * @param {import('./LibraryScan')} libraryScan + * @param {import('./LibraryItemScanData')} libraryItemData + * @param {Object} podcastMetadata + * @param {string} [existingLibraryItemId] */ async scanPodcastMetadataFile(libraryScan, libraryItemData, podcastMetadata, existingLibraryItemId = null) { const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile @@ -71,7 +72,7 @@ class AbsMetadataFileScanner { if (metadataText) { libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`) - const abMetadata = abmetadataGenerator.parseJson(metadataText) || {} + const abMetadata = abmetadataGenerator.parseJson(metadataText, 'podcast') || {} for (const key in abMetadata) { if (abMetadata[key] === undefined || abMetadata[key] === null) continue if (key === 'tags' && !abMetadata.tags?.length) continue @@ -81,4 +82,4 @@ class AbsMetadataFileScanner { } } } -module.exports = new AbsMetadataFileScanner() \ No newline at end of file +module.exports = new AbsMetadataFileScanner() diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index ac93c6379..8ae616fe8 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -825,6 +825,9 @@ class BookScanner { const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) + /** + * Keys must match abmetadataGenerator.js + */ const jsonObject = { tags: libraryItem.media.tags || [], chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [], diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 6ab2d332c..f3241d536 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -425,6 +425,9 @@ class PodcastScanner { const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) + /** + * Keys must match abmetadataGenerator.js + */ const jsonObject = { tags: libraryItem.media.tags || [], title: libraryItem.media.title, diff --git a/server/utils/generators/abmetadataGenerator.js b/server/utils/generators/abmetadataGenerator.js index 01f853288..d0d278752 100644 --- a/server/utils/generators/abmetadataGenerator.js +++ b/server/utils/generators/abmetadataGenerator.js @@ -1,7 +1,51 @@ const Logger = require('../../Logger') const parseSeriesString = require('../parsers/parseSeriesString') -function parseJsonMetadataText(text) { +const mediaTypeKeys = { + book: { + tags: 'stringArray', + title: 'string', + subtitle: 'string', + authors: 'stringArray', + narrators: 'stringArray', + series: 'stringArray', + genres: 'stringArray', + publishedYear: 'string', + publishedDate: 'string', + publisher: 'string', + description: 'string', + isbn: 'string', + asin: 'string', + language: 'string', + explicit: 'boolean', + abridged: 'boolean' + }, + podcast: { + tags: 'stringArray', + title: 'string', + author: 'string', + description: 'string', + releaseDate: 'string', + genres: 'stringArray', + feedURL: 'string', + imageURL: 'string', + itunesPageURL: 'string', + itunesId: 'string', + itunesArtistId: 'string', + asin: 'string', + language: 'string', + explicit: 'boolean', + podcastType: 'string' + } +} + +/** + * + * @param {string} text + * @param {"book" | "podcast"} mediaType + * @returns {Object} + */ +function parseJsonMetadataText(text, mediaType) { try { const abmetadataData = JSON.parse(text) @@ -19,28 +63,41 @@ function parseJsonMetadataText(text) { } delete abmetadataData.metadata - if (abmetadataData.series?.length) { - abmetadataData.series = [...new Set(abmetadataData.series.map((t) => t?.trim()).filter((t) => t))] - abmetadataData.series = abmetadataData.series.map((series) => parseSeriesString.parse(series)) + const expectedKeys = mediaTypeKeys[mediaType] + if (!expectedKeys) { + Logger.error(`[abmetadataGenerator] Invalid media type "${mediaType}"`) + return null } - // clean tags & remove dupes - if (abmetadataData.tags?.length) { - abmetadataData.tags = [...new Set(abmetadataData.tags.map((t) => t?.trim()).filter((t) => t))] + + const validated = {} + for (const key in expectedKeys) { + const expectedType = expectedKeys[key] + if (!(key in abmetadataData)) continue + + const validatedValue = validateMetadataValue(key, abmetadataData[key], expectedType) + if (validatedValue !== undefined) { + validated[key] = validatedValue + } } - if (abmetadataData.chapters?.length) { - abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title) + + if (validated.series?.length) { + validated.series = validated.series.map((series) => parseSeriesString.parse(series)).filter(Boolean) } - // clean remove dupes - if (abmetadataData.authors?.length) { - abmetadataData.authors = [...new Set(abmetadataData.authors.map((t) => t?.trim()).filter((t) => t))] + + if (mediaType === 'book' && 'chapters' in abmetadataData) { + if (abmetadataData.chapters === null) { + validated.chapters = [] + } else if (Array.isArray(abmetadataData.chapters)) { + const cleanedChapters = cleanChaptersArray(abmetadataData.chapters, validated.title ?? abmetadataData.title) + if (cleanedChapters) { + validated.chapters = cleanedChapters + } + } else { + Logger.warn(`[abmetadataGenerator] Invalid metadata key "chapters" expected array, got ${typeof abmetadataData.chapters}`) + } } - if (abmetadataData.narrators?.length) { - abmetadataData.narrators = [...new Set(abmetadataData.narrators.map((t) => t?.trim()).filter((t) => t))] - } - if (abmetadataData.genres?.length) { - abmetadataData.genres = [...new Set(abmetadataData.genres.map((t) => t?.trim()).filter((t) => t))] - } - return abmetadataData + + return validated } catch (error) { Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error) return null @@ -48,6 +105,54 @@ function parseJsonMetadataText(text) { } module.exports.parseJson = parseJsonMetadataText +/** + * @param {string} key + * @param {*} value + * @param {string} expectedType + * @returns {*|undefined} undefined excludes the key + */ +function validateMetadataValue(key, value, expectedType) { + if (expectedType === 'string') { + if (value === null) return null + if (typeof value === 'number') return String(value) + if (typeof value === 'string') return value + Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected string, got ${typeof value}`) + return undefined + } + + if (expectedType === 'boolean') { + if (value === null) return null + if (typeof value === 'boolean') return value + if (typeof value === 'string') { + const lower = value.toLowerCase() + if (lower === 'true') return true + if (lower === 'false') return false + } + Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected boolean, got ${typeof value}`) + return undefined + } + + // Filter empty strings and deduplicate + if (expectedType === 'stringArray') { + if (value === null) return [] + if (!Array.isArray(value)) { + Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected string array, got ${typeof value}`) + return undefined + } + + const cleanedArray = value.filter((t) => typeof t === 'string') + return [...new Set(cleanedArray.map((t) => t.trim()).filter((t) => t))] + } + + Logger.warn(`[abmetadataGenerator] Unknown expected type "${expectedType}" for key "${key}"`) + return undefined +} + +/** + * @param {Object[]} chaptersArray + * @param {string} mediaTitle + * @returns {Object[]} + */ function cleanChaptersArray(chaptersArray, mediaTitle) { const chapters = [] let index = 0 From 499b52b4dd29b5d91ff142069339e2913bc6cd20 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 28 May 2026 14:49:49 -0500 Subject: [PATCH 273/285] Update Sequelize where query for User username/email case insensitive --- server/models/User.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/server/models/User.js b/server/models/User.js index 936efde13..f380f8e4f 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -352,11 +352,7 @@ class User extends Model { if (cachedUser) return cachedUser const user = await this.findOne({ - where: { - username: { - [sequelize.Op.like]: username - } - }, + where: sequelize.where(sequelize.fn('lower', sequelize.col('username')), username.toLowerCase()), include: this.sequelize.models.mediaProgress }) @@ -377,11 +373,7 @@ class User extends Model { if (cachedUser) return cachedUser const user = await this.findOne({ - where: { - email: { - [sequelize.Op.like]: email - } - }, + where: sequelize.where(sequelize.fn('lower', sequelize.col('email')), email.toLowerCase()), include: this.sequelize.models.mediaProgress }) From aacdcc47ec0b6fa778b84627c79deb7ee0cecd70 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 28 May 2026 15:22:55 -0500 Subject: [PATCH 274/285] Version bump v2.35.1 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index ba071852b..502ba4b03 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.35.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.35.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 71d64f60a..0ec070b32 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.35.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 312675d71..9cee15b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.35.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.35.1", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index a0b340244..a7755fd66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.35.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From e70e4b9d40a6251897e114c4154add8c05ad0944 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 30 May 2026 15:43:50 -0500 Subject: [PATCH 275/285] Fix typo on onTest notification body --- server/utils/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/notifications.js b/server/utils/notifications.js index 700d3c389..497d7a1a1 100644 --- a/server/utils/notifications.js +++ b/server/utils/notifications.js @@ -100,7 +100,7 @@ module.exports.notificationData = { variables: ['version'], defaults: { title: 'Test Notification on Abs {{version}}', - body: 'Test notificataion body for abs {{version}}.' + body: 'Test notification body for abs {{version}}.' }, testData: { version: 'v' + version From 036bc081f00a0b94d829e50715943143c2c185fa Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 4 Jun 2026 14:03:59 +0300 Subject: [PATCH 276/285] index.js: Read AllowedDevOrigins from dev,js into ALLOWED_DEV_ORIGINS env var --- index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.js b/index.js index 7379322e8..ce0195e95 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,11 @@ if (isDev || options['prod-with-dev-env']) { if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1' if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath if (devEnv.ReactClientPath) process.env.REACT_CLIENT_PATH = devEnv.ReactClientPath + if (devEnv.AllowedDevOrigins) { + process.env.ALLOWED_DEV_ORIGINS = Array.isArray(devEnv.AllowedDevOrigins) + ? devEnv.AllowedDevOrigins.join(',') + : String(devEnv.AllowedDevOrigins) + } process.env.SOURCE = 'local' process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf' } From 3417c0c721b1b8d280da2e8a5b15b26014377717 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 18 Jun 2026 12:39:50 +0300 Subject: [PATCH 277/285] Enhance PodcastController to handle enclosure updates, allowing for null values and object structure validation for enclosure properties. --- server/controllers/PodcastController.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 85173e622..5eaa2a64a 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -437,6 +437,17 @@ class PodcastController { } updatePayload[key] = req.body[key] + } else if (key === 'enclosure') { + const enclosure = req.body.enclosure + if (enclosure === null) { + updatePayload.enclosureURL = null + updatePayload.enclosureSize = null + updatePayload.enclosureType = null + } else if (typeof enclosure === 'object' && typeof enclosure.url === 'string') { + updatePayload.enclosureURL = enclosure.url + updatePayload.enclosureType = typeof enclosure.type === 'string' ? enclosure.type : null + updatePayload.enclosureSize = enclosure.length !== undefined && enclosure.length !== null ? enclosure.length : null + } } else if (key === 'chapters' && Array.isArray(req.body[key]) && req.body[key].every((ch) => typeof ch === 'object' && ch.title && ch.start)) { updatePayload[key] = req.body[key] } else if (key === 'publishedAt' && typeof req.body[key] === 'number') { From 56e60b842020956bb835043dd32ce2325295f6b9 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 23 Jun 2026 14:08:16 +0300 Subject: [PATCH 278/285] Server.js: Let next.js handle file uploads through internal-api routes --- server/Server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/Server.js b/server/Server.js index d6f748a1e..ef0888927 100644 --- a/server/Server.js +++ b/server/Server.js @@ -302,7 +302,9 @@ class Server { this.server = http.createServer(app) + // Skip file upload parsing for internal-api routes (Next.js proxies read multipart bodies). router.use( + /^(?!\/internal-api).*/, fileUpload({ defCharset: 'utf8', defParamCharset: 'utf8', From 354e0bf79384f3d6079b97b258e3395db64972d9 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 29 Jun 2026 16:32:41 -0500 Subject: [PATCH 279/285] Update readme remove reverse proxy set up and link to docs page --- readme.md | 264 +----------------------------------------------------- 1 file changed, 2 insertions(+), 262 deletions(-) diff --git a/readme.md b/readme.md index 877ea091c..989562a3e 100644 --- a/readme.md +++ b/readme.md @@ -87,269 +87,9 @@ See [install docs](https://www.audiobookshelf.org/docs) #### Note: Using a subfolder is supported with no additional changes but the path must be `/audiobookshelf` (this is not changeable). See [discussion](https://github.com/advplyr/audiobookshelf/discussions/3535) -### NGINX Proxy Manager +See [Reverse Proxy docs](https://audiobookshelf.org/docs/category/reverse-proxy) -Toggle websockets support. - -NGINX Web socket - -### NGINX Reverse Proxy - -Add this to the site config file on your nginx server after you have changed the relevant parts in the <> brackets, and inserted your certificate paths. - -```bash -server { - listen 443 ssl; - server_name ..; - - access_log /var/log/nginx/audiobookshelf.access.log; - error_log /var/log/nginx/audiobookshelf.error.log; - - ssl_certificate /path/to/certificate; - ssl_certificate_key /path/to/key; - - location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $http_host; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - proxy_http_version 1.1; - - proxy_pass http://; - proxy_redirect http:// https://; - - # Prevent 413 Request Entity Too Large error - # by increasing the maximum allowed size of the client request body - # For example, set it to 10 GiB - client_max_body_size 10240M; - } -} -``` - -### Apache Reverse Proxy - -Add this to the site config file on your Apache server after you have changed the relevant parts in the <> brackets, and inserted your certificate paths. - -For this to work you must enable at least the following mods using `a2enmod`: - -- `ssl` -- `proxy` -- `proxy_http` -- `proxy_balancer` -- `proxy_wstunnel` -- `rewrite` - -```bash - - - ServerName .. - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - ProxyPreserveHost On - ProxyPass / http://localhost:/ - RewriteEngine on - RewriteCond %{HTTP:Upgrade} websocket [NC] - RewriteCond %{HTTP:Connection} upgrade [NC] - RewriteRule ^/?(.*) "ws://localhost:/$1" [P,L] - - # unless you're doing something special this should be generated by a - # tool like certbot by let's encrypt - SSLCertificateFile /path/to/cert/file - SSLCertificateKeyFile /path/to/key/file - - -``` - -If using Apache >= 2.4.47 you can use the following, without having to use any of the `RewriteEngine`, `RewriteCond`, or `RewriteRule` directives. For example: - -```xml - - ProxyPreserveHost on - ProxyPass http://localhost:/audiobookshelf upgrade=websocket - ProxyPassReverse http://localhost:/audiobookshelf - -``` - -Some SSL certificates like those signed by Let's Encrypt require ACME validation. To allow Let's Encrypt to write and confirm the ACME challenge, edit your VirtualHost definition to prevent proxying traffic that queries `/.well-known` and instead serve that directly: - -```bash - - # ... - - # create the directory structure /.well-known/acme-challenges - # within DocumentRoot and give the HTTP user recursive write - # access to it. - DocumentRoot /path/to/local/directory - - ProxyPreserveHost On - ProxyPass /.well-known ! - ProxyPass / http://localhost:/ - - # ... - -``` - -### SWAG Reverse Proxy - -[See LinuxServer.io config sample](https://github.com/linuxserver/reverse-proxy-confs/blob/master/audiobookshelf.subdomain.conf.sample) - -### Synology NAS Reverse Proxy Setup (DSM 7+/Quickconnect) - -1. **Open Control Panel** - - - Navigate to `Login Portal > Advanced`. - -2. **General Tab** - - - Click `Reverse Proxy` > `Create`. - - | Setting | Value | - | ------------------ | -------------- | - | Reverse Proxy Name | audiobookshelf | - -3. **Source Configuration** - - | Setting | Value | - | ---------------------- | ---------------------------------------- | - | Protocol | HTTPS | - | Hostname | `..synology.me` | - | Port | 443 | - | Access Control Profile | Leave as is | - - - Example Hostname: `audiobookshelf.mydomain.synology.me` - -4. **Destination Configuration** - - | Setting | Value | - | -------- | ----------- | - | Protocol | HTTP | - | Hostname | Your NAS IP | - | Port | 13378 | - -5. **Custom Header Tab** - - - Go to `Create > Websocket`. - - Configure Headers (leave as is): - - | Header Name | Value | - | ----------- | --------------------- | - | Upgrade | `$http_upgrade` | - | Connection | `$connection_upgrade` | - -6. **Advanced Settings Tab** - - Leave as is. - -### [Traefik Reverse Proxy](https://doc.traefik.io/traefik/) - -Middleware relating to CORS will cause the app to report Unknown Error when logging in. To prevent this don't apply any of the following headers to the router for this site: - -
    -
  • accessControlAllowMethods
  • -
  • accessControlAllowOriginList
  • -
  • accessControlMaxAge
  • -
- -From [@Dondochaka](https://discord.com/channels/942908292873723984/942914154254176257/945074590374318170) and [@BeastleeUK](https://discord.com/channels/942908292873723984/942914154254176257/970366039294611506)
- -### Example Caddyfile - [Caddy Reverse Proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) - -``` -subdomain.domain.com { - encode gzip zstd - reverse_proxy : -} -``` - -### HAProxy - -Below is a generic HAProxy config, using `audiobookshelf.YOUR_DOMAIN.COM`. - -To use `http2`, `ssl` is needed. - -```make -global - # ... (your global settings go here) - -defaults - mode http - # ... (your default settings go here) - -frontend my_frontend - # Bind to port 443, enable SSL, and specify the certificate list file - bind :443 name :443 ssl crt-list /path/to/cert.crt_list alpn h2,http/1.1 - mode http - - # Define an ACL for subdomains starting with "audiobookshelf" - acl is_audiobookshelf hdr_beg(host) -i audiobookshelf - - # Use the ACL to route traffic to audiobookshelf_backend if the condition is met, - # otherwise, use the default_backend - use_backend audiobookshelf_backend if is_audiobookshelf - default_backend default_backend - -backend audiobookshelf_backend - mode http - # ... (backend settings for audiobookshelf go here) - - # Define the server for the audiobookshelf backend - server audiobookshelf_server 127.0.0.99:13378 - -backend default_backend - mode http - # ... (default backend settings go here) - - # Define the server for the default backend - server default_server 127.0.0.123:8081 - -``` - -### pfSense and HAProxy - -For pfSense the inputs are graphical, and `Health checking` is enabled. - -#### Frontend, Default backend, access control lists and actions - -##### Access Control lists - -| Name | Expression | CS | Not | Value | -| :------------: | :---------------: | :-: | :-: | :-------------: | -| audiobookshelf | Host starts with: | | | audiobookshelf. | - -##### Actions - -The `condition acl names` needs to match the name above `audiobookshelf`. - -| Action | Parameters | Condition acl names | -| :-----------: | :------------: | :-----------------: | -| `Use Backend` | audiobookshelf | audiobookshelf | - -#### Backend - -The `Name` needs to match the `Parameters` above `audiobookshelf`. - -| Name | audiobookshelf | -| ---- | -------------- | - -##### Server list: - -| Name | Expression | CS | Not | Value | -| :------------: | :---------------: | :-: | :-: | :-------------: | -| audiobookshelf | Host starts with: | | | audiobookshelf. | - -##### Health checking: - -Health checking is enabled by default. `Http check method` of `OPTIONS` is not supported on Audiobookshelf. If Health check fails, data will not be forwared. Need to do one of following: - -- To disable: Change `Health check method` to `none`. -- To make Health checking function: Change `Http check method` to `HEAD` or `GET`. - -# Run from source - -# Contributing +# Contributing / Run from source This application is built using [NodeJs](https://nodejs.org/). From 58dcda36a550fcbb2313d19d04e6edf94805bb48 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 29 Jun 2026 16:37:54 -0500 Subject: [PATCH 280/285] Cleanup readme and link to contributing docs --- readme.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 989562a3e..a79d91d5d 100644 --- a/readme.md +++ b/readme.md @@ -87,15 +87,24 @@ See [install docs](https://www.audiobookshelf.org/docs) #### Note: Using a subfolder is supported with no additional changes but the path must be `/audiobookshelf` (this is not changeable). See [discussion](https://github.com/advplyr/audiobookshelf/discussions/3535) -See [Reverse Proxy docs](https://audiobookshelf.org/docs/category/reverse-proxy) +See [reverse proxy docs](https://audiobookshelf.org/docs/category/reverse-proxy) -# Contributing / Run from source +
-This application is built using [NodeJs](https://nodejs.org/). +# Contributing + +See [contributing docs](https://audiobookshelf.org/docs/contributing/general/) ### Localization -Thank you to [Weblate](https://hosted.weblate.org/engage/audiobookshelf/) for hosting our localization infrastructure pro-bono. If you want to see Audiobookshelf in your language, please help us localize. Additional information on helping with the translations [here](https://www.audiobookshelf.org/faq#how-do-i-help-with-translations). Translation status +Thank you to [Weblate](https://hosted.weblate.org/engage/audiobookshelf/) for hosting our localization infrastructure pro-bono. If you want to see Audiobookshelf in your language, please help us localize. Additional information on helping with the translations [here](https://www.audiobookshelf.org/faq#how-do-i-help-with-translations). + Translation status + +
+ +# Run from source + +This application is built using [NodeJs](https://nodejs.org/). ### Dev Container Setup @@ -187,6 +196,3 @@ If you are using VSCode, this project includes a couple of pre-defined targets t - `Debug client (nuxt)`—Run the client with live reload. - `Debug server and client (nuxt)`—Runs both the preceding two debug targets. -# How to Support - -[See the incomplete "How to Support" page](https://www.audiobookshelf.org/support) From b2f593f1c4550a9dbf9849470fa9122c0ff666a6 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 29 Jun 2026 16:43:24 -0500 Subject: [PATCH 281/285] Update readme organizing docs link & remove api docs link --- readme.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index a79d91d5d..762968ef0 100644 --- a/readme.md +++ b/readme.md @@ -57,21 +57,17 @@ Try it out on the [Google Play Store](https://play.google.com/store/apps/details Using Test Flight: https://testflight.apple.com/join/wiic7QIW **_(beta is full)_** -### Build your own tools & clients - -Check out the [API documentation](https://api.audiobookshelf.org/) -
Library Screenshot
-# Organizing your audiobooks +# Organizing your media #### Directory structure and folder names are important to Audiobookshelf! -See [documentation](https://audiobookshelf.org/docs#book-directory-structure) for supported directory structure, folder naming conventions, and audio file metadata usage. +See [library docs](https://audiobookshelf.org/docs/category/libraries) for supported directory structures, folder naming conventions, and audio file metadata usage.
From 6f03467f35e8dc4a8131b47733b9763bca164398 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 29 Jun 2026 16:46:33 -0500 Subject: [PATCH 282/285] Update links in the README --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 762968ef0..0ecfcc526 100644 --- a/readme.md +++ b/readme.md @@ -6,9 +6,9 @@
Documentation · - User Guides - · Support + · + Demo

From 3bb53d939d8367494d5203b404b254e6b03144a2 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 1 Jul 2026 22:30:21 +0300 Subject: [PATCH 283/285] Add all minified fields to expanded library item JSON so socket updates remain a strict superset of shelf payloads --- server/models/Book.js | 19 ++- server/models/LibraryItem.js | 29 ++-- server/models/Podcast.js | 26 +-- .../models/oldJsonSerialization.test.js | 158 ++++++++++++++++++ 4 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 test/server/models/oldJsonSerialization.test.js diff --git a/server/models/Book.js b/server/models/Book.js index d9f2ff132..7e1a2e30c 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -647,6 +647,11 @@ class Book extends Model { } } + /** + * Minified book JSON for list/shelf endpoints. + * `toOldJSONExpanded()` must be a strict superset: every key here must exist in expanded + * with the same value semantics. Only additive changes to expanded; never remove or rename keys. + */ toOldJSONMinified() { if (!this.authors) { throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`) @@ -669,6 +674,12 @@ class Book extends Model { } } + /** + * Expanded book JSON for item detail and socket events. + * Must be a strict superset of `toOldJSONMinified()` — built by spreading minified, then adding expanded-only fields. + * + * @param {string} libraryItemId + */ toOldJSONExpanded(libraryItemId) { if (!libraryItemId) { throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`) @@ -681,16 +692,12 @@ class Book extends Model { } return { - id: this.id, - libraryItemId: libraryItemId, + ...this.toOldJSONMinified(), + libraryItemId, metadata: this.oldMetadataToJSONExpanded(), - coverPath: this.coverPath, - tags: [...(this.tags || [])], audioFiles: structuredClone(this.audioFiles), chapters: structuredClone(this.chapters), ebookFile: structuredClone(this.ebookFile), - duration: this.duration, - size: this.size, tracks: this.getTracklist(libraryItemId) } } diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index b33fa585c..c03667efc 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1004,6 +1004,11 @@ class LibraryItem extends Model { } } + /** + * Minified library item JSON for list/shelf endpoints. + * `toOldJSONExpanded()` must be a strict superset: every key here must exist in expanded + * with the same value semantics. Only additive changes to expanded; never remove or rename keys. + */ toOldJSONMinified() { if (!this.media) { throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`) @@ -1032,30 +1037,18 @@ class LibraryItem extends Model { } } + /** + * Expanded library item JSON for item detail and socket events. + * Must be a strict superset of `toOldJSONMinified()` — built by spreading minified, then adding expanded-only fields. + */ toOldJSONExpanded() { return { - id: this.id, - ino: this.ino, - oldLibraryItemId: this.extraData?.oldLibraryItemId || null, - libraryId: this.libraryId, - folderId: this.libraryFolderId, - path: this.path, - relPath: this.relPath, - isFile: this.isFile, - mtimeMs: this.mtime?.valueOf(), - ctimeMs: this.ctime?.valueOf(), - birthtimeMs: this.birthtime?.valueOf(), - addedAt: this.createdAt.valueOf(), - updatedAt: this.updatedAt.valueOf(), + ...this.toOldJSONMinified(), lastScan: this.lastScan?.valueOf(), scanVersion: this.lastScanVersion, - isMissing: !!this.isMissing, - isInvalid: !!this.isInvalid, - mediaType: this.mediaType, media: this.media.toOldJSONExpanded(this.id), // LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database - libraryFiles: this.getLibraryFilesJson(), - size: this.size + libraryFiles: this.getLibraryFilesJson() } } } diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 6dbe1cd19..9c222e48a 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -451,6 +451,11 @@ class Podcast extends Model { } } + /** + * Minified podcast JSON for list/shelf endpoints. + * `toOldJSONExpanded()` must be a strict superset: every key here must exist in expanded + * with the same value semantics. Only additive changes to expanded; never remove or rename keys. + */ toOldJSONMinified() { return { id: this.id, @@ -468,6 +473,12 @@ class Podcast extends Model { } } + /** + * Expanded podcast JSON for item detail and socket events. + * Must be a strict superset of `toOldJSONMinified()` — built by spreading minified, then adding expanded-only fields. + * + * @param {string} libraryItemId + */ toOldJSONExpanded(libraryItemId) { if (!libraryItemId) { throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`) @@ -477,18 +488,9 @@ class Podcast extends Model { } return { - id: this.id, - libraryItemId: libraryItemId, - metadata: this.oldMetadataToJSONExpanded(), - coverPath: this.coverPath, - tags: [...(this.tags || [])], - episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)), - autoDownloadEpisodes: this.autoDownloadEpisodes, - autoDownloadSchedule: this.autoDownloadSchedule, - lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null, - maxEpisodesToKeep: this.maxEpisodesToKeep, - maxNewEpisodesToDownload: this.maxNewEpisodesToDownload, - size: this.size + ...this.toOldJSONMinified(), + libraryItemId, + episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)) } } } diff --git a/test/server/models/oldJsonSerialization.test.js b/test/server/models/oldJsonSerialization.test.js new file mode 100644 index 000000000..98bab6afa --- /dev/null +++ b/test/server/models/oldJsonSerialization.test.js @@ -0,0 +1,158 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') + +const Database = require('../../../server/Database') + +/** + * Assert that `expanded` contains every key from `minified` with matching values. + * Used to enforce the contract: toOldJSONExpanded() is a strict superset of toOldJSONMinified(). + * + * @param {object} minified + * @param {object} expanded + * @param {string} [path] + */ +function assertExpandedSuperset(minified, expanded, path = '') { + for (const key of Object.keys(minified)) { + const keyPath = path ? `${path}.${key}` : key + expect(expanded, `missing key ${keyPath}`).to.have.property(key) + + const minifiedValue = minified[key] + const expandedValue = expanded[key] + + if (minifiedValue !== null && typeof minifiedValue === 'object' && !Array.isArray(minifiedValue)) { + assertExpandedSuperset(minifiedValue, expandedValue, keyPath) + } else if (Array.isArray(minifiedValue)) { + expect(expandedValue, `expected array at ${keyPath}`).to.be.an('array') + expect(expandedValue).to.deep.equal(minifiedValue) + } else { + expect(expandedValue, `value mismatch at ${keyPath}`).to.equal(minifiedValue) + } + } +} + +describe('old JSON serialization', () => { + let bookLibraryItemId + let podcastLibraryItemId + + beforeEach(async () => { + global.ServerSettings = {} + Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + await Database.buildModels() + + const bookLibrary = await Database.libraryModel.create({ name: 'Book Library', mediaType: 'book' }) + const bookLibraryFolder = await Database.libraryFolderModel.create({ path: '/books', libraryId: bookLibrary.id }) + + const book = await Database.bookModel.create({ + title: 'Test Book', + audioFiles: [{ index: 1, ino: '1', metadata: { filename: 'track.mp3', ext: '.mp3', path: '/track.mp3', relPath: 'track.mp3', size: 1000 } }], + tags: ['fiction'], + narrators: ['Narrator One'], + genres: ['Fantasy'], + chapters: [{ id: 0, start: 0, end: 100, title: 'Chapter 1' }], + ebookFile: { ino: '2', metadata: { filename: 'book.epub', ext: '.epub', path: '/book.epub', relPath: 'book.epub', size: 500 }, ebookFormat: 'epub' } + }) + const bookLibraryItem = await Database.libraryItemModel.create({ + libraryFiles: [{ ino: '1', metadata: { filename: 'track.mp3', ext: '.mp3', path: '/track.mp3', relPath: 'track.mp3' } }], + mediaId: book.id, + mediaType: 'book', + libraryId: bookLibrary.id, + libraryFolderId: bookLibraryFolder.id + }) + bookLibraryItemId = bookLibraryItem.id + + const author = await Database.authorModel.create({ name: 'Test Author', libraryId: bookLibrary.id }) + await Database.bookAuthorModel.create({ bookId: book.id, authorId: author.id }) + + const series = await Database.seriesModel.create({ name: 'Test Series', libraryId: bookLibrary.id }) + await Database.bookSeriesModel.create({ bookId: book.id, seriesId: series.id, sequence: '2' }) + + const podcastLibrary = await Database.libraryModel.create({ name: 'Podcast Library', mediaType: 'podcast' }) + const podcastLibraryFolder = await Database.libraryFolderModel.create({ path: '/podcasts', libraryId: podcastLibrary.id }) + + const podcast = await Database.podcastModel.create({ + title: 'Test Podcast', + tags: ['news'], + genres: ['Technology'], + autoDownloadEpisodes: false + }) + const podcastLibraryItem = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: podcast.id, + mediaType: 'podcast', + libraryId: podcastLibrary.id, + libraryFolderId: podcastLibraryFolder.id + }) + podcastLibraryItemId = podcastLibraryItem.id + + await Database.podcastEpisodeModel.create({ + podcastId: podcast.id, + title: 'Episode 1', + index: 1, + audioFile: { ino: '3', metadata: { filename: 'ep1.mp3', ext: '.mp3', path: '/ep1.mp3', relPath: 'ep1.mp3' } } + }) + }) + + afterEach(async () => { + await Database.sequelize.sync({ force: true }) + }) + + describe('Book', () => { + it('toOldJSONExpanded is a strict superset of toOldJSONMinified', async () => { + const libraryItem = await Database.libraryItemModel.getExpandedById(bookLibraryItemId) + const minified = libraryItem.media.toOldJSONMinified() + const expanded = libraryItem.media.toOldJSONExpanded(libraryItem.id) + + assertExpandedSuperset(minified, expanded) + + expect(expanded).to.have.property('libraryItemId', libraryItem.id) + expect(expanded).to.have.property('audioFiles') + expect(expanded).to.have.property('chapters') + expect(expanded).to.have.property('tracks') + expect(expanded.numTracks).to.equal(expanded.tracks.length) + expect(expanded.numAudioFiles).to.equal(expanded.audioFiles.length) + expect(expanded.numChapters).to.equal(expanded.chapters.length) + expect(expanded.ebookFormat).to.equal('epub') + }) + }) + + describe('Podcast', () => { + it('toOldJSONExpanded is a strict superset of toOldJSONMinified', async () => { + const libraryItem = await Database.libraryItemModel.getExpandedById(podcastLibraryItemId) + const minified = libraryItem.media.toOldJSONMinified() + const expanded = libraryItem.media.toOldJSONExpanded(libraryItem.id) + + assertExpandedSuperset(minified, expanded) + + expect(expanded).to.have.property('libraryItemId', libraryItem.id) + expect(expanded).to.have.property('episodes') + expect(expanded.numEpisodes).to.equal(expanded.episodes.length) + }) + }) + + describe('LibraryItem', () => { + it('book library item expanded is a strict superset of minified', async () => { + const libraryItem = await Database.libraryItemModel.getExpandedById(bookLibraryItemId) + const minified = libraryItem.toOldJSONMinified() + const expanded = libraryItem.toOldJSONExpanded() + + assertExpandedSuperset(minified, expanded) + + expect(expanded).to.have.property('libraryFiles') + expect(expanded).to.have.property('lastScan') + expect(expanded.numFiles).to.equal(expanded.libraryFiles.length) + expect(expanded.media.numTracks).to.equal(expanded.media.tracks.length) + }) + + it('podcast library item expanded is a strict superset of minified', async () => { + const libraryItem = await Database.libraryItemModel.getExpandedById(podcastLibraryItemId) + const minified = libraryItem.toOldJSONMinified() + const expanded = libraryItem.toOldJSONExpanded() + + assertExpandedSuperset(minified, expanded) + + expect(expanded).to.have.property('libraryFiles') + expect(expanded.media.numEpisodes).to.equal(expanded.media.episodes.length) + }) + }) +}) From 7ee5e1bdf61f26d5ad4a1c8a874cdca7e9bbb00f Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 9 Jul 2026 10:35:18 +0300 Subject: [PATCH 284/285] Emit author socket events when book scans create or link authors --- server/scanner/BookScanner.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 8ae616fe8..757b1814a 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -228,6 +228,11 @@ class BookScanner { bookId: media.id, authorId: existingAuthorId }) + const author = await Database.authorModel.findByPk(existingAuthorId) + if (author) { + const numBooks = await Database.bookAuthorModel.getCountForAuthor(existingAuthorId) + SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks)) + } libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added author "${authorName}"`) authorsUpdated = true } else { @@ -238,6 +243,7 @@ class BookScanner { }) await media.addAuthor(newAuthor) Database.addAuthorToFilterData(libraryItemData.libraryId, newAuthor.name, newAuthor.id) + SocketAuthority.emitter('author_added', newAuthor.toOldJSON()) libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new author "${authorName}"`) authorsUpdated = true } @@ -478,6 +484,8 @@ class BookScanner { } const createdAtTimestamp = new Date().getTime() + const linkedAuthorIds = new Set() + const newAuthorNames = new Set() if (bookMetadata.authors.length) { for (const authorName of bookMetadata.authors) { const matchingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName) @@ -485,6 +493,7 @@ class BookScanner { bookObject.bookAuthors.push({ authorId: matchingAuthorId }) + linkedAuthorIds.add(matchingAuthorId) } else { // New author bookObject.bookAuthors.push({ @@ -496,6 +505,7 @@ class BookScanner { lastFirst: Database.authorModel.getLastFirst(authorName) } }) + newAuthorNames.add(authorName) } } } @@ -593,6 +603,12 @@ class BookScanner { for (const ba of libraryItem.book.bookAuthors) { if (ba.author) { Database.addAuthorToFilterData(libraryItemData.libraryId, ba.author.name, ba.author.id) + if (newAuthorNames.has(ba.author.name)) { + SocketAuthority.emitter('author_added', ba.author.toOldJSON()) + } else if (linkedAuthorIds.has(ba.author.id)) { + const numBooks = await Database.bookAuthorModel.getCountForAuthor(ba.author.id) + SocketAuthority.emitter('author_updated', ba.author.toOldJSONExpanded(numBooks)) + } } } } From 9d9ab49664c24f03a740f8712f995e6c73e05f03 Mon Sep 17 00:00:00 2001 From: mikiher Date: Fri, 10 Jul 2026 17:37:31 +0300 Subject: [PATCH 285/285] Batch author numBooks updates at scan end via authors_num_books_updated --- server/models/BookAuthor.js | 28 ++++++++++++++++++- server/scanner/BookScanner.js | 42 +++++++++++++++++++++------- server/scanner/LibraryItemScanner.js | 12 +++++--- server/scanner/LibraryScan.js | 2 ++ server/scanner/LibraryScanner.js | 6 ++-- server/scanner/ScanLogger.js | 2 ++ 6 files changed, 75 insertions(+), 17 deletions(-) diff --git a/server/models/BookAuthor.js b/server/models/BookAuthor.js index d7d65728e..f2cbf60f0 100644 --- a/server/models/BookAuthor.js +++ b/server/models/BookAuthor.js @@ -1,4 +1,4 @@ -const { DataTypes, Model } = require('sequelize') +const { DataTypes, Model, fn, col } = require('sequelize') class BookAuthor extends Model { constructor(values, options) { @@ -37,6 +37,32 @@ class BookAuthor extends Model { }) } + /** + * Get number of books for each author + * + * @param {string[]} authorIds + * @returns {Promise>} + */ + static async getCountsForAuthors(authorIds) { + if (!authorIds.length) return {} + + const rows = await this.findAll({ + attributes: ['authorId', [fn('COUNT', col('id')), 'count']], + where: { + authorId: authorIds + }, + group: ['authorId'], + raw: true + }) + + /** @type {Record} */ + const counts = {} + for (const row of rows) { + counts[row.authorId] = Number(row.count) + } + return counts + } + /** * Initialize model * @param {import('../Database').sequelize} sequelize diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 757b1814a..2b106cdc8 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -228,11 +228,7 @@ class BookScanner { bookId: media.id, authorId: existingAuthorId }) - const author = await Database.authorModel.findByPk(existingAuthorId) - if (author) { - const numBooks = await Database.bookAuthorModel.getCountForAuthor(existingAuthorId) - SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks)) - } + libraryScan.authorsNumBooksChangedIds.add(existingAuthorId) libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added author "${authorName}"`) authorsUpdated = true } else { @@ -253,6 +249,7 @@ class BookScanner { for (const author of media.authors) { if (!bookMetadata.authors.includes(author.name)) { await author.bookAuthor.destroy() + libraryScan.authorsNumBooksChangedIds.add(author.id) libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed author "${author.name}"`) authorsUpdated = true bookAuthorsRemoved.push(author.id) @@ -484,7 +481,6 @@ class BookScanner { } const createdAtTimestamp = new Date().getTime() - const linkedAuthorIds = new Set() const newAuthorNames = new Set() if (bookMetadata.authors.length) { for (const authorName of bookMetadata.authors) { @@ -493,7 +489,8 @@ class BookScanner { bookObject.bookAuthors.push({ authorId: matchingAuthorId }) - linkedAuthorIds.add(matchingAuthorId) + // Existing author — only numBooks changes; batch emit at scan end + libraryScan.authorsNumBooksChangedIds.add(matchingAuthorId) } else { // New author bookObject.bookAuthors.push({ @@ -605,9 +602,6 @@ class BookScanner { Database.addAuthorToFilterData(libraryItemData.libraryId, ba.author.name, ba.author.id) if (newAuthorNames.has(ba.author.name)) { SocketAuthority.emitter('author_added', ba.author.toOldJSON()) - } else if (linkedAuthorIds.has(ba.author.id)) { - const numBooks = await Database.bookAuthorModel.getCountForAuthor(ba.author.id) - SocketAuthority.emitter('author_updated', ba.author.toOldJSONExpanded(numBooks)) } } } @@ -907,6 +901,34 @@ class BookScanner { }) } + /** + * Emit authors_num_books_updated for authors whose book count changed during a scan (linked or unlinked). + * Authors with no book links are omitted — orphans are handled by author_removed. + * @param {string} libraryId + * @param {import('./LibraryScan')|import('./ScanLogger')} scanLogger + * @returns {Promise} + */ + async emitAuthorsNumBooksUpdated(libraryId, scanLogger) { + if (!scanLogger.authorsNumBooksChangedIds?.size) return + + const authorIds = [...scanLogger.authorsNumBooksChangedIds] + scanLogger.authorsNumBooksChangedIds.clear() + + const countsByAuthorId = await Database.bookAuthorModel.getCountsForAuthors(authorIds) + + const authors = Object.entries(countsByAuthorId).map(([id, numBooks]) => ({ + id, + numBooks + })) + + if (!authors.length) return + + SocketAuthority.emitter('authors_num_books_updated', { + libraryId, + authors + }) + } + /** * Check authors that were removed from a book and remove them if they no longer have any books * keep authors without books that have a asin, description or imagePath diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index 501df4274..de95c9a09 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -66,7 +66,7 @@ class LibraryItemScanner { if (libraryItemDataUpdated || wasUpdated) { SocketAuthority.libraryItemEmitter('item_updated', expandedLibraryItem) - await this.checkAuthorsAndSeriesRemovedFromBooks(library.id, scanLogger) + await this.finalizeAuthorsAndSeriesFromScan(library.id, scanLogger) return ScanResult.UPDATED } @@ -76,12 +76,13 @@ class LibraryItemScanner { } /** - * Remove empty authors and series + * Emit author book count updates, then remove empty authors/series left after the scan * @param {string} libraryId * @param {ScanLogger} scanLogger * @returns {Promise} */ - async checkAuthorsAndSeriesRemovedFromBooks(libraryId, scanLogger) { + async finalizeAuthorsAndSeriesFromScan(libraryId, scanLogger) { + await BookScanner.emitAuthorsNumBooksUpdated(libraryId, scanLogger) if (scanLogger.authorsRemovedFromBooks.length) { await BookScanner.checkAuthorsRemovedFromBooks(libraryId, scanLogger) } @@ -215,7 +216,10 @@ class LibraryItemScanner { scanLogger.verbose = true scanLogger.setData('libraryItem', libraryItemScanData.relPath) - return this.scanNewLibraryItem(libraryItemScanData, library.settings, scanLogger) + const newLibraryItem = await this.scanNewLibraryItem(libraryItemScanData, library.settings, scanLogger) + // Watcher path does not go through full-library scan cleanup — emit author book count updates here + await BookScanner.emitAuthorsNumBooksUpdated(library.id, scanLogger) + return newLibraryItem } } module.exports = new LibraryItemScanner() diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js index e77f30ba6..0366addba 100644 --- a/server/scanner/LibraryScan.js +++ b/server/scanner/LibraryScan.js @@ -24,6 +24,8 @@ class LibraryScan { /** @type {string[]} */ this.authorsRemovedFromBooks = [] + /** @type {Set} */ + this.authorsNumBooksChangedIds = new Set() /** @type {string[]} */ this.seriesRemovedFromBooks = [] diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 640c82d76..73cb51553 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -234,8 +234,7 @@ class LibraryScanner { SocketAuthority.libraryItemsEmitter('items_updated', libraryItemsUpdated) } - // Authors and series that were removed from books should be removed if they are now empty - await LibraryItemScanner.checkAuthorsAndSeriesRemovedFromBooks(libraryScan.libraryId, libraryScan) + if (this.shouldCancelScan(libraryScan)) return true // Update missing library items if (libraryItemIdsMissing.length) { @@ -281,6 +280,9 @@ class LibraryScanner { } } + // Author book count updates and empty authors/series cleanup after all items are processed + await LibraryItemScanner.finalizeAuthorsAndSeriesFromScan(libraryScan.libraryId, libraryScan) + libraryScan.addLog(LogLevel.INFO, `Scan completed. ${libraryScan.resultStats}`) return false } diff --git a/server/scanner/ScanLogger.js b/server/scanner/ScanLogger.js index 7aed21f13..2748f1f3b 100644 --- a/server/scanner/ScanLogger.js +++ b/server/scanner/ScanLogger.js @@ -14,6 +14,8 @@ class ScanLogger { /** @type {string[]} */ this.authorsRemovedFromBooks = [] + /** @type {Set} */ + this.authorsNumBooksChangedIds = new Set() /** @type {string[]} */ this.seriesRemovedFromBooks = []