+
-
-
+
{{ $strings.LabelAutoFetchMetadata }}
@@ -33,13 +33,13 @@
-
-
{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}
+
+
{{ isDragging ? $strings.LabelUploaderDropFiles : isIOS ? $strings.LabelUploaderDragAndDropFilesOnly : $strings.LabelUploaderDragAndDrop }}
{{ $strings.MessageOr }}
{{ $strings.ButtonChooseFiles }}
- {{ $strings.ButtonChooseAFolder }}
+ {{ $strings.ButtonChooseAFolder }}
@@ -48,7 +48,7 @@
- {{ $strings.NoteUploaderFoldersWithMediaFiles }} {{ $strings.NoteUploaderOnlyAudioFiles }}
+ {{ $strings.NoteUploaderFoldersWithMediaFiles }} {{ $strings.NoteUploaderOnlyAudioFiles }}
@@ -84,8 +84,8 @@
-
-
+
+
@@ -127,6 +127,10 @@ export default {
})
return extensions
},
+ isIOS() {
+ const ua = window.navigator.userAgent
+ return /iPad|iPhone|iPod/.test(ua) && !window.MSStream
+ },
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
@@ -384,12 +388,6 @@ export default {
else itemsFailed++
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
}
- if (itemsUploaded) {
- this.$toast.success(`Successfully uploaded ${itemsUploaded} item${itemsUploaded > 1 ? 's' : ''}`)
- }
- if (itemsFailed) {
- this.$toast.success(`Failed to upload ${itemsFailed} item${itemsFailed > 1 ? 's' : ''}`)
- }
this.processing = false
this.uploadFinished = true
}
diff --git a/client/players/AudioTrack.js b/client/players/AudioTrack.js
index 78ddfd76b8..9627d3cdab 100644
--- a/client/players/AudioTrack.js
+++ b/client/players/AudioTrack.js
@@ -23,10 +23,6 @@ export default class AudioTrack {
get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
- if (process.env.NODE_ENV === 'development') {
- return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
- }
-
return this.contentUrl + `?token=${this.userToken}`
}
}
diff --git a/client/players/LocalAudioPlayer.js b/client/players/LocalAudioPlayer.js
index eb1484bb6c..7fc17e7aae 100644
--- a/client/players/LocalAudioPlayer.js
+++ b/client/players/LocalAudioPlayer.js
@@ -147,7 +147,7 @@ export default class LocalAudioPlayer extends EventEmitter {
timeoutRetry: {
maxNumRetry: 4,
retryDelayMs: 0,
- maxRetryDelayMs: 0,
+ maxRetryDelayMs: 0
},
errorRetry: {
maxNumRetry: 8,
@@ -160,7 +160,7 @@ export default class LocalAudioPlayer extends EventEmitter {
}
return retry
}
- },
+ }
}
}
}
@@ -194,7 +194,7 @@ export default class LocalAudioPlayer extends EventEmitter {
setDirectPlay() {
// Set initial track and track time offset
- var trackIndex = this.audioTracks.findIndex(t => this.startTime >= t.startOffset && this.startTime < (t.startOffset + t.duration))
+ var trackIndex = this.audioTracks.findIndex((t) => this.startTime >= t.startOffset && this.startTime < t.startOffset + t.duration)
this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
this.loadCurrentTrack()
@@ -270,7 +270,7 @@ export default class LocalAudioPlayer extends EventEmitter {
// Seeking Direct play
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
// Change Track
- var trackIndex = this.audioTracks.findIndex(t => time >= t.startOffset && time < (t.startOffset + t.duration))
+ var trackIndex = this.audioTracks.findIndex((t) => time >= t.startOffset && time < t.startOffset + t.duration)
if (trackIndex >= 0) {
this.startTime = time
this.currentTrackIndex = trackIndex
@@ -293,7 +293,6 @@ export default class LocalAudioPlayer extends EventEmitter {
this.player.volume = volume
}
-
// Utils
isValidDuration(duration) {
if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
@@ -338,4 +337,4 @@ export default class LocalAudioPlayer extends EventEmitter {
var last = bufferedRanges[bufferedRanges.length - 1]
return last.end
}
-}
\ No newline at end of file
+}
diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js
index a327f831f2..ba71fc6c7a 100644
--- a/client/players/PlayerHandler.js
+++ b/client/players/PlayerHandler.js
@@ -297,7 +297,6 @@ export default class PlayerHandler {
if (listeningTimeToAdd > 20) {
syncData = {
timeListened: listeningTimeToAdd,
- duration: this.getDuration(),
currentTime: this.getCurrentTime()
}
}
@@ -317,7 +316,6 @@ export default class PlayerHandler {
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
const syncData = {
timeListened: listeningTimeToAdd,
- duration: this.getDuration(),
currentTime
}
diff --git a/client/plugins/axios.js b/client/plugins/axios.js
index 4ea9b85b4c..c2ce8dad62 100644
--- a/client/plugins/axios.js
+++ b/client/plugins/axios.js
@@ -1,5 +1,5 @@
export default function ({ $axios, store, $config }) {
- $axios.onRequest(config => {
+ $axios.onRequest((config) => {
if (!config.url) {
console.error('Axios request invalid config', config)
return
@@ -13,14 +13,13 @@ export default function ({ $axios, store, $config }) {
}
if (process.env.NODE_ENV === 'development') {
- config.url = `/dev${config.url}`
console.log('Making request to ' + config.url)
}
})
- $axios.onError(error => {
+ $axios.onError((error) => {
const code = parseInt(error.response && error.response.status)
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
console.error('Axios error', code, message)
})
-}
\ No newline at end of file
+}
diff --git a/client/plugins/constants.js b/client/plugins/constants.js
index d89fbbbd6b..90c40b8c44 100644
--- a/client/plugins/constants.js
+++ b/client/plugins/constants.js
@@ -1,6 +1,6 @@
const SupportedFileTypes = {
image: ['png', 'jpg', 'jpeg', 'webp'],
- audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'],
+ audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpeg', 'mpg'],
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
info: ['nfo'],
text: ['txt'],
@@ -81,11 +81,9 @@ const Hotkeys = {
}
}
-export {
- Constants
-}
+export { Constants }
export default ({ app }, inject) => {
inject('constants', Constants)
inject('keynames', KeyNames)
inject('hotkeys', Hotkeys)
-}
\ No newline at end of file
+}
diff --git a/client/store/globals.js b/client/store/globals.js
index 8e98c56d63..65878fb445 100644
--- a/client/store/globals.js
+++ b/client/store/globals.js
@@ -72,13 +72,13 @@ export const state = () => ({
}
],
podcastTypes: [
- { text: 'Episodic', value: 'episodic' },
- { text: 'Serial', value: 'serial' }
+ { text: 'Episodic', value: 'episodic', descriptionKey: 'LabelEpisodic' },
+ { text: 'Serial', value: 'serial', descriptionKey: 'LabelSerial' }
],
episodeTypes: [
- { text: 'Full', value: 'full' },
- { text: 'Trailer', value: 'trailer' },
- { text: 'Bonus', value: 'bonus' }
+ { text: 'Full', value: 'full', descriptionKey: 'LabelFull' },
+ { text: 'Trailer', value: 'trailer', descriptionKey: 'LabelTrailer' },
+ { text: 'Bonus', value: 'bonus', descriptionKey: 'LabelBonus' }
],
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
})
@@ -98,13 +98,7 @@ export const getters = {
const userToken = rootGetters['user/getToken']
const lastUpdate = libraryItem.updatedAt || Date.now()
const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
-
- if (process.env.NODE_ENV !== 'production') {
- // Testing
- return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
- }
-
- return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
+ return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?ts=${lastUpdate}${raw ? '&raw=1' : ''}`
},
getLibraryItemCoverSrcById:
(state, getters, rootState, rootGetters) =>
@@ -112,11 +106,7 @@ export const getters = {
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
if (!libraryItemId) return placeholder
const userToken = rootGetters['user/getToken']
- if (process.env.NODE_ENV !== 'production') {
- // Testing
- return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
- }
- return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
+ return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
},
getIsBatchSelectingMediaItems: (state) => {
return state.selectedMediaItems.length
diff --git a/client/store/libraries.js b/client/store/libraries.js
index b92800baf2..8964d9f19b 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -240,7 +240,8 @@ export const mutations = {
series: [],
narrators: [],
languages: [],
- publishers: []
+ publishers: [],
+ publishedDecades: []
}
*/
const mediaMetadata = libraryItem.media.metadata
@@ -307,6 +308,16 @@ export const mutations = {
state.filterData.publishers.sort((a, b) => a.localeCompare(b))
}
+ // Add publishedDecades
+ if (mediaMetadata.publishedYear && !isNaN(mediaMetadata.publishedYear)) {
+ const publishedYear = parseInt(mediaMetadata.publishedYear, 10)
+ const decade = (Math.floor(publishedYear / 10) * 10).toString()
+ if (!state.filterData.publishedDecades.includes(decade)) {
+ state.filterData.publishedDecades.push(decade)
+ state.filterData.publishedDecades.sort((a, b) => a - b)
+ }
+ }
+
// Add language
if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) {
state.filterData.languages.push(mediaMetadata.language)
diff --git a/client/store/user.js b/client/store/user.js
index dab86bc197..5e1fa12e37 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -101,7 +101,7 @@ export const actions = {
if (state.settings.orderBy == 'media.metadata.publishedYear') {
settingsUpdate.orderBy = 'media.metadata.title'
}
- const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
+ const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
if (invalidFilters.includes(filterByFirstPart)) {
settingsUpdate.filterBy = 'all'
diff --git a/client/strings/ar.json b/client/strings/ar.json
new file mode 100644
index 0000000000..5bcd4c3a17
--- /dev/null
+++ b/client/strings/ar.json
@@ -0,0 +1,156 @@
+{
+ "ButtonAdd": "إضافة",
+ "ButtonAddChapters": "إضافة الفصول",
+ "ButtonAddDevice": "إضافة جهاز",
+ "ButtonAddLibrary": "إضافة مكتبة",
+ "ButtonAddPodcasts": "إضافة بودكاست",
+ "ButtonAddUser": "إضافة مستخدم",
+ "ButtonAddYourFirstLibrary": "أضف مكتبتك الأولى",
+ "ButtonApply": "حفظ",
+ "ButtonApplyChapters": "حفظ الفصول",
+ "ButtonAuthors": "المؤلفون",
+ "ButtonBack": "الرجوع",
+ "ButtonBrowseForFolder": "البحث عن المجلد",
+ "ButtonCancel": "إلغاء",
+ "ButtonCancelEncode": "إلغاء الترميز",
+ "ButtonChangeRootPassword": "تغيير كلمة المرور الرئيسية",
+ "ButtonCheckAndDownloadNewEpisodes": "التحقق من الحلقات الجديدة وتنزيلها",
+ "ButtonChooseAFolder": "اختر المجلد",
+ "ButtonChooseFiles": "اختر الملفات",
+ "ButtonClearFilter": "تصفية الفرز",
+ "ButtonCloseFeed": "إغلاق",
+ "ButtonCloseSession": "إغلاق الجلسة المفتوحة",
+ "ButtonCollections": "المجموعات",
+ "ButtonConfigureScanner": "إعدادات الماسح الضوئي",
+ "ButtonCreate": "إنشاء",
+ "ButtonCreateBackup": "إنشاء نسخة احتياطية",
+ "ButtonDelete": "حذف",
+ "ButtonDownloadQueue": "قائمة",
+ "ButtonEdit": "تعديل",
+ "ButtonEditChapters": "تعديل الفصول",
+ "ButtonEditPodcast": "تعديل البودكاست",
+ "ButtonEnable": "تفعيل",
+ "ButtonFireAndFail": "النار والفشل",
+ "ButtonFireOnTest": "حادثة إطلاق النار",
+ "ButtonForceReScan": "فرض إعادة المسح",
+ "ButtonFullPath": "المسار الكامل",
+ "ButtonHide": "إخفاء",
+ "ButtonHome": "الرئيسية",
+ "ButtonIssues": "مشاكل",
+ "ButtonJumpBackward": "اقفز للخلف",
+ "ButtonJumpForward": "اقفز للأمام",
+ "ButtonLatest": "أحدث",
+ "ButtonLibrary": "المكتبة",
+ "ButtonLogout": "تسجيل الخروج",
+ "ButtonLookup": "البحث",
+ "ButtonManageTracks": "إدارة المقاطع",
+ "ButtonMapChapterTitles": "مطابقة عناوين الفصول",
+ "ButtonMatchAllAuthors": "مطابقة كل المؤلفون",
+ "ButtonMatchBooks": "مطابقة الكتب",
+ "ButtonNevermind": "لا تهتم",
+ "ButtonNext": "التالي",
+ "ButtonNextChapter": "الفصل التالي",
+ "ButtonNextItemInQueue": "العنصر التالي في قائمة الانتظار",
+ "ButtonOk": "نعم",
+ "ButtonOpenFeed": "فتح التغذية",
+ "ButtonOpenManager": "فتح الإدارة",
+ "ButtonPause": "تَوَقَّف",
+ "ButtonPlay": "تشغيل",
+ "ButtonPlayAll": "تشغيل الكل",
+ "ButtonPlaying": "مشغل الآن",
+ "ButtonPlaylists": "قوائم التشغيل",
+ "ButtonPrevious": "سابِق",
+ "ButtonPreviousChapter": "الفصل السابق",
+ "ButtonProbeAudioFile": "فحص ملف الصوت",
+ "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": "تَحَقُق من المكتبة",
+ "ButtonSearch": "بحث",
+ "ButtonSelectFolderPath": "حدد مسار المجلد",
+ "ButtonSeries": "سلسلة",
+ "ButtonSetChaptersFromTracks": "تعيين الفصول من الملفات",
+ "ButtonShare": "نشر",
+ "ButtonShiftTimes": "أوقات العمل",
+ "ButtonShow": "عرض",
+ "ButtonStartM4BEncode": "ابدأ ترميز M4B",
+ "ButtonStartMetadataEmbed": "ابدأ تضمين البيانات الوصفية",
+ "ButtonStats": "الإحصائيات",
+ "ButtonSubmit": "تقديم",
+ "ButtonTest": "اختبار",
+ "ButtonUnlinkOpenId": "إلغاء ربط المعرف",
+ "ButtonUpload": "رفع",
+ "ButtonUploadBackup": "تحميل النسخة الاحتياطية",
+ "ButtonUploadCover": "ارفق الغلاف",
+ "ButtonUploadOPMLFile": "رفع ملف OPML",
+ "ButtonUserDelete": "حذف المستخدم {0}",
+ "ButtonUserEdit": "تعديل المستخدم {0}",
+ "ButtonViewAll": "عرض الكل",
+ "ButtonYes": "نعم",
+ "ErrorUploadFetchMetadataAPI": "خطأ في جلب البيانات الوصفية",
+ "ErrorUploadFetchMetadataNoResults": "لم يتم العثور على البيانات الوصفية - حاول تحديث العنوان و/أو المؤلف",
+ "ErrorUploadLacksTitle": "يجب أن يكون له عنوان",
+ "HeaderAccount": "الحساب",
+ "HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص",
+ "HeaderAdvanced": "متقدم",
+ "HeaderAppriseNotificationSettings": "إعدادات الإشعارات",
+ "HeaderAudioTracks": "المسارات الصوتية",
+ "HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية",
+ "HeaderAuthentication": "المصادقة",
+ "HeaderBackups": "النسخ الاحتياطية",
+ "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": "إدارة العلامات"
+}
diff --git a/client/strings/bg.json b/client/strings/bg.json
index 3c934d8933..8e124d063d 100644
--- a/client/strings/bg.json
+++ b/client/strings/bg.json
@@ -711,10 +711,8 @@
"PlaceholderNewPlaylist": "Ново име на плейлиста",
"PlaceholderSearch": "Търсене...",
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
- "ToastAccountUpdateFailed": "Неуспешно обновяване на акаунта",
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
- "ToastAuthorUpdateFailed": "Неуспешно обновяване на автора",
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
"ToastAuthorUpdateSuccess": "Автора обновен",
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)",
@@ -728,17 +726,13 @@
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
"ToastBookmarkCreateSuccess": "Отметката е създадена",
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
- "ToastBookmarkUpdateFailed": "Неуспешно обновяване на отметка",
"ToastBookmarkUpdateSuccess": "Отметката е обновена",
"ToastChaptersHaveErrors": "Главите имат грешки",
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
"ToastCollectionItemsRemoveSuccess": "Елемент(и) премахнати от колекция",
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
- "ToastCollectionUpdateFailed": "Неуспешно обновяване на колекция",
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
- "ToastItemCoverUpdateFailed": "Неуспешно обновяване на корица на елемент",
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
- "ToastItemDetailsUpdateFailed": "Неуспешно обновяване на детайли на елемент",
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като завършено",
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
@@ -750,12 +744,10 @@
"ToastLibraryDeleteSuccess": "Библиотеката е изтрита",
"ToastLibraryScanFailedToStart": "Неуспешно стартиране на сканиране",
"ToastLibraryScanStarted": "Сканирането на библиотеката е стартирано",
- "ToastLibraryUpdateFailed": "Неуспешно обновяване на библиотека",
"ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена",
"ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист",
"ToastPlaylistCreateSuccess": "Плейлистът е създаден",
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
- "ToastPlaylistUpdateFailed": "Неуспешно обновяване на плейлист",
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
"ToastPodcastCreateSuccess": "Подкастът е създаден",
diff --git a/client/strings/bn.json b/client/strings/bn.json
index d5856c1148..b705a802e3 100644
--- a/client/strings/bn.json
+++ b/client/strings/bn.json
@@ -8,7 +8,7 @@
"ButtonAddYourFirstLibrary": "আপনার প্রথম লাইব্রেরি যোগ করুন",
"ButtonApply": "প্রয়োগ করুন",
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
- "ButtonAuthors": "লেখক",
+ "ButtonAuthors": "লেখকগণ",
"ButtonBack": "পেছনে যান",
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
"ButtonCancel": "বাতিল করুন",
@@ -56,6 +56,7 @@
"ButtonOpenManager": "ম্যানেজার খুলুন",
"ButtonPause": "বিরতি",
"ButtonPlay": "বাজান",
+ "ButtonPlayAll": "সব চালান",
"ButtonPlaying": "বাজছে",
"ButtonPlaylists": "প্লেলিস্ট",
"ButtonPrevious": "পূর্ববর্তী",
@@ -549,7 +550,7 @@
"LabelSleepTimer": "স্লিপ টাইমার",
"LabelSlug": "স্লাগ",
"LabelStart": "শুরু",
- "LabelStartTime": "শুরু করার সময়",
+ "LabelStartTime": "শুরুর সময়",
"LabelStarted": "শুরু হয়েছে",
"LabelStartedAt": "এতে শুরু হয়েছে",
"LabelStatsAudioTracks": "অডিও ট্র্যাক",
@@ -702,7 +703,7 @@
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
"MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।",
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
- "MessageFetching": "আনয় হচ্ছে...",
+ "MessageFetching": "আনয় হচ্ছে.।",
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
"MessageImportantNotice": "গুরুত্বপূর্ণ বিজ্ঞপ্তি!",
"MessageInsertChapterBelow": "নীচে অধ্যায় ঢোকান",
@@ -710,7 +711,7 @@
"MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে",
"MessageJoinUsOn": "আমাদের সাথে যোগ দিন",
"MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন",
- "MessageLoading": "লোড হচ্ছে...",
+ "MessageLoading": "লোড হচ্ছে.।",
"MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...",
"MessageLogsDescription": "লগগুলি JSON ফাইল হিসাবে
/metadata/logs
-এ সংরক্ষণ করা হয়। ক্র্যাশ লগগুলি
/metadata/logs/crash_logs.txt
-এ সংরক্ষণ করা হয়।",
"MessageM4BFailed": "M4B ব্যর্থ!",
@@ -776,6 +777,38 @@
"MessageShareExpiresIn": "মেয়াদ শেষ হবে {0}",
"MessageShareURLWillBe": "শেয়ার করা ইউআরএল হবে
{0} ",
"MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?",
+ "MessageTaskAudioFileNotWritable": "অডিও ফাইল \"{0}\" লেখার যোগ্য নয়",
+ "MessageTaskCanceledByUser": "ব্যবহারকারী দ্বারা টাস্ক বাতিল করা হয়েছে",
+ "MessageTaskDownloadingEpisodeDescription": "\"{0}\" পর্ব ডাউনলোড করা হচ্ছে",
+ "MessageTaskEmbeddingMetadata": "মেটাডেটা এম্বেড করা হচ্ছে",
+ "MessageTaskEmbeddingMetadataDescription": "অডিওবুক \"{0}\" এ মেটাডেটা এম্বেড করা হচ্ছে",
+ "MessageTaskEncodingM4b": "এনকোডিং M4B",
+ "MessageTaskEncodingM4bDescription": "একটি একক m4b ফাইলে অডিওবুক \"{0}\" এনকোড করা হচ্ছে",
+ "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}টি পডকাস্ট যোগ করা হয়েছে",
+ "MessageTaskScanItemsAdded": "{0}টি করা হয়েছে",
+ "MessageTaskScanItemsMissing": "{0}টি অনুপস্থিত",
+ "MessageTaskScanItemsUpdated": "{0} টি আপডেট করা হয়েছে",
+ "MessageTaskScanNoChangesNeeded": "কোন পরিবর্তন প্রয়োজন নেই",
+ "MessageTaskScanningFileChanges": "\"{0}\" এ ফাইলের পরিবর্তন স্ক্যান করা হচ্ছে",
+ "MessageTaskScanningLibrary": "\"{0}\" লাইব্রেরি স্ক্যান করা হচ্ছে",
+ "MessageTaskTargetDirectoryNotWritable": "টার্গেট ডিরেক্টরি লেখার যোগ্য নয়",
"MessageThinking": "চিন্তা করছি...",
"MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ",
"MessageUploaderItemSuccess": "সফলভাবে আপলোড হয়েছে!",
@@ -816,14 +849,12 @@
"StatsTopNarrators": "শীর্ষ কথকগণ",
"StatsTotalDuration": "মোট সময়কাল…",
"StatsYearInReview": "বাৎসরিক পর্যালোচনা",
- "ToastAccountUpdateFailed": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
"ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে",
"ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে",
"ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে",
"ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি",
"ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে",
"ToastAuthorSearchNotFound": "লেখক পাওয়া যায়নি",
- "ToastAuthorUpdateFailed": "লেখক আপডেট করতে ব্যর্থ",
"ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে",
"ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন",
"ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)",
@@ -834,7 +865,6 @@
"ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে",
"ToastBackupInvalidMaxKeep": "রাখার জন্য অকার্যকর ব্যাকআপের সংখ্যা",
"ToastBackupInvalidMaxSize": "অকার্যকর সর্বোচ্চ ব্যাকআপ আকার",
- "ToastBackupPathUpdateFailed": "ব্যাকআপ পথ আপডেট করতে ব্যর্থ হয়েছে",
"ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ",
"ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ",
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
@@ -845,7 +875,6 @@
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
- "ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ",
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
"ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
"ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
@@ -856,7 +885,6 @@
"ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে",
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
- "ToastCollectionUpdateFailed": "সংগ্রহ আপডেট করতে ব্যর্থ",
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
"ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে",
"ToastDeleteFileFailed": "ফাইল মুছে ফেলতে ব্যর্থ হয়েছে",
@@ -865,8 +893,6 @@
"ToastDeviceNameAlreadyExists": "এই নামের ইরিডার ডিভাইস ইতিমধ্যেই বিদ্যমান",
"ToastDeviceTestEmailFailed": "পরীক্ষামূলক ইমেল পাঠাতে ব্যর্থ হয়েছে",
"ToastDeviceTestEmailSuccess": "পরীক্ষামূলক ইমেল পাঠানো হয়েছে",
- "ToastDeviceUpdateFailed": "ডিভাইস আপডেট করতে ব্যর্থ হয়েছে",
- "ToastEmailSettingsUpdateFailed": "ইমেল সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
"ToastEmailSettingsUpdateSuccess": "ইমেল সেটিংস আপডেট করা হয়েছে",
"ToastEncodeCancelFailed": "এনকোড বাতিল করতে ব্যর্থ হয়েছে",
"ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে",
@@ -875,21 +901,17 @@
"ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
"ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
"ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
- "ToastFailedToUpdateAccount": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
- "ToastFailedToUpdateUser": "ব্যবহারকারী আপডেট করতে ব্যর্থ",
+ "ToastFailedToUpdate": "আপডেট করতে ব্যর্থ হয়েছে",
"ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
"ToastInvalidUrl": "অকার্যকর ইউআরএল",
- "ToastItemCoverUpdateFailed": "আইটেম কভার আপডেট করতে ব্যর্থ হয়েছে",
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
"ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ",
"ToastItemDeletedSuccess": "মুছে ফেলা আইটেম",
- "ToastItemDetailsUpdateFailed": "আইটেমের বিবরণ আপডেট করতে ব্যর্থ",
"ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে",
"ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত",
"ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত",
- "ToastItemUpdateFailed": "আইটেম আপডেট করতে ব্যর্থ",
"ToastItemUpdateSuccess": "আইটেম আপডেট করা হয়েছে",
"ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ",
"ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে",
@@ -897,7 +919,6 @@
"ToastLibraryDeleteSuccess": "লাইব্রেরি মুছে ফেলা হয়েছে",
"ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ",
"ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে",
- "ToastLibraryUpdateFailed": "লাইব্রেরি আপডেট করতে ব্যর্থ",
"ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে",
"ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক",
"ToastNameRequired": "নাম আবশ্যক",
@@ -912,16 +933,13 @@
"ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ",
"ToastNotificationFailedMaximum": "সর্বাধিক ব্যর্থ প্রচেষ্টা >= 0 হতে হবে",
"ToastNotificationQueueMaximum": "সর্বাধিক বিজ্ঞপ্তি সারি >= 0 হতে হবে",
- "ToastNotificationSettingsUpdateFailed": "বিজ্ঞপ্তি সেটিংস আপডেট করতে ব্যর্থ",
"ToastNotificationSettingsUpdateSuccess": "বিজ্ঞপ্তি সেটিংস আপডেট করা হয়েছে",
"ToastNotificationTestTriggerFailed": "পরীক্ষামূলক বিজ্ঞপ্তি ট্রিগার করতে ব্যর্থ হয়েছে",
"ToastNotificationTestTriggerSuccess": "পরীক্ষামুলক বিজ্ঞপ্তি ট্রিগার হয়েছে",
- "ToastNotificationUpdateFailed": "বিজ্ঞপ্তি আপডেট করতে ব্যর্থ",
"ToastNotificationUpdateSuccess": "বিজ্ঞপ্তি আপডেট হয়েছে",
"ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ",
"ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে",
"ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে",
- "ToastPlaylistUpdateFailed": "প্লেলিস্ট আপডেট করতে ব্যর্থ",
"ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে",
"ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে",
@@ -950,7 +968,6 @@
"ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে",
"ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে",
"ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য",
- "ToastServerSettingsUpdateFailed": "সার্ভার সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
"ToastServerSettingsUpdateSuccess": "সার্ভার সেটিংস আপডেট করা হয়েছে",
"ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে",
"ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ",
@@ -961,7 +978,6 @@
"ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন",
"ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে",
"ToastSortingPrefixesEmptyError": "কমপক্ষে ১ টি সাজানোর উপসর্গ থাকতে হবে",
- "ToastSortingPrefixesUpdateFailed": "বাছাই উপসর্গ আপডেট করতে ব্যর্থ হয়েছে",
"ToastSortingPrefixesUpdateSuccess": "বাছাই করা উপসর্গ আপডেট করা হয়েছে ({0}টি আইটেম)",
"ToastTitleRequired": "শিরোনাম আবশ্যক",
"ToastUnknownError": "অজানা ত্রুটি",
diff --git a/client/strings/cs.json b/client/strings/cs.json
index 1f6237fa01..0dd6367c8b 100644
--- a/client/strings/cs.json
+++ b/client/strings/cs.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Vybrat soubory",
"ButtonClearFilter": "Vymazat filtr",
"ButtonCloseFeed": "Zavřít kanál",
+ "ButtonCloseSession": "Zavřít otevřenou relaci",
"ButtonCollections": "Kolekce",
"ButtonConfigureScanner": "Konfigurovat Prohledávání",
"ButtonCreate": "Vytvořit",
@@ -28,6 +29,9 @@
"ButtonEdit": "Upravit",
"ButtonEditChapters": "Upravit kapitoly",
"ButtonEditPodcast": "Upravit podcast",
+ "ButtonEnable": "Povolit",
+ "ButtonFireAndFail": "Spustit a selhat",
+ "ButtonFireOnTest": "Spustit událost onTest",
"ButtonForceReScan": "Vynutit opětovné prohledání",
"ButtonFullPath": "Úplná cesta",
"ButtonHide": "Skrýt",
@@ -44,19 +48,26 @@
"ButtonMatchAllAuthors": "Spárovat všechny autory",
"ButtonMatchBooks": "Spárovat Knihy",
"ButtonNevermind": "Nevadí",
+ "ButtonNext": "Další",
"ButtonNextChapter": "Další Kapitola",
+ "ButtonNextItemInQueue": "Žádná další položka ve frontě",
+ "ButtonOk": "Ok",
"ButtonOpenFeed": "Otevřít kanál",
"ButtonOpenManager": "Otevřít správce",
+ "ButtonPause": "Pozastavit",
"ButtonPlay": "Přehrát",
+ "ButtonPlayAll": "Přehrát vše",
"ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb",
"ButtonPrevious": "Předchozí",
"ButtonPreviousChapter": "Předchozí Kapitola",
+ "ButtonProbeAudioFile": "Prozkoumat audio soubor",
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
"ButtonQueueAddItem": "Přidat do fronty",
"ButtonQueueRemoveItem": "Odstranit z fronty",
- "ButtonQuickEmbedMetadata": "Rychle Zapsat Metadata",
+ "ButtonQuickEmbed": "Rychle Zapsat",
+ "ButtonQuickEmbedMetadata": "Rychle zapsat Metadata",
"ButtonQuickMatch": "Rychlé přiřazení",
"ButtonReScan": "Znovu prohledat",
"ButtonRead": "Číst",
@@ -88,6 +99,8 @@
"ButtonStartMetadataEmbed": "Spustit vkládání metadat",
"ButtonStats": "Statistiky",
"ButtonSubmit": "Odeslat",
+ "ButtonTest": "Test",
+ "ButtonUnlinkOpenId": "Odpojit OpenID",
"ButtonUpload": "Nahrát",
"ButtonUploadBackup": "Nahrát zálohu",
"ButtonUploadCover": "Nahrát obálku",
@@ -100,10 +113,12 @@
"ErrorUploadFetchMetadataNoResults": "Nepodařilo se načíst metadata - zkuste aktualizovat název a/nebo autora",
"ErrorUploadLacksTitle": "Musí mít titul",
"HeaderAccount": "Účet",
+ "HeaderAddCustomMetadataProvider": "Přidat vlastního poskytovatele metadat",
"HeaderAdvanced": "Pokročilé",
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
"HeaderAudioTracks": "Zvukové stopy",
"HeaderAudiobookTools": "Nástroje pro správu souborů audioknih",
+ "HeaderAuthentication": "Autentizace",
"HeaderBackups": "Zálohy",
"HeaderChangePassword": "Změnit heslo",
"HeaderChapters": "Kapitoly",
@@ -144,10 +159,14 @@
"HeaderMetadataToEmbed": "Metadata k vložení",
"HeaderNewAccount": "Nový účet",
"HeaderNewLibrary": "Nová knihovna",
+ "HeaderNotificationCreate": "Vytvořit notifikaci",
+ "HeaderNotificationUpdate": "Aktualizovat notifikaci",
"HeaderNotifications": "Oznámení",
"HeaderOpenIDConnectAuthentication": "Ověřování pomocí OpenID Connect",
+ "HeaderOpenListeningSessions": "Otevřené relace přehrávače",
"HeaderOpenRSSFeed": "Otevřít RSS kanál",
"HeaderOtherFiles": "Ostatní soubory",
+ "HeaderPasswordAuthentication": "Autentizace heslem",
"HeaderPermissions": "Oprávnění",
"HeaderPlayerQueue": "Fronta přehrávače",
"HeaderPlayerSettings": "Nastavení přehrávače",
@@ -162,6 +181,7 @@
"HeaderRemoveEpisodes": "Odstranit {0} epizody",
"HeaderSavedMediaProgress": "Průběh uložených médií",
"HeaderSchedule": "Plán",
+ "HeaderScheduleEpisodeDownloads": "Naplánovat automatické stahování epizod",
"HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven",
"HeaderSession": "Relace",
"HeaderSetBackupSchedule": "Nastavit plán zálohování",
@@ -200,13 +220,18 @@
"LabelAddToPlaylist": "Přidat do seznamu přehrávání",
"LabelAddToPlaylistBatch": "Přidat {0} položky do seznamu přehrávání",
"LabelAddedAt": "Přidáno v",
+ "LabelAddedDate": "Přidáno {0}",
"LabelAdminUsersOnly": "Pouze administrátoři",
"LabelAll": "Vše",
"LabelAllUsers": "Všichni uživatelé",
"LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů",
"LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů",
"LabelAlreadyInYourLibrary": "Již ve vaší knihovně",
+ "LabelApiToken": "API Token",
"LabelAppend": "Připojit",
+ "LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
+ "LabelAudioChannels": "Zvukové kanály (1 nebo 2)",
+ "LabelAudioCodec": "Kodek audia",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (jméno a příjmení)",
"LabelAuthorLastFirst": "Autor (příjmení a jméno)",
@@ -219,6 +244,7 @@
"LabelAutoRegister": "Automatická registrace",
"LabelAutoRegisterDescription": "Automaticky vytvářet nové uživatele po přihlášení",
"LabelBackToUser": "Zpět k uživateli",
+ "LabelBackupAudioFiles": "Zálohovat zvukové soubory",
"LabelBackupLocation": "Umístění zálohy",
"LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování",
"LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups",
@@ -227,10 +253,13 @@
"LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat",
"LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.",
"LabelBitrate": "Datový tok",
+ "LabelBonus": "Bonus",
"LabelBooks": "Knihy",
"LabelButtonText": "Text tlačítka",
+ "LabelByAuthor": "od {0}",
"LabelChangePassword": "Změnit heslo",
"LabelChannels": "Kanály",
+ "LabelChapterCount": "{0} Kapitol",
"LabelChapterTitle": "Název kapitoly",
"LabelChapters": "Kapitoly",
"LabelChaptersFound": "Kapitoly nalezeny",
@@ -238,6 +267,7 @@
"LabelClosePlayer": "Zavřít přehrávač",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Sbalit sérii",
+ "LabelCollapseSubSeries": "Sbalit podsérie",
"LabelCollection": "Kolekce",
"LabelCollections": "Kolekce",
"LabelComplete": "Dokončeno",
@@ -288,16 +318,21 @@
"LabelEpisode": "Epizoda",
"LabelEpisodeTitle": "Název epizody",
"LabelEpisodeType": "Typ epizody",
+ "LabelEpisodes": "Epizody",
"LabelExample": "Příklad",
"LabelExpandSeries": "Rozbalit série",
+ "LabelExpandSubSeries": "Rozbalit podsérie",
"LabelExplicit": "Explicitní",
"LabelExplicitChecked": "Explicitní (zaškrtnuto)",
"LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)",
+ "LabelExportOPML": "Export OPML",
"LabelFeedURL": "URL zdroje",
"LabelFetchingMetadata": "Získávání metadat",
"LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vzniku souboru",
+ "LabelFileBornDate": "Vytvořeno {0}",
"LabelFileModified": "Soubor změněn",
+ "LabelFileModifiedDate": "Změněno {0}",
"LabelFilename": "Název souboru",
"LabelFilterByUser": "Filtrovat podle uživatele",
"LabelFindEpisodes": "Najít epizody",
@@ -307,6 +342,7 @@
"LabelFontBold": "Tučně",
"LabelFontBoldness": "Výraznost písma",
"LabelFontFamily": "Rodina písem",
+ "LabelFontItalic": "Kurzíva",
"LabelFontScale": "Měřítko písma",
"LabelFontStrikethrough": "Přeškrtnutí",
"LabelFormat": "Formát",
@@ -325,6 +361,7 @@
"LabelInProgress": "Probíhá",
"LabelIncludeInTracklist": "Zahrnout do seznamu stop",
"LabelIncomplete": "Neúplné",
+ "LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Vlastní denně/týdně",
"LabelIntervalEvery12Hours": "Každých 12 hodin",
"LabelIntervalEvery15Minutes": "Každých 15 minut",
@@ -421,17 +458,22 @@
"LabelPersonalYearReview": "Váš přehled roku ({0})",
"LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlayMethod": "Metoda přehrávání",
+ "LabelPlayerChapterNumberMarker": "{0} z {1}",
"LabelPlaylists": "Seznamy skladeb",
+ "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Oblast vyhledávání podcastu",
"LabelPodcastType": "Typ podcastu",
"LabelPodcasts": "Podcasty",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)",
"LabelPreventIndexing": "Zabránit indexování vašeho kanálu v adresářích podcastů iTunes a Google",
"LabelPrimaryEbook": "Hlavní e-kniha",
"LabelProgress": "Průběh",
"LabelProvider": "Poskytovatel",
+ "LabelProviderAuthorizationValue": "Hodnota autorizačního headeru",
"LabelPubDate": "Datum vydání",
"LabelPublishYear": "Rok vydání",
+ "LabelPublishedDate": "Vydáno {0}",
"LabelPublisher": "Vydavatel",
"LabelPublishers": "Vydavatelé",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
@@ -441,6 +483,7 @@
"LabelRSSFeedSlug": "RSS kanál Slug",
"LabelRSSFeedURL": "URL RSS kanálu",
"LabelRandomly": "Náhodně",
+ "LabelReAddSeriesToContinueListening": "Znovu přidat sérii k pokračování poslechu",
"LabelRead": "Číst",
"LabelReadAgain": "Číst znovu",
"LabelReadEbookWithoutProgress": "Číst e-knihu bez zachování průběhu",
@@ -448,6 +491,7 @@
"LabelRecentlyAdded": "Nedávno přidané",
"LabelRecommended": "Doporučeno",
"LabelRedo": "Přepracovat",
+ "LabelRegion": "Region",
"LabelReleaseDate": "Datum vydání",
"LabelRemoveCover": "Odstranit obálku",
"LabelRowsPerPage": "Řádky na stránku",
@@ -539,6 +583,7 @@
"LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli",
"LabelTasks": "Spuštěné Úlohy",
"LabelTextEditorBulletedList": "Seznam s odrážkami",
+ "LabelTextEditorLink": "Odkaz",
"LabelTextEditorNumberedList": "Seznam s čísly",
"LabelTextEditorUnlink": "Zrušit odkaz",
"LabelTheme": "Téma",
@@ -572,6 +617,7 @@
"LabelUnabridged": "Nezkráceno",
"LabelUndo": "Zpět",
"LabelUnknown": "Neznámý",
+ "LabelUnknownPublishDate": "Neznámé datum vydání",
"LabelUpdateCover": "Aktualizovat obálku",
"LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda",
"LabelUpdateDetails": "Aktualizovat podrobnosti",
@@ -620,14 +666,19 @@
"MessageCheckingCron": "Kontrola cronu...",
"MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?",
"MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?",
+ "MessageConfirmDeleteDevice": "Opravdu chcete vymazat zařízení e-reader \"{0}\"?",
"MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?",
"MessageConfirmDeleteLibrary": "Opravdu chcete trvale smazat knihovnu \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Tento krok odstraní položku knihovny z databáze a vašeho souborového systému. Jste si jisti?",
"MessageConfirmDeleteLibraryItems": "Tímto smažete {0} položkek knihovny z databáze a vašeho souborového systému. Jsi si jisti?",
+ "MessageConfirmDeleteMetadataProvider": "Opravdu chcete vymazat vlastního poskytovatele metadat \"{0}\"?",
+ "MessageConfirmDeleteNotification": "Opravdu chcete vymazat tuto notifikaci?",
"MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?",
"MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?",
"MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?",
"MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?",
+ "MessageConfirmMarkItemFinished": "Opravdu chcete označit \"{0}\" jako dokončené?",
+ "MessageConfirmMarkItemNotFinished": "Opravdu chcete označit \"{0}\" jako nedokončené?",
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
"MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese
/metadata/cache
.
Určitě chcete odstranit adresář mezipaměti?",
@@ -648,7 +699,9 @@
"MessageConfirmRenameTag": "Opravdu chcete přejmenovat tag \"{0}\" na \"{1}\" pro všechny položky?",
"MessageConfirmRenameTagMergeNote": "Poznámka: Tato značka již existuje, takže budou sloučeny.",
"MessageConfirmRenameTagWarning": "Varování! Podobná značka s jinými velkými a malými písmeny již existuje \"{0}\".",
+ "MessageConfirmResetProgress": "Opravdu chcete zahodit svůj pokrok?",
"MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?",
+ "MessageConfirmUnlinkOpenId": "Opravdu chcete odpojit tohoto uživatele z OpenID?",
"MessageDownloadingEpisode": "Stahuji epizodu",
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
"MessageEmbedFailed": "Vložení selhalo!",
@@ -656,7 +709,7 @@
"MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení",
"MessageEreaderDevices": "Aby bylo zajištěno doručení elektronických knih, může být nutné přidat výše uvedenou e-mailovou adresu jako platného odesílatele pro každé zařízení uvedené níže.",
"MessageFeedURLWillBe": "URL zdroje bude {0}",
- "MessageFetching": "Stahování...",
+ "MessageFetching": "Načítání...",
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
"MessageImportantNotice": "Důležité upozornění!",
"MessageInsertChapterBelow": "Vložit kapitolu níže",
@@ -683,6 +736,7 @@
"MessageNoCollections": "Žádné kolekce",
"MessageNoCoversFound": "Nebyly nalezeny žádné obálky",
"MessageNoDescription": "Bez popisu",
+ "MessageNoDevices": "Žádná zařízení",
"MessageNoDownloadsInProgress": "Momentálně neprobíhá žádné stahování",
"MessageNoDownloadsQueued": "Žádné stahování ve frontě",
"MessageNoEpisodeMatchesFound": "Nebyly nalezeny žádné odpovídající epizody",
@@ -710,6 +764,7 @@
"MessagePauseChapter": "Pozastavit přehrávání kapitoly",
"MessagePlayChapter": "Poslechnout si začátek kapitoly",
"MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce",
+ "MessagePleaseWait": "Čekejte prosím...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání",
"MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".",
"MessageRemoveChapter": "Odstranit kapitolu",
@@ -728,17 +783,46 @@
"MessageShareExpiresIn": "Expiruje za {0}",
"MessageShareURLWillBe": "Sdílené URL bude
{0} ",
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
+ "MessageTaskAudioFileNotWritable": "Nelze zapisovat do audio souboru \"{0}\"",
+ "MessageTaskCanceledByUser": "Task zrušen uživatelem",
+ "MessageTaskDownloadingEpisodeDescription": "Stahování epizody \"{0}\"",
+ "MessageTaskEmbeddingMetadata": "Vkládání metadat",
+ "MessageTaskEmbeddingMetadataDescription": "Vkládání metadat do audioknihy \"{0}\"",
+ "MessageTaskEncodingM4b": "Kódování M4B",
+ "MessageTaskEncodingM4bDescription": "Kódování audioknihy \"{0}\" do jednoho m4b souboru",
+ "MessageTaskFailed": "Selhalo",
+ "MessageTaskFailedToBackupAudioFile": "Zálohování audio souboru \"{0}\" se selhalo",
+ "MessageTaskFailedToCreateCacheDirectory": "Vytvoření cache adresáře selhalo",
+ "MessageTaskFailedToEmbedMetadataInFile": "Vkládání metadat do souboru \"{0}\" selhalo",
+ "MessageTaskFailedToMergeAudioFiles": "Spojení audio souborů selhalo",
+ "MessageTaskFailedToMoveM4bFile": "Přesunutí m4b souboru selhalo",
+ "MessageTaskFailedToWriteMetadataFile": "Zápis souboru metadat selhal",
+ "MessageTaskNoFilesToScan": "Žádné soubory ke skenování",
+ "MessageTaskOpmlImport": "Import OPML",
+ "MessageTaskOpmlImportDescription": "Vytváření podcastů z {0} RSS feedů",
+ "MessageTaskOpmlImportFeedDescription": "Importování RSS feedu \"{0}\"",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Vytváření podcastu \"{0}\"",
+ "MessageTaskOpmlImportFeedPodcastExists": "Podcast se stejnou cestou již existuje",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Vytváření podcastu selhalo",
+ "MessageTaskOpmlImportFinished": "Přidáno {0} podcastů",
+ "MessageTaskScanItemsAdded": "{0} přidáno",
+ "MessageTaskScanItemsMissing": "{0} chybí",
+ "MessageTaskScanItemsUpdated": "{0} aktualizováno",
+ "MessageTaskScanNoChangesNeeded": "Žádné změny nejsou nutné",
+ "MessageTaskScanningFileChanges": "Skenování změn souborů v \"{0}\"",
+ "MessageTaskScanningLibrary": "Skenování \"{0}\" knihovny",
+ "MessageTaskTargetDirectoryNotWritable": "Do cílové složky nelze zapisovat",
"MessageThinking": "Přemýšlení...",
- "MessageUploaderItemFailed": "Nahrávání se nezdařilo",
- "MessageUploaderItemSuccess": "Nahráno bylo úspěšně!",
- "MessageUploading": "Odesílám...",
+ "MessageUploaderItemFailed": "Nahrávání selhalo",
+ "MessageUploaderItemSuccess": "Úspěšně nahráno!",
+ "MessageUploading": "Nahrávám...",
"MessageValidCronExpression": "Platný výraz cronu",
"MessageWatcherIsDisabledGlobally": "Hlídač je globálně zakázán v nastavení serveru",
"MessageXLibraryIsEmpty": "{0} knihovna je prázdná!",
- "MessageYourAudiobookDurationIsLonger": "Doba trvání audioknihy je delší než nalezená délka",
+ "MessageYourAudiobookDurationIsLonger": "Délka audioknihy je delší, než byla nalezena",
"MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena",
"NoteChangeRootPassword": "Uživatel root je jediný uživatel, který může mít prázdné heslo",
- "NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat v 0:00 a čas začátku poslední kapitoly nesmí překročit tuto dobu trvání audioknihy.",
+ "NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat na 0:00 a čas začátku poslední kapitoly nesmí překročit dobu trvání audioknihy.",
"NoteFolderPicker": "Poznámka: složky, které jsou již namapovány, nebudou zobrazeny",
"NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.",
@@ -752,8 +836,10 @@
"PlaceholderSearchEpisode": "Hledat epizodu..",
"StatsAuthorsAdded": "autoři přidáni",
"StatsBooksAdded": "knihy přidány",
+ "StatsBooksAdditional": "Některé další zahrnují…",
"StatsBooksFinished": "dokončené knihy",
"StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…",
+ "StatsCollectionGrewTo": "Vaše kolekce knih se rozrostla na…",
"StatsSessions": "sezení",
"StatsSpentListening": "stráveno posloucháním",
"StatsTopAuthor": "TOP AUTOR",
@@ -763,59 +849,75 @@
"StatsTopMonth": "TOP MĚSÍC",
"StatsTotalDuration": "S celkovou dobou…",
"StatsYearInReview": "ROK V PŘEHLEDU",
- "ToastAccountUpdateFailed": "Aktualizace účtu se nezdařila",
"ToastAccountUpdateSuccess": "Účet aktualizován",
+ "ToastAppriseUrlRequired": "Je nutné zadat Apprise URL",
"ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn",
- "ToastAuthorUpdateFailed": "Aktualizace autora se nezdařila",
+ "ToastAuthorNotFound": "Author \"{0}\" nenalezen",
+ "ToastAuthorRemoveSuccess": "Autor odstraněn",
+ "ToastAuthorSearchNotFound": "Autor nenalezen",
"ToastAuthorUpdateMerged": "Autor sloučen",
"ToastAuthorUpdateSuccess": "Autor aktualizován",
"ToastAuthorUpdateSuccessNoImageFound": "Autor aktualizován (nebyl nalezen žádný obrázek)",
+ "ToastBackupAppliedSuccess": "Záloha obnovena",
"ToastBackupCreateFailed": "Vytvoření zálohy se nezdařilo",
"ToastBackupCreateSuccess": "Záloha vytvořena",
"ToastBackupDeleteFailed": "Nepodařilo se smazat zálohu",
"ToastBackupDeleteSuccess": "Záloha smazána",
+ "ToastBackupInvalidMaxKeep": "Neplatný počet záloh k zachování",
+ "ToastBackupInvalidMaxSize": "Neplatná maximální velikost zálohy",
"ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu",
"ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu",
"ToastBackupUploadSuccess": "Záloha nahrána",
+ "ToastBatchDeleteFailed": "Hromadné smazání selhalo",
+ "ToastBatchDeleteSuccess": "Hromadné smazání proběhlo úspěšně",
"ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila",
"ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně",
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
"ToastBookmarkCreateSuccess": "Přidána záložka",
"ToastBookmarkRemoveSuccess": "Záložka odstraněna",
- "ToastBookmarkUpdateFailed": "Aktualizace záložky se nezdařila",
"ToastBookmarkUpdateSuccess": "Záložka aktualizována",
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
+ "ToastChaptersRemoved": "Kapitoly odstraněny",
"ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce",
"ToastCollectionRemoveSuccess": "Kolekce odstraněna",
- "ToastCollectionUpdateFailed": "Aktualizace kolekce se nezdařila",
"ToastCollectionUpdateSuccess": "Kolekce aktualizována",
+ "ToastCoverUpdateFailed": "Aktualizace obálky selhala",
"ToastDeleteFileFailed": "Nepodařilo se smazat soubor",
"ToastDeleteFileSuccess": "Soubor smazán",
+ "ToastDeviceAddFailed": "Přidání zařízení selhalo",
+ "ToastDeviceNameAlreadyExists": "Zařízení se stejným jménem již existuje",
+ "ToastDeviceTestEmailFailed": "Odeslání testovacího emailu selhalo",
+ "ToastDeviceTestEmailSuccess": "Testovací email byl odeslán",
+ "ToastEmailSettingsUpdateSuccess": "Nastavení emailu aktualizována",
+ "ToastEpisodeDownloadQueueClearFailed": "Vyčištění fronty selhalo",
"ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
"ToastFailedToLoadData": "Nepodařilo se načíst data",
- "ToastItemCoverUpdateFailed": "Aktualizace obálky se nezdařila",
+ "ToastFailedToShare": "Sdílení selhalo",
+ "ToastFailedToUpdate": "Aktualizace selhala",
+ "ToastInvalidImageUrl": "Neplatná URL obrázku",
+ "ToastInvalidUrl": "Neplatná URL",
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
- "ToastItemDetailsUpdateFailed": "Nepodařilo se aktualizovat podrobnosti o položce",
+ "ToastItemDeletedFailed": "Smazání položky selhalo",
+ "ToastItemDeletedSuccess": "Položka smazána",
"ToastItemDetailsUpdateSuccess": "Podrobnosti o položce byly aktualizovány",
"ToastItemMarkedAsFinishedFailed": "Nepodařilo se označit jako dokončené",
"ToastItemMarkedAsFinishedSuccess": "Položka označena jako dokončená",
"ToastItemMarkedAsNotFinishedFailed": "Nepodařilo se označit jako nedokončené",
"ToastItemMarkedAsNotFinishedSuccess": "Položka označena jako nedokončená",
+ "ToastItemUpdateSuccess": "Položka aktualizována",
"ToastLibraryCreateFailed": "Vytvoření knihovny se nezdařilo",
"ToastLibraryCreateSuccess": "Knihovna \"{0}\" vytvořena",
"ToastLibraryDeleteFailed": "Nepodařilo se smazat knihovnu",
"ToastLibraryDeleteSuccess": "Knihovna smazána",
"ToastLibraryScanFailedToStart": "Nepodařilo se spustit kontrolu",
"ToastLibraryScanStarted": "Kontrola knihovny spuštěna",
- "ToastLibraryUpdateFailed": "Aktualizace knihovny se nezdařila",
"ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována",
"ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo",
"ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen",
"ToastPlaylistRemoveSuccess": "Seznam přehrávání odstraněn",
- "ToastPlaylistUpdateFailed": "Aktualizace seznamu přehrávání se nezdařila",
"ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován",
"ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo",
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
@@ -827,7 +929,6 @@
"ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"",
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
- "ToastServerSettingsUpdateFailed": "Nepodařilo se aktualizovat nastavení serveru",
"ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno",
"ToastSessionDeleteFailed": "Nepodařilo se smazat relaci",
"ToastSessionDeleteSuccess": "Relace smazána",
@@ -835,7 +936,6 @@
"ToastSocketDisconnected": "Socket odpojen",
"ToastSocketFailedToConnect": "Socket se nepodařilo připojit",
"ToastSortingPrefixesEmptyError": "Musí mít alespoň 1 třídicí předponu",
- "ToastSortingPrefixesUpdateFailed": "Nepodařilo se aktualizovat třídicí předpony",
"ToastSortingPrefixesUpdateSuccess": "Aktualizovány předpony třídění ({0} položek)",
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
"ToastUserDeleteSuccess": "Uživatel smazán"
diff --git a/client/strings/da.json b/client/strings/da.json
index 2e055b42a3..b4b92bc86f 100644
--- a/client/strings/da.json
+++ b/client/strings/da.json
@@ -1,7 +1,10 @@
{
"ButtonAdd": "Tilføj",
"ButtonAddChapters": "Tilføj kapitler",
+ "ButtonAddDevice": "Tilføj enhed",
+ "ButtonAddLibrary": "Tilføj Bibliotek",
"ButtonAddPodcasts": "Tilføj podcasts",
+ "ButtonAddUser": "Tilføj bruger",
"ButtonAddYourFirstLibrary": "Tilføj din første bibliotek",
"ButtonApply": "Anvend",
"ButtonApplyChapters": "Anvend kapitler",
@@ -25,6 +28,7 @@
"ButtonEdit": "Rediger",
"ButtonEditChapters": "Rediger kapitler",
"ButtonEditPodcast": "Rediger podcast",
+ "ButtonEnable": "Aktiver",
"ButtonForceReScan": "Tvungen genindlæsning",
"ButtonFullPath": "Fuld sti",
"ButtonHide": "Skjul",
@@ -42,6 +46,7 @@
"ButtonOk": "OK",
"ButtonOpenFeed": "Åbn feed",
"ButtonOpenManager": "Åbn manager",
+ "ButtonPause": "Pause",
"ButtonPlay": "Afspil",
"ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister",
@@ -66,7 +71,7 @@
"ButtonScanLibrary": "Scan Bibliotek",
"ButtonSearch": "Søg",
"ButtonSelectFolderPath": "Vælg Mappen Sti",
- "ButtonSeries": "Serie",
+ "ButtonSeries": "Serier",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
"ButtonShiftTimes": "Skift Tider",
"ButtonShow": "Vis",
@@ -188,14 +193,14 @@
"LabelChapters": "Kapitler",
"LabelChaptersFound": "fundne kapitler",
"LabelClosePlayer": "Luk afspiller",
- "LabelCollapseSeries": "Fold Serie Sammen",
+ "LabelCollapseSeries": "Fold Serier Sammen",
"LabelCollection": "Samling",
"LabelCollections": "Samlinger",
"LabelComplete": "Fuldfør",
"LabelConfirmPassword": "Bekræft Adgangskode",
- "LabelContinueListening": "Fortsæt Lytning",
- "LabelContinueReading": "Fortsæt Læsning",
- "LabelContinueSeries": "Fortsæt Serie",
+ "LabelContinueListening": "Fortsæt med at lytte",
+ "LabelContinueReading": "Fortsæt med at læse",
+ "LabelContinueSeries": "Fortsæt Serien",
"LabelCover": "Omslag",
"LabelCoverImageURL": "Omslagsbillede URL",
"LabelCreatedAt": "Oprettet Kl.",
@@ -212,6 +217,7 @@
"LabelDiscFromFilename": "Disk fra Filnavn",
"LabelDiscFromMetadata": "Disk fra Metadata",
"LabelDiscover": "Opdag",
+ "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episoder",
"LabelDuration": "Varighed",
"LabelDurationFound": "Fundet varighed:",
@@ -225,12 +231,15 @@
"LabelEmbeddedCover": "Indlejret Omslag",
"LabelEnable": "Aktivér",
"LabelEnd": "Slut",
+ "LabelEndOfChapter": "Slutningen af kapitel",
+ "LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episodetitel",
"LabelEpisodeType": "Episodetype",
"LabelExample": "Eksempel",
"LabelExplicit": "Eksplisit",
+ "LabelFeedURL": "Feed URL",
"LabelFile": "Fil",
- "LabelFileBirthtime": "Fødselstidspunkt for fil",
+ "LabelFileBirthtime": "Oprettelsestidspunkt for fil",
"LabelFileModified": "Fil ændret",
"LabelFilename": "Filnavn",
"LabelFilterByUser": "Filtrér efter bruger",
@@ -238,8 +247,10 @@
"LabelFinished": "Færdig",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
+ "LabelFontBoldness": "Skrift tykkelse",
"LabelFontFamily": "Fontfamilie",
"LabelFontScale": "Skriftstørrelse",
+ "LabelGenre": "Genre",
"LabelGenres": "Genrer",
"LabelHardDeleteFile": "Permanent slet fil",
"LabelHasEbook": "Har e-bog",
@@ -267,6 +278,7 @@
"LabelLastSeen": "Sidst set",
"LabelLastTime": "Sidste gang",
"LabelLastUpdate": "Seneste opdatering",
+ "LabelLayout": "Layout",
"LabelLayoutSinglePage": "Enkeltside",
"LabelLayoutSplitPage": "Opdelt side",
"LabelLess": "Mindre",
@@ -344,10 +356,11 @@
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRSSFeedURL": "RSS-feed-URL",
+ "LabelRandomly": "Tilfældigt",
"LabelRead": "Læst",
"LabelReadAgain": "Læs igen",
"LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt",
- "LabelRecentSeries": "Seneste serie",
+ "LabelRecentSeries": "Seneste serier",
"LabelRecentlyAdded": "Senest tilføjet",
"LabelRecommended": "Anbefalet",
"LabelReleaseDate": "Udgivelsesdato",
@@ -604,10 +617,8 @@
"PlaceholderNewPlaylist": "Nyt afspilningslistnavn",
"PlaceholderSearch": "Søg..",
"PlaceholderSearchEpisode": "Søg efter episode..",
- "ToastAccountUpdateFailed": "Mislykkedes opdatering af konto",
"ToastAccountUpdateSuccess": "Konto opdateret",
"ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet",
- "ToastAuthorUpdateFailed": "Mislykkedes opdatering af forfatter",
"ToastAuthorUpdateMerged": "Forfatter fusioneret",
"ToastAuthorUpdateSuccess": "Forfatter opdateret",
"ToastAuthorUpdateSuccessNoImageFound": "Forfatter opdateret (ingen billede fundet)",
@@ -623,17 +634,13 @@
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
"ToastBookmarkCreateSuccess": "Bogmærke tilføjet",
"ToastBookmarkRemoveSuccess": "Bogmærke fjernet",
- "ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke",
"ToastBookmarkUpdateSuccess": "Bogmærke opdateret",
"ToastChaptersHaveErrors": "Kapitler har fejl",
"ToastChaptersMustHaveTitles": "Kapitler skal have titler",
"ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen",
"ToastCollectionRemoveSuccess": "Samling fjernet",
- "ToastCollectionUpdateFailed": "Mislykkedes opdatering af samling",
"ToastCollectionUpdateSuccess": "Samling opdateret",
- "ToastItemCoverUpdateFailed": "Mislykkedes opdatering af varens omslag",
"ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
- "ToastItemDetailsUpdateFailed": "Mislykkedes opdatering af varedetaljer",
"ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
"ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
@@ -645,12 +652,10 @@
"ToastLibraryDeleteSuccess": "Bibliotek slettet",
"ToastLibraryScanFailedToStart": "Mislykkedes start af skanning",
"ToastLibraryScanStarted": "Biblioteksskanning startet",
- "ToastLibraryUpdateFailed": "Mislykkedes opdatering af bibliotek",
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret",
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
"ToastPlaylistCreateSuccess": "Afspilningsliste oprettet",
"ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet",
- "ToastPlaylistUpdateFailed": "Mislykkedes opdatering af afspilningsliste",
"ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret",
"ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast",
"ToastPodcastCreateSuccess": "Podcast oprettet med succes",
diff --git a/client/strings/de.json b/client/strings/de.json
index 9aec24f364..6dff93381b 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -56,6 +56,7 @@
"ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pausieren",
"ButtonPlay": "Abspielen",
+ "ButtonPlayAll": "Alles abspielen",
"ButtonPlaying": "Spielt",
"ButtonPlaylists": "Wiedergabelisten",
"ButtonPrevious": "Zurück",
@@ -65,12 +66,13 @@
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
+ "ButtonQuickEmbed": "Schnelles Hinzufügen",
"ButtonQuickEmbedMetadata": "Schnelles Hinzufügen von Metadaten",
"ButtonQuickMatch": "Schnellabgleich",
"ButtonReScan": "Neu scannen",
"ButtonRead": "Lesen",
- "ButtonReadLess": "Weniger anzeigen",
- "ButtonReadMore": "Mehr anzeigen",
+ "ButtonReadLess": "weniger Anzeigen",
+ "ButtonReadMore": "Mehr Anzeigen",
"ButtonRefresh": "Neu Laden",
"ButtonRemove": "Entfernen",
"ButtonRemoveAll": "Alles entfernen",
@@ -161,6 +163,7 @@
"HeaderNotificationUpdate": "Benachrichtigung bearbeiten",
"HeaderNotifications": "Benachrichtigungen",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
+ "HeaderOpenListeningSessions": "Aktive Hörbuch-Sitzungen",
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
"HeaderOtherFiles": "Sonstige Dateien",
"HeaderPasswordAuthentication": "Passwortauthentifizierung",
@@ -178,6 +181,7 @@
"HeaderRemoveEpisodes": "Entferne {0} Episoden",
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
"HeaderSchedule": "Zeitplan",
+ "HeaderScheduleEpisodeDownloads": "Automatische Episoden-Downloads planen",
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
"HeaderSession": "Sitzung",
"HeaderSetBackupSchedule": "Zeitplan für die Datensicherung festlegen",
@@ -216,14 +220,18 @@
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
"LabelAddedAt": "Hinzugefügt am",
- "LabelAddedDate": "Hinzugefügt {0}",
+ "LabelAddedDate": "{0} Hinzugefügt",
"LabelAdminUsersOnly": "Nur Admin Benutzer",
"LabelAll": "Alle",
"LabelAllUsers": "Alle Benutzer",
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
"LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste",
"LabelAlreadyInYourLibrary": "Bereits in der Bibliothek",
+ "LabelApiToken": "API Schlüssel",
"LabelAppend": "Anhängen",
+ "LabelAudioBitrate": "Audiobitrate (z. B. 128 kbit/s)",
+ "LabelAudioChannels": "Audiokanäle (1 oder 2)",
+ "LabelAudioCodec": "Audiocodec",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
@@ -236,6 +244,7 @@
"LabelAutoRegister": "Automatische Registrierung",
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren",
"LabelBackToUser": "Zurück zum Benutzer",
+ "LabelBackupAudioFiles": "Audio-Dateien sichern",
"LabelBackupLocation": "Backup-Ort",
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups werden in /metadata/backups gespeichert",
@@ -244,15 +253,18 @@
"LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen",
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.",
"LabelBitrate": "Bitrate",
+ "LabelBonus": "Bonus",
"LabelBooks": "Bücher",
"LabelButtonText": "Knopftext",
"LabelByAuthor": "von {0}",
"LabelChangePassword": "Passwort ändern",
"LabelChannels": "Kanäle",
+ "LabelChapterCount": "{0} Kapitel",
"LabelChapterTitle": "Kapitelüberschrift",
"LabelChapters": "Kapitel",
"LabelChaptersFound": "Gefundene Kapitel",
"LabelClickForMoreInfo": "Klicken für mehr Informationen",
+ "LabelClickToUseCurrentValue": "Anklicken um aktuellen Wert zu verwenden",
"LabelClosePlayer": "Player schließen",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Serien einklappen",
@@ -302,12 +314,25 @@
"LabelEmailSettingsTestAddress": "Test-Adresse",
"LabelEmbeddedCover": "Eingebettetes Cover",
"LabelEnable": "Aktivieren",
+ "LabelEncodingBackupLocation": "Eine Sicherungskopie der originalen Audiodateien wird gespeichert in:",
+ "LabelEncodingChaptersNotEmbedded": "Kapitel sind in mehrspurigen Hörbüchern nicht eingebettet.",
+ "LabelEncodingClearItemCache": "Stelle sicher, dass der Cache regelmäßig geleert wird.",
+ "LabelEncodingFinishedM4B": "Die fertige M4B-Datei wird im Hörbuch-Ordner unter folgendem Pfad abgelegt:",
+ "LabelEncodingInfoEmbedded": "Metadaten werden in die Audiodateien innerhalb des Audiobook Ordners eingebunden.",
+ "LabelEncodingStartedNavigation": "Sobald die Aufgabe gestartet ist, kann die Seite verlassen werden.",
+ "LabelEncodingTimeWarning": "Kodierung kann bis zu 30 Minuten dauern.",
+ "LabelEncodingWarningAdvancedSettings": "Achtung: Ändere diese Einstellungen nur, wenn du dich mit ffmpeg Kodierung auskennst.",
+ "LabelEncodingWatcherDisabled": "Wenn der Watcher deaktiviert ist musst du das Hörbuch danach erneut scannen.",
"LabelEnd": "Ende",
"LabelEndOfChapter": "Ende des Kapitels",
"LabelEpisode": "Episode",
+ "LabelEpisodeNotLinkedToRssFeed": "Episode nicht mit RSS-Feed verknüpft",
+ "LabelEpisodeNumber": "Episode #{0}",
"LabelEpisodeTitle": "Episodentitel",
"LabelEpisodeType": "Episodentyp",
+ "LabelEpisodeUrlFromRssFeed": "Episoden URL vom RSS-Feed",
"LabelEpisodes": "Episoden",
+ "LabelEpisodic": "Episodisch",
"LabelExample": "Beispiel",
"LabelExpandSeries": "Serie ausklappen",
"LabelExpandSubSeries": "Unterserie ausklappen",
@@ -335,6 +360,7 @@
"LabelFontScale": "Schriftgröße",
"LabelFontStrikethrough": "Durchgestrichen",
"LabelFormat": "Format",
+ "LabelFull": "Voll",
"LabelGenre": "Kategorie",
"LabelGenres": "Kategorien",
"LabelHardDeleteFile": "Datei dauerhaft löschen",
@@ -390,6 +416,10 @@
"LabelLowestPriority": "Niedrigste Priorität",
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet",
+ "LabelMaxEpisodesToDownload": "Max. Anzahl an Episoden zum Herunterladen, 0 für unbegrenzte Episoden.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Max. Anzahl neuer Episoden zum Herunterladen pro Abfrage",
+ "LabelMaxEpisodesToKeep": "Max. Anzahl zu behaltender Episoden",
+ "LabelMaxEpisodesToKeepHelp": "0 setzt keine Begrenzung. Wenn eine neue Episode automatisch heruntergeladen wird, wird die älteste Episode gelöscht, wenn du mehr als X Episoden gespeichert hast. Es wird nur eine Episode pro neuem Download gelöscht.",
"LabelMediaPlayer": "Mediaplayer",
"LabelMediaType": "Medientyp",
"LabelMetaTag": "Meta Schlagwort",
@@ -435,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als
groups
bezeichnet.
Wenn konfiguriert , wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.",
"LabelOpenRSSFeed": "Öffne RSS-Feed",
"LabelOverwrite": "Überschreiben",
+ "LabelPaginationPageXOfY": "Seite {0} von {1}",
"LabelPassword": "Passwort",
"LabelPath": "Pfad",
"LabelPermanent": "Dauerhaft",
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
"LabelPermissionsAccessAllTags": "Zugriff auf alle Schlagwörter",
"LabelPermissionsAccessExplicitContent": "Zugriff auf explizite (alterbeschränkte) Inhalte",
+ "LabelPermissionsCreateEreader": "Kann E-Reader erstellen",
"LabelPermissionsDelete": "Darf Löschen",
"LabelPermissionsDownload": "Herunterladen",
"LabelPermissionsUpdate": "Aktualisieren",
@@ -464,6 +496,8 @@
"LabelPubDate": "Veröffentlichungsdatum",
"LabelPublishYear": "Jahr",
"LabelPublishedDate": "Veröffentlicht {0}",
+ "LabelPublishedDecade": "Jahrzehnt",
+ "LabelPublishedDecades": "Jahrzehnte",
"LabelPublisher": "Herausgeber",
"LabelPublishers": "Herausgeber",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
@@ -483,21 +517,28 @@
"LabelRedo": "Wiederholen",
"LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum",
+ "LabelRemoveAllMetadataAbs": "Alle metadata.abs Dateien löschen",
+ "LabelRemoveAllMetadataJson": "Alle metadata.json Dateien löschen",
"LabelRemoveCover": "Entferne Titelbild",
+ "LabelRemoveMetadataFile": "Metadaten-Dateien in Bibliotheksordnern löschen",
+ "LabelRemoveMetadataFileHelp": "Alle metadata.json und metadata.abs Dateien aus den Ordnern {0} löschen.",
"LabelRowsPerPage": "Zeilen pro Seite",
"LabelSearchTerm": "Begriff suchen",
"LabelSearchTitle": "Titel suchen",
"LabelSearchTitleOrASIN": "Titel oder ASIN suchen",
"LabelSeason": "Staffel",
+ "LabelSeasonNumber": "Staffel #{0}",
"LabelSelectAll": "Alles auswählen",
"LabelSelectAllEpisodes": "Alle Episoden auswählen",
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
"LabelSelectUsers": "Benutzer auswählen",
"LabelSendEbookToDevice": "E-Buch senden an …",
"LabelSequence": "Reihenfolge",
+ "LabelSerial": "fortlaufend",
"LabelSeries": "Serien",
"LabelSeriesName": "Serienname",
"LabelSeriesProgress": "Serienfortschritt",
+ "LabelServerLogLevel": "Server Log Level",
"LabelServerYearReview": "Server Jahr in Übersicht ({0})",
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
@@ -522,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.",
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "In Prozent gehört größer als",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Verbleibende Zeit ist weniger als (Sekunden)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Markiere Mediendateien als fertig, wenn",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Überspringe vorherige Bücher in fortführender Serie",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Die Startseite von \"Fortführende Serien\" zeigt das erste noch nicht begonnene Buch in Serien an, die mindestens ein Buch abgeschlossen und keine Bücher begonnen haben. Wenn diese Einstellung aktiviert wird, werden Serien ab dem letzten abgeschlossenen Buch fortgesetzt und nicht ab dem ersten nicht begonnenen Buch.",
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
@@ -566,7 +610,7 @@
"LabelStatsMinutesListening": "Gehörte Minuten",
"LabelStatsOverallDays": "Gesamte Tage",
"LabelStatsOverallHours": "Gesamte Stunden",
- "LabelStatsWeekListening": "Gehörte Wochen",
+ "LabelStatsWeekListening": "Wochenhördauer",
"LabelSubtitle": "Untertitel",
"LabelSupportedFileTypes": "Unterstützte Dateitypen",
"LabelTag": "Schlagwort",
@@ -586,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} Minuten",
"LabelTimeDurationXSeconds": "{0} Sekunden",
"LabelTimeInMinutes": "Zeit in Minuten",
+ "LabelTimeLeft": "{0} verbleibend",
"LabelTimeListened": "Gehörte Zeit",
"LabelTimeListenedToday": "Heute gehörte Zeit",
"LabelTimeRemaining": "{0} verbleibend",
@@ -593,6 +638,7 @@
"LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Metadaten einbetten",
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
+ "LabelToolsM4bEncoder": "M4B Kodierer",
"LabelToolsMakeM4b": "M4B-Datei erstellen",
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
"LabelToolsSplitM4b": "M4B in MP3s aufteilen",
@@ -605,6 +651,7 @@
"LabelTracksMultiTrack": "Mehrfachdatei",
"LabelTracksNone": "Keine Dateien",
"LabelTracksSingleTrack": "Einzeldatei",
+ "LabelTrailer": "Vorschau",
"LabelType": "Typ",
"LabelUnabridged": "Ungekürzt",
"LabelUndo": "Rückgängig machen",
@@ -618,8 +665,10 @@
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
"LabelUploaderDropFiles": "Dateien löschen",
"LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie",
+ "LabelUseAdvancedOptions": "Nutze Erweiterte Optionen",
"LabelUseChapterTrack": "Kapiteldatei verwenden",
"LabelUseFullTrack": "Gesamte Datei verwenden",
+ "LabelUseZeroForUnlimited": "0 für unbegrenzt",
"LabelUser": "Benutzer",
"LabelUsername": "Benutzername",
"LabelValue": "Wert",
@@ -632,8 +681,8 @@
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
"LabelXBooks": "{0} Bücher",
"LabelXItems": "{0} Medien",
- "LabelYearReviewHide": "Verstecke Jahr in Übersicht",
- "LabelYearReviewShow": "Zeige Jahr in Übersicht",
+ "LabelYearReviewHide": "Jahresrückblick verbergen",
+ "LabelYearReviewShow": "Jahresrückblick anzeigen",
"LabelYourAudiobookDuration": "Laufzeit deines Mediums",
"LabelYourBookmarks": "Lesezeichen",
"LabelYourPlaylists": "Eigene Wiedergabelisten",
@@ -666,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "Möchtest du den benutzerdefinierten Metadatenanbieter \"{0}\" wirklich löschen?",
"MessageConfirmDeleteNotification": "Möchtest du diese Benachrichtigung wirklich löschen?",
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "Bist du dir sicher, dass die Metadaten in {0} Audiodateien eingebettet werden sollen?",
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?",
@@ -677,6 +727,7 @@
"MessageConfirmPurgeCache": "Cache leeren wird das ganze Verzeichnis
/metadata/cache
löschen.
Bist du dir sicher, dass das Cache Verzeichnis gelöscht werden soll?",
"MessageConfirmPurgeItemsCache": "Durch Elementcache leeren wird das gesamte Verzeichnis unter
/metadata/cache/items
gelöscht.
Bist du dir sicher?",
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt.
Möchtest du fortfahren?",
+ "MessageConfirmQuickMatchEpisodes": "Schnelles Zuordnen von Episoden überschreibt die Details, wenn eine Übereinstimmung gefunden wird. Nur nicht zugeordnete Episoden werden aktualisiert. Bist du sicher?",
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?",
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
@@ -684,6 +735,7 @@
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
+ "MessageConfirmRemoveMetadataFiles": "Bist du sicher, dass du alle metadata.{0} Dateien in deinen Bibliotheksordnern löschen willst?",
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
@@ -699,6 +751,7 @@
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
"MessageEmbedFailed": "Einbetten fehlgeschlagen!",
"MessageEmbedFinished": "Einbettung abgeschlossen!",
+ "MessageEmbedQueue": "Eingereiht zum einbinden von Metadaten ({0} in Warteschlange)",
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
"MessageEreaderDevices": "Um die Zustellung von E-Büchern sicherzustellen, musst du eventuell die oben genannte E-Mail-Adresse als gültigen Absender für jedes unten aufgeführte Gerät hinzufügen.",
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
@@ -743,6 +796,7 @@
"MessageNoLogs": "Keine Protokolle",
"MessageNoMediaProgress": "Kein Medienfortschritt",
"MessageNoNotifications": "Keine Benachrichtigungen",
+ "MessageNoPodcastFeed": "Ungültiger Podcast: Kein Feed",
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
"MessageNoResults": "Keine Ergebnisse",
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
@@ -759,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
"MessagePleaseWait": "Bitte warten...",
"MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
+ "MessagePodcastSearchField": "Suchbegriff oder RSS-Feed URL eingeben",
+ "MessageQuickEmbedInProgress": "Schnellabgleich läuft",
+ "MessageQuickEmbedQueue": "In Warteschlange für Schnelles einbinden ({0} eingereiht)",
+ "MessageQuickMatchAllEpisodes": "Quick Match aller Episoden",
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
"MessageRemoveChapter": "Kapitel entfernen",
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
@@ -776,6 +834,41 @@
"MessageShareExpiresIn": "Läuft in {0} ab",
"MessageShareURLWillBe": "Der Freigabe Link wird
{0} sein.",
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
+ "MessageTaskAudioFileNotWritable": "Die Audiodatei \"{0}\" ist schreibgeschützt",
+ "MessageTaskCanceledByUser": "Aufgabe vom Benutzer abgebrochen",
+ "MessageTaskDownloadingEpisodeDescription": "Folge \"{0}\" wird heruntergeladen",
+ "MessageTaskEmbeddingMetadata": "Metadaten werden eingebettet",
+ "MessageTaskEmbeddingMetadataDescription": "Metadaten werden in Hörbuch \"{0}\" eingebettet",
+ "MessageTaskEncodingM4b": "M4B wird encodiert",
+ "MessageTaskEncodingM4bDescription": "Hörbuch \"{0}\" wird in eine einzelne m4b Datei encodiert",
+ "MessageTaskFailed": "Fehlgeschlagen",
+ "MessageTaskFailedToBackupAudioFile": "Sicherung der Audiodatei \"{0}\" fehlgeschlagen",
+ "MessageTaskFailedToCreateCacheDirectory": "Fehler beim erstellen des Cache-Verzeichnisses",
+ "MessageTaskFailedToEmbedMetadataInFile": "Einbetten der Metadaten in die Datei \"{0}\" fehlgeschlagen",
+ "MessageTaskFailedToMergeAudioFiles": "Fehler beim zusammenführen der Audiodateien",
+ "MessageTaskFailedToMoveM4bFile": "Fehler beim verschieben der m4b Datei",
+ "MessageTaskFailedToWriteMetadataFile": "Fehler beim schreiben der Metadaten-Datei",
+ "MessageTaskMatchingBooksInLibrary": "Vergleiche Bücher in Bibliothek \"{0}\"",
+ "MessageTaskNoFilesToScan": "Keine Dateien zum scannen",
+ "MessageTaskOpmlImport": "OPML-Import",
+ "MessageTaskOpmlImportDescription": "Podcasts von {0} RSS-Feeds werden ersrtellt",
+ "MessageTaskOpmlImportFeed": "OPML-Feed importieren",
+ "MessageTaskOpmlImportFeedDescription": "RSS-Feed \"{0}\" wird importiert",
+ "MessageTaskOpmlImportFeedFailed": "Podcast Feed konnte nicht geladen werden",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Podcast \"{0}\" wird erstellt",
+ "MessageTaskOpmlImportFeedPodcastExists": "Der Podcast ist bereits im Pfad vorhanden",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Erstellen des Podcasts fehlgeschlagen",
+ "MessageTaskOpmlImportFinished": "{0} Podcasts hinzugefügt",
+ "MessageTaskOpmlParseFailed": "Fehler beim lesen der OPML Datei",
+ "MessageTaskOpmlParseFastFail": "Ungültie OPML Datei:
ODER tag wurde nicht gefunden",
+ "MessageTaskOpmlParseNoneFound": "Keine feeds in der OPML Datei gefunden",
+ "MessageTaskScanItemsAdded": "{0} hinzugefügt",
+ "MessageTaskScanItemsMissing": "{0} fehlend",
+ "MessageTaskScanItemsUpdated": "{0} aktualisiert",
+ "MessageTaskScanNoChangesNeeded": "Keine Änderungen nötig",
+ "MessageTaskScanningFileChanges": "Überprüfe \"{0}\" nach geänderten Dateien",
+ "MessageTaskScanningLibrary": "Bibliothek \"{0}\" wird durchsucht",
+ "MessageTaskTargetDirectoryNotWritable": "Das Zielverzeichnis ist schreibgeschützt",
"MessageThinking": "Nachdenken...",
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
@@ -793,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
"NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.",
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
+ "NotificationOnBackupCompletedDescription": "Wird ausgeführt wenn ein Backup erstellt wurde",
+ "NotificationOnBackupFailedDescription": "Wird ausgeführt wenn ein Backup fehlgeschlagen ist",
+ "NotificationOnEpisodeDownloadedDescription": "Wird ausgeführt wenn eine Podcast Folge automatisch heruntergeladen wird",
+ "NotificationOnTestDescription": "Wird ausgeführt wenn das Benachrichtigungssystem getestet wird",
"PlaceholderNewCollection": "Neuer Sammlungsname",
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
@@ -816,14 +913,13 @@
"StatsTopNarrators": "TOP SPRECHER",
"StatsTotalDuration": "Mit einer totalen Dauer von…",
"StatsYearInReview": "DAS JAHR IM RÜCKBLICK",
- "ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
"ToastAccountUpdateSuccess": "Konto aktualisiert",
"ToastAppriseUrlRequired": "Eine Apprise-URL ist notwendig",
+ "ToastAsinRequired": "ASIN ist erforderlich",
"ToastAuthorImageRemoveSuccess": "Autorenbild entfernt",
"ToastAuthorNotFound": "Autor \"{0}\" nicht gefunden",
"ToastAuthorRemoveSuccess": "Autor entfernt",
"ToastAuthorSearchNotFound": "Autor nicht gefunden",
- "ToastAuthorUpdateFailed": "Aktualisierung des Autors fehlgeschlagen",
"ToastAuthorUpdateMerged": "Autor zusammengeführt",
"ToastAuthorUpdateSuccess": "Autor aktualisiert",
"ToastAuthorUpdateSuccessNoImageFound": "Autor aktualisiert (kein Bild gefunden)",
@@ -834,29 +930,29 @@
"ToastBackupDeleteSuccess": "Sicherung gelöscht",
"ToastBackupInvalidMaxKeep": "Ungültige Anzahl aufzubewahrender Backups",
"ToastBackupInvalidMaxSize": "Ungültige maximale Backupgröße",
- "ToastBackupPathUpdateFailed": "Der Backuppfad konnte nicht aktualisiert werden",
"ToastBackupRestoreFailed": "Sicherung konnte nicht wiederhergestellt werden",
"ToastBackupUploadFailed": "Sicherung konnte nicht hochgeladen werden",
"ToastBackupUploadSuccess": "Sicherung hochgeladen",
"ToastBatchDeleteFailed": "Batch-Löschen fehlgeschlagen",
"ToastBatchDeleteSuccess": "Batch-Löschung erfolgreich",
+ "ToastBatchQuickMatchFailed": "Batch-Schnellabgleich fehlgeschlagen!",
+ "ToastBatchQuickMatchStarted": "Batch-Schnellabgleich für {0} Bücher gestartet!",
"ToastBatchUpdateFailed": "Stapelaktualisierung fehlgeschlagen",
"ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich",
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
"ToastBookmarkRemoveSuccess": "Lesezeichen entfernt",
- "ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
"ToastCachePurgeFailed": "Cache leeren fehlgeschlagen",
"ToastCachePurgeSuccess": "Cache geleert",
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
"ToastChaptersRemoved": "Kapitel entfernt",
+ "ToastChaptersUpdated": "Kapitel aktualisiert",
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
"ToastCollectionItemsAddSuccess": "Element(e) erfolgreich zur Sammlung hinzugefügt",
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
- "ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
@@ -865,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "E-Reader-Gerät mit diesem Namen existiert bereits",
"ToastDeviceTestEmailFailed": "Senden der Test-E-Mail fehlgeschlagen",
"ToastDeviceTestEmailSuccess": "Test-E-Mail gesendet",
- "ToastDeviceUpdateFailed": "Das Gerät konnte nicht aktualisiert werden",
- "ToastEmailSettingsUpdateFailed": "E-Mail-Einstellungen konnten nicht aktualisiert werden",
"ToastEmailSettingsUpdateSuccess": "E-Mail-Einstellungen aktualisiert",
"ToastEncodeCancelFailed": "Das Encoding konnte nicht abgebrochen werden",
"ToastEncodeCancelSucces": "Encoding abgebrochen",
"ToastEpisodeDownloadQueueClearFailed": "Warteschlange konnte nicht gelöscht werden",
"ToastEpisodeDownloadQueueClearSuccess": "Warteschlange für Episoden-Downloads gelöscht",
+ "ToastEpisodeUpdateSuccess": "{0} Episoden aktualisiert",
"ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden",
"ToastFailedToLoadData": "Daten laden fehlgeschlagen",
+ "ToastFailedToMatch": "Fehler beim Abgleich",
"ToastFailedToShare": "Fehler beim Teilen",
- "ToastFailedToUpdateAccount": "Fehler beim ändern des Accounts",
- "ToastFailedToUpdateUser": "Fehler beim ändern des Benutzers",
+ "ToastFailedToUpdate": "Aktualisierung ist fehlgeschlagen",
"ToastInvalidImageUrl": "Ungültiger Bild URL",
+ "ToastInvalidMaxEpisodesToDownload": "Ungültige Max. Anzahl an Episoden zum Herunterladen",
"ToastInvalidUrl": "Ungültiger URL",
- "ToastItemCoverUpdateFailed": "Fehler bei der Aktualisierung des Titelbildes",
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
"ToastItemDeletedFailed": "Fehler beim löschen des Artikels",
"ToastItemDeletedSuccess": "Artikel gelöscht",
- "ToastItemDetailsUpdateFailed": "Fehler bei der Aktualisierung der Artikeldetails",
"ToastItemDetailsUpdateSuccess": "Artikeldetails aktualisiert",
"ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Artikels als \"Beendet\"",
"ToastItemMarkedAsFinishedSuccess": "Artikel als \"Beendet\" markiert",
"ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Artikels als \"Nicht Beendet\"",
"ToastItemMarkedAsNotFinishedSuccess": "Artikel als \"Nicht Beendet\" markiert",
- "ToastItemUpdateFailed": "Fehler beim ändern des Artikels",
"ToastItemUpdateSuccess": "Artikel wurde verändert",
"ToastLibraryCreateFailed": "Bibliothek konnte nicht erstellt werden",
"ToastLibraryCreateSuccess": "Bibliothek \"{0}\" erstellt",
@@ -897,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Bibliothek gelöscht",
"ToastLibraryScanFailedToStart": "Scan konnte nicht gestartet werden",
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
- "ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
+ "ToastMatchAllAuthorsFailed": "Nicht alle Autoren konnten zugeordnet werden",
+ "ToastMetadataFilesRemovedError": "Fehler beim löschen von metadata.{0} Dateien",
+ "ToastMetadataFilesRemovedNoneFound": "Keine metadata.{0} Dateien in Bibliothek gefunden",
+ "ToastMetadataFilesRemovedNoneRemoved": "Keine metadata.{0} Dateien gelöscht",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} Datei(en) gelöscht",
+ "ToastMustHaveAtLeastOnePath": "Es muss mindestens ein Pfad angegeben werden",
"ToastNameEmailRequired": "Name und E-Mail sind erforderlich",
"ToastNameRequired": "Name ist erforderlich",
+ "ToastNewEpisodesFound": "{0} neue Episoden gefunden",
"ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"",
"ToastNewUserCreatedSuccess": "Neuer Account erstellt",
"ToastNewUserLibraryError": "Mindestens eine Bibliothek muss ausgewählt werden",
"ToastNewUserPasswordError": "Passwort erforderlich, nur der root Benutzer darf ein leeres Passwort haben",
"ToastNewUserTagError": "Mindestens ein Tag muss ausgewählt sein",
"ToastNewUserUsernameError": "Nutzername eingeben",
+ "ToastNoNewEpisodesFound": "Keine neuen Episoden gefunden",
"ToastNoUpdatesNecessary": "Keine Änderungen nötig",
"ToastNotificationCreateFailed": "Fehler beim erstellen der Benachrichtig",
"ToastNotificationDeleteFailed": "Fehler beim löschen der Benachrichtigung",
"ToastNotificationFailedMaximum": "Maximale Fehlversuche muss >= 0 sein",
"ToastNotificationQueueMaximum": "Maximale Benachrichtigungswarteschlange muss >= 0 sein",
- "ToastNotificationSettingsUpdateFailed": "Fehler beim ändern der Benachrichtigungseinstellungen",
"ToastNotificationSettingsUpdateSuccess": "Benachrichtigungseinstellungen geändert",
"ToastNotificationTestTriggerFailed": "Fehler beim Auslösen der Testbenachrichtigung",
"ToastNotificationTestTriggerSuccess": "Testbenachrichtigung ausgelöst",
- "ToastNotificationUpdateFailed": "Fehler bein ändern der Benachrichtigung",
"ToastNotificationUpdateSuccess": "Benachrichtigung geändert",
"ToastPlaylistCreateFailed": "Erstellen der Wiedergabeliste fehlgeschlagen",
"ToastPlaylistCreateSuccess": "Wiedergabeliste erstellt",
"ToastPlaylistRemoveSuccess": "Wiedergabeliste gelöscht",
- "ToastPlaylistUpdateFailed": "Aktualisieren der Wiedergabeliste fehlgeschlagen",
"ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert",
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
"ToastPodcastCreateSuccess": "Podcast erstellt",
"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",
"ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt",
"ToastProviderNameAndUrlRequired": "Name und URL notwendig",
@@ -950,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "E-Buch an Gerät „{0}“ gesendet",
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
- "ToastServerSettingsUpdateFailed": "Die Server-Einstellungen wurden nicht gespeichert",
"ToastServerSettingsUpdateSuccess": "Die Server-Einstellungen wurden geupdated",
"ToastSessionCloseFailed": "Fehler beim schließen der Sitzung",
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
+ "ToastSleepTimerDone": "Einschlaf-Timer aktiviert... zZzzZz",
"ToastSlugMustChange": "URL-Schlüssel enthält ungültige Zeichen",
"ToastSlugRequired": "URL-Schlüssel erforderlich",
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
"ToastSortingPrefixesEmptyError": "Es muss mindestens ein Sortier-Prefix vorhanden sein",
- "ToastSortingPrefixesUpdateFailed": "Update der Sortier-Prefixe ist fehlgeschlagen",
"ToastSortingPrefixesUpdateSuccess": "Die Sortier-Prefixe wirden geupdated ({0} Einträge)",
"ToastTitleRequired": "Titel erforderlich",
"ToastUnknownError": "Unbekannter Fehler",
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index d00e4bbc7c..c7661a0253 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -66,6 +66,7 @@
"ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonQueueAddItem": "Add to queue",
"ButtonQueueRemoveItem": "Remove from queue",
+ "ButtonQuickEmbed": "Quick Embed",
"ButtonQuickEmbedMetadata": "Quick Embed Metadata",
"ButtonQuickMatch": "Quick Match",
"ButtonReScan": "Re-Scan",
@@ -163,6 +164,7 @@
"HeaderNotificationUpdate": "Update Notification",
"HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
+ "HeaderOpenListeningSessions": "Open Listening Sessions",
"HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderOtherFiles": "Other Files",
"HeaderPasswordAuthentication": "Password Authentication",
@@ -180,6 +182,7 @@
"HeaderRemoveEpisodes": "Remove {0} Episodes",
"HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule",
+ "HeaderScheduleEpisodeDownloads": "Schedule Automatic Episode Downloads",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
"HeaderSession": "Session",
"HeaderSetBackupSchedule": "Set Backup Schedule",
@@ -225,7 +228,11 @@
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library",
+ "LabelApiToken": "API Token",
"LabelAppend": "Append",
+ "LabelAudioBitrate": "Audio Bitrate (e.g. 128k)",
+ "LabelAudioChannels": "Audio Channels (1 or 2)",
+ "LabelAudioCodec": "Audio Codec",
"LabelAuthor": "Author",
"LabelAuthorFirstLast": "Author (First Last)",
"LabelAuthorLastFirst": "Author (Last, First)",
@@ -238,6 +245,7 @@
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Back to User",
+ "LabelBackupAudioFiles": "Backup Audio Files",
"LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
@@ -246,15 +254,18 @@
"LabelBackupsNumberToKeep": "Number of backups to keep",
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBitrate": "Bitrate",
+ "LabelBonus": "Bonus",
"LabelBooks": "Books",
"LabelButtonText": "Button Text",
"LabelByAuthor": "by {0}",
"LabelChangePassword": "Change Password",
"LabelChannels": "Channels",
+ "LabelChapterCount": "{0} Chapters",
"LabelChapterTitle": "Chapter Title",
"LabelChapters": "Chapters",
"LabelChaptersFound": "chapters found",
"LabelClickForMoreInfo": "Click for more info",
+ "LabelClickToUseCurrentValue": "Click to use current value",
"LabelClosePlayer": "Close player",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Collapse Series",
@@ -304,12 +315,25 @@
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable",
+ "LabelEncodingBackupLocation": "A backup of your original audio files will be stored in:",
+ "LabelEncodingChaptersNotEmbedded": "Chapters are not embedded in multi-track audiobooks.",
+ "LabelEncodingClearItemCache": "Make sure to periodically purge items cache.",
+ "LabelEncodingFinishedM4B": "Finished M4B will be put into your audiobook folder at:",
+ "LabelEncodingInfoEmbedded": "Metadata will be embedded in the audio tracks inside your audiobook folder.",
+ "LabelEncodingStartedNavigation": "Once the task is started you can navigate away from this page.",
+ "LabelEncodingTimeWarning": "Encoding can take up to 30 minutes.",
+ "LabelEncodingWarningAdvancedSettings": "Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.",
+ "LabelEncodingWatcherDisabled": "If you have the watcher disabled you will need to re-scan this audiobook afterwards.",
"LabelEnd": "End",
"LabelEndOfChapter": "End of Chapter",
"LabelEpisode": "Episode",
+ "LabelEpisodeNotLinkedToRssFeed": "Episode not linked to RSS feed",
+ "LabelEpisodeNumber": "Episode #{0}",
"LabelEpisodeTitle": "Episode Title",
"LabelEpisodeType": "Episode Type",
+ "LabelEpisodeUrlFromRssFeed": "Episode URL from RSS feed",
"LabelEpisodes": "Episodes",
+ "LabelEpisodic": "Episodic",
"LabelExample": "Example",
"LabelExpandSeries": "Expand Series",
"LabelExpandSubSeries": "Expand Sub Series",
@@ -338,6 +362,7 @@
"LabelFontScale": "Font scale",
"LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format",
+ "LabelFull": "Full",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard delete file",
@@ -393,6 +418,10 @@
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
+ "LabelMaxEpisodesToDownload": "Max # of episodes to download. Use 0 for unlimited.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Max # of new episodes to download per check",
+ "LabelMaxEpisodesToKeep": "Max # of episodes to keep",
+ "LabelMaxEpisodesToKeepHelp": "Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. This will only delete 1 episode per new download.",
"LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type",
"LabelMetaTag": "Meta Tag",
@@ -437,12 +466,14 @@
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as groups
. If configured , the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Open RSS Feed",
"LabelOverwrite": "Overwrite",
+ "LabelPaginationPageXOfY": "Page {0} of {1}",
"LabelPassword": "Password",
"LabelPath": "Path",
"LabelPermanent": "Permanent",
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
"LabelPermissionsAccessAllTags": "Can Access All Tags",
"LabelPermissionsAccessExplicitContent": "Can Access Explicit Content",
+ "LabelPermissionsCreateEreader": "Can Create Ereader",
"LabelPermissionsDelete": "Can Delete",
"LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update",
@@ -466,6 +497,8 @@
"LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year",
"LabelPublishedDate": "Published {0}",
+ "LabelPublishedDecade": "Published Decade",
+ "LabelPublishedDecades": "Published Decades",
"LabelPublisher": "Publisher",
"LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
@@ -485,21 +518,28 @@
"LabelRedo": "Redo",
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
+ "LabelRemoveAllMetadataAbs": "Remove all metadata.abs files",
+ "LabelRemoveAllMetadataJson": "Remove all metadata.json files",
"LabelRemoveCover": "Remove cover",
+ "LabelRemoveMetadataFile": "Remove metadata files in library item folders",
+ "LabelRemoveMetadataFileHelp": "Remove all metadata.json and metadata.abs files in your {0} folders.",
"LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Search Term",
"LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season",
+ "LabelSeasonNumber": "Season #{0}",
"LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequence",
+ "LabelSerial": "Serial",
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress",
+ "LabelServerLogLevel": "Server Log Level",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary",
@@ -524,6 +564,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Percent complete is greater than",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Time remaining is less than (seconds)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Mark media item as finished when",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles",
@@ -588,6 +631,7 @@
"LabelTimeDurationXMinutes": "{0} minutes",
"LabelTimeDurationXSeconds": "{0} seconds",
"LabelTimeInMinutes": "Time in minutes",
+ "LabelTimeLeft": "{0} left",
"LabelTimeListened": "Time Listened",
"LabelTimeListenedToday": "Time Listened Today",
"LabelTimeRemaining": "{0} remaining",
@@ -595,6 +639,7 @@
"LabelTitle": "Title",
"LabelToolsEmbedMetadata": "Embed Metadata",
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
+ "LabelToolsM4bEncoder": "M4B Encoder",
"LabelToolsMakeM4b": "Make M4B Audiobook File",
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
"LabelToolsSplitM4b": "Split M4B to MP3's",
@@ -607,6 +652,7 @@
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
+ "LabelTrailer": "Trailer",
"LabelType": "Type",
"LabelUnabridged": "Unabridged",
"LabelUndo": "Undo",
@@ -618,10 +664,13 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUpdatedAt": "Updated At",
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
+ "LabelUploaderDragAndDropFilesOnly": "Drag & drop files",
"LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
+ "LabelUseAdvancedOptions": "Use Advanced Options",
"LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track",
+ "LabelUseZeroForUnlimited": "Use 0 for unlimited",
"LabelUser": "User",
"LabelUsername": "Username",
"LabelValue": "Value",
@@ -668,6 +717,7 @@
"MessageConfirmDeleteMetadataProvider": "Are you sure you want to delete custom metadata provider \"{0}\"?",
"MessageConfirmDeleteNotification": "Are you sure you want to delete this notification?",
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
@@ -679,6 +729,7 @@
"MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache
. Are you sure you want to remove the cache directory?",
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items
. Are you sure?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. Would you like to continue?",
+ "MessageConfirmQuickMatchEpisodes": "Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
@@ -686,6 +737,7 @@
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
+ "MessageConfirmRemoveMetadataFiles": "Are you sure you want to remove all metadata.{0} files in your library item folders?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -701,6 +753,7 @@
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFailed": "Embed Failed!",
"MessageEmbedFinished": "Embed Finished!",
+ "MessageEmbedQueue": "Queued for metadata embed ({0} in queue)",
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
"MessageFeedURLWillBe": "Feed URL will be {0}",
@@ -745,6 +798,7 @@
"MessageNoLogs": "No Logs",
"MessageNoMediaProgress": "No Media Progress",
"MessageNoNotifications": "No Notifications",
+ "MessageNoPodcastFeed": "Invalid podcast: No Feed",
"MessageNoPodcastsFound": "No podcasts found",
"MessageNoResults": "No Results",
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
@@ -761,6 +815,10 @@
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
"MessagePleaseWait": "Please wait...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
+ "MessagePodcastSearchField": "Enter search term or RSS feed URL",
+ "MessageQuickEmbedInProgress": "Quick embed in progress",
+ "MessageQuickEmbedQueue": "Queued for quick embed ({0} in queue)",
+ "MessageQuickMatchAllEpisodes": "Quick Match All Episodes",
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
"MessageRemoveChapter": "Remove chapter",
"MessageRemoveEpisodes": "Remove {0} episode(s)",
@@ -803,6 +861,9 @@
"MessageTaskOpmlImportFeedPodcastExists": "Podcast already exists at path",
"MessageTaskOpmlImportFeedPodcastFailed": "Failed to create podcast",
"MessageTaskOpmlImportFinished": "Added {0} podcasts",
+ "MessageTaskOpmlParseFailed": "Failed to parse OPML file",
+ "MessageTaskOpmlParseFastFail": "Invalid OPML file tag not found OR an tag was not found",
+ "MessageTaskOpmlParseNoneFound": "No feeds found in OPML file",
"MessageTaskScanItemsAdded": "{0} added",
"MessageTaskScanItemsMissing": "{0} missing",
"MessageTaskScanItemsUpdated": "{0} updated",
@@ -827,6 +888,10 @@
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",
"NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.",
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
+ "NotificationOnBackupCompletedDescription": "Triggered when a backup is completed",
+ "NotificationOnBackupFailedDescription": "Triggered when a backup fails",
+ "NotificationOnEpisodeDownloadedDescription": "Triggered when a podcast episode is auto-downloaded",
+ "NotificationOnTestDescription": "Event for testing the notification system",
"PlaceholderNewCollection": "New collection name",
"PlaceholderNewFolderPath": "New folder path",
"PlaceholderNewPlaylist": "New playlist name",
@@ -850,14 +915,13 @@
"StatsTopNarrators": "TOP NARRATORS",
"StatsTotalDuration": "With a total duration of…",
"StatsYearInReview": "YEAR IN REVIEW",
- "ToastAccountUpdateFailed": "Failed to update account",
"ToastAccountUpdateSuccess": "Account updated",
"ToastAppriseUrlRequired": "Must enter an Apprise URL",
+ "ToastAsinRequired": "ASIN is required",
"ToastAuthorImageRemoveSuccess": "Author image removed",
"ToastAuthorNotFound": "Author \"{0}\" not found",
"ToastAuthorRemoveSuccess": "Author removed",
"ToastAuthorSearchNotFound": "Author not found",
- "ToastAuthorUpdateFailed": "Failed to update author",
"ToastAuthorUpdateMerged": "Author merged",
"ToastAuthorUpdateSuccess": "Author updated",
"ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
@@ -868,29 +932,29 @@
"ToastBackupDeleteSuccess": "Backup deleted",
"ToastBackupInvalidMaxKeep": "Invalid number of backups to keep",
"ToastBackupInvalidMaxSize": "Invalid maximum backup size",
- "ToastBackupPathUpdateFailed": "Failed to update backup path",
"ToastBackupRestoreFailed": "Failed to restore backup",
"ToastBackupUploadFailed": "Failed to upload backup",
"ToastBackupUploadSuccess": "Backup uploaded",
"ToastBatchDeleteFailed": "Batch delete failed",
"ToastBatchDeleteSuccess": "Batch delete success",
+ "ToastBatchQuickMatchFailed": "Batch Quick Match failed!",
+ "ToastBatchQuickMatchStarted": "Batch Quick Match of {0} books started!",
"ToastBatchUpdateFailed": "Batch update failed",
"ToastBatchUpdateSuccess": "Batch update success",
"ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkCreateSuccess": "Bookmark added",
"ToastBookmarkRemoveSuccess": "Bookmark removed",
- "ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastBookmarkUpdateSuccess": "Bookmark updated",
"ToastCachePurgeFailed": "Failed to purge cache",
"ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Chapters have errors",
"ToastChaptersMustHaveTitles": "Chapters must have titles",
"ToastChaptersRemoved": "Chapters removed",
+ "ToastChaptersUpdated": "Chapters updated",
"ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
"ToastCollectionItemsAddSuccess": "Item(s) added to collection success",
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
"ToastCollectionRemoveSuccess": "Collection removed",
- "ToastCollectionUpdateFailed": "Failed to update collection",
"ToastCollectionUpdateSuccess": "Collection updated",
"ToastCoverUpdateFailed": "Cover update failed",
"ToastDeleteFileFailed": "Failed to delete file",
@@ -899,31 +963,28 @@
"ToastDeviceNameAlreadyExists": "Ereader device with that name already exists",
"ToastDeviceTestEmailFailed": "Failed to send test email",
"ToastDeviceTestEmailSuccess": "Test email sent",
- "ToastDeviceUpdateFailed": "Failed to update device",
- "ToastEmailSettingsUpdateFailed": "Failed to update email settings",
"ToastEmailSettingsUpdateSuccess": "Email settings updated",
"ToastEncodeCancelFailed": "Failed to cancel encode",
"ToastEncodeCancelSucces": "Encode canceled",
"ToastEpisodeDownloadQueueClearFailed": "Failed to clear queue",
"ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared",
+ "ToastEpisodeUpdateSuccess": "{0} episodes updated",
"ToastErrorCannotShare": "Cannot share natively on this device",
"ToastFailedToLoadData": "Failed to load data",
+ "ToastFailedToMatch": "Failed to match",
"ToastFailedToShare": "Failed to share",
- "ToastFailedToUpdateAccount": "Failed to update account",
- "ToastFailedToUpdateUser": "Failed to update user",
+ "ToastFailedToUpdate": "Failed to update",
"ToastInvalidImageUrl": "Invalid image URL",
+ "ToastInvalidMaxEpisodesToDownload": "Invalid max episodes to download",
"ToastInvalidUrl": "Invalid URL",
- "ToastItemCoverUpdateFailed": "Failed to update item cover",
"ToastItemCoverUpdateSuccess": "Item cover updated",
"ToastItemDeletedFailed": "Failed to delete item",
"ToastItemDeletedSuccess": "Deleted item",
- "ToastItemDetailsUpdateFailed": "Failed to update item details",
"ToastItemDetailsUpdateSuccess": "Item details updated",
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
"ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
"ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
- "ToastItemUpdateFailed": "Failed to update item",
"ToastItemUpdateSuccess": "Item updated",
"ToastLibraryCreateFailed": "Failed to create library",
"ToastLibraryCreateSuccess": "Library \"{0}\" created",
@@ -931,37 +992,42 @@
"ToastLibraryDeleteSuccess": "Library deleted",
"ToastLibraryScanFailedToStart": "Failed to start scan",
"ToastLibraryScanStarted": "Library scan started",
- "ToastLibraryUpdateFailed": "Failed to update library",
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
+ "ToastMatchAllAuthorsFailed": "Failed to match all authors",
+ "ToastMetadataFilesRemovedError": "Error removing metadata.{0} files",
+ "ToastMetadataFilesRemovedNoneFound": "No metadata.{0} files found in library",
+ "ToastMetadataFilesRemovedNoneRemoved": "No metadata.{0} files removed",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} files removed",
+ "ToastMustHaveAtLeastOnePath": "Must have at least one path",
"ToastNameEmailRequired": "Name and email are required",
"ToastNameRequired": "Name is required",
+ "ToastNewEpisodesFound": "{0} new episodes found",
"ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
"ToastNewUserCreatedSuccess": "New account created",
"ToastNewUserLibraryError": "Must select at least one library",
"ToastNewUserPasswordError": "Must have a password, only root user can have an empty password",
"ToastNewUserTagError": "Must select at least one tag",
"ToastNewUserUsernameError": "Enter a username",
+ "ToastNoNewEpisodesFound": "No new episodes found",
"ToastNoUpdatesNecessary": "No updates necessary",
"ToastNotificationCreateFailed": "Failed to create notification",
"ToastNotificationDeleteFailed": "Failed to delete notification",
"ToastNotificationFailedMaximum": "Max failed attempts must be >= 0",
"ToastNotificationQueueMaximum": "Max notification queue must be >= 0",
- "ToastNotificationSettingsUpdateFailed": "Failed to update notification settings",
"ToastNotificationSettingsUpdateSuccess": "Notification settings updated",
"ToastNotificationTestTriggerFailed": "Failed to trigger test notification",
"ToastNotificationTestTriggerSuccess": "Triggered test notification",
- "ToastNotificationUpdateFailed": "Failed to update notification",
"ToastNotificationUpdateSuccess": "Notification updated",
"ToastPlaylistCreateFailed": "Failed to create playlist",
"ToastPlaylistCreateSuccess": "Playlist created",
"ToastPlaylistRemoveSuccess": "Playlist removed",
- "ToastPlaylistUpdateFailed": "Failed to update playlist",
"ToastPlaylistUpdateSuccess": "Playlist updated",
"ToastPodcastCreateFailed": "Failed to create podcast",
"ToastPodcastCreateSuccess": "Podcast created successfully",
"ToastPodcastGetFeedFailed": "Failed to get podcast feed",
"ToastPodcastNoEpisodesInFeed": "No episodes found in RSS feed",
"ToastPodcastNoRssFeed": "Podcast does not have an RSS feed",
+ "ToastProgressIsNotBeingSynced": "Progress is not being synced, restart playback",
"ToastProviderCreatedFailed": "Failed to add provider",
"ToastProviderCreatedSuccess": "New provider added",
"ToastProviderNameAndUrlRequired": "Name and Url required",
@@ -984,18 +1050,17 @@
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
"ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionCloseFailed": "Failed to close session",
"ToastSessionDeleteFailed": "Failed to delete session",
"ToastSessionDeleteSuccess": "Session deleted",
+ "ToastSleepTimerDone": "Sleep timer done... zZzzZz",
"ToastSlugMustChange": "Slug contains invalid characters",
"ToastSlugRequired": "Slug is required",
"ToastSocketConnected": "Socket connected",
"ToastSocketDisconnected": "Socket disconnected",
"ToastSocketFailedToConnect": "Socket failed to connect",
"ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
"ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastTitleRequired": "Title is required",
"ToastUnknownError": "Unknown error",
diff --git a/client/strings/es.json b/client/strings/es.json
index 2f7781db02..b45d253457 100644
--- a/client/strings/es.json
+++ b/client/strings/es.json
@@ -1,6 +1,6 @@
{
- "ButtonAdd": "Agregar",
- "ButtonAddChapters": "Agregar Capitulo",
+ "ButtonAdd": "Agregaro",
+ "ButtonAddChapters": "Agregar",
"ButtonAddDevice": "Agregar Dispositivo",
"ButtonAddLibrary": "Crear Biblioteca",
"ButtonAddPodcasts": "Agregar Podcasts",
@@ -30,6 +30,7 @@
"ButtonEditChapters": "Editar Capítulo",
"ButtonEditPodcast": "Editar Podcast",
"ButtonEnable": "Permitir",
+ "ButtonFireAndFail": "Ejecutado y fallido",
"ButtonFireOnTest": "Activar evento de prueba",
"ButtonForceReScan": "Forzar Re-Escaneo",
"ButtonFullPath": "Ruta de Acceso Completa",
@@ -55,6 +56,7 @@
"ButtonOpenManager": "Abrir Editor",
"ButtonPause": "Pausar",
"ButtonPlay": "Reproducir",
+ "ButtonPlayAll": "Reproducir todo",
"ButtonPlaying": "Reproduciendo",
"ButtonPlaylists": "Listas de reproducción",
"ButtonPrevious": "Anterior",
@@ -64,12 +66,13 @@
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
"ButtonQueueAddItem": "Agregar a la Fila",
"ButtonQueueRemoveItem": "Remover de la Fila",
+ "ButtonQuickEmbed": "Inserción rápida",
"ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente",
"ButtonQuickMatch": "Encontrar Rápido",
"ButtonReScan": "Re-Escanear",
"ButtonRead": "Leer",
- "ButtonReadLess": "Lea menos",
- "ButtonReadMore": "Lea mas",
+ "ButtonReadLess": "Leer menos",
+ "ButtonReadMore": "Leer más",
"ButtonRefresh": "Refrecar",
"ButtonRemove": "Remover",
"ButtonRemoveAll": "Remover Todos",
@@ -160,6 +163,7 @@
"HeaderNotificationUpdate": "Notificación de actualización",
"HeaderNotifications": "Notificaciones",
"HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect",
+ "HeaderOpenListeningSessions": "Sesiones públicas de escucha",
"HeaderOpenRSSFeed": "Abrir fuente RSS",
"HeaderOtherFiles": "Otros Archivos",
"HeaderPasswordAuthentication": "Autenticación por contraseña",
@@ -177,6 +181,7 @@
"HeaderRemoveEpisodes": "Remover {0} Episodios",
"HeaderSavedMediaProgress": "Guardar Progreso de Multimedia",
"HeaderSchedule": "Horario",
+ "HeaderScheduleEpisodeDownloads": "Programar descargas automáticas de episodios",
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
"HeaderSession": "Sesión",
"HeaderSetBackupSchedule": "Programar Respaldo",
@@ -215,14 +220,18 @@
"LabelAddToPlaylist": "Añadido a la lista de reproducción",
"LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción",
"LabelAddedAt": "Añadido",
- "LabelAddedDate": "Añadido {0}",
+ "LabelAddedDate": "{0} Añadido",
"LabelAdminUsersOnly": "Solamente usuarios administradores",
"LabelAll": "Todos",
"LabelAllUsers": "Todos los Usuarios",
"LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
"LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca",
+ "LabelApiToken": "Token de la API",
"LabelAppend": "Adjuntar",
+ "LabelAudioBitrate": "Tasa de bits del audio (por ejemplo, 128k)",
+ "LabelAudioChannels": "Canales de audio (1 o 2)",
+ "LabelAudioCodec": "Códec de audio",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Nombre Apellido)",
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
@@ -235,6 +244,7 @@
"LabelAutoRegister": "Registro automático",
"LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión",
"LabelBackToUser": "Regresar a Usuario",
+ "LabelBackupAudioFiles": "Copia de seguridad de archivos de audio",
"LabelBackupLocation": "Ubicación del Respaldo",
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
"LabelBackupsEnableAutomaticBackupsHelp": "Respaldo Guardado en /metadata/backups",
@@ -243,15 +253,18 @@
"LabelBackupsNumberToKeep": "Numero de respaldos para conservar",
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.",
"LabelBitrate": "Tasa de bits",
+ "LabelBonus": "Bonus",
"LabelBooks": "Libros",
"LabelButtonText": "Texto del botón",
"LabelByAuthor": "por {0}",
"LabelChangePassword": "Cambiar Contraseña",
"LabelChannels": "Canales",
+ "LabelChapterCount": "{0} capítulos",
"LabelChapterTitle": "Titulo del Capítulo",
"LabelChapters": "Capítulos",
"LabelChaptersFound": "Capítulo Encontrado",
"LabelClickForMoreInfo": "Click para más información",
+ "LabelClickToUseCurrentValue": "Haz clic para utilizar el valor actual",
"LabelClosePlayer": "Cerrar reproductor",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Colapsar serie",
@@ -301,12 +314,25 @@
"LabelEmailSettingsTestAddress": "Probar Dirección",
"LabelEmbeddedCover": "Portada Integrada",
"LabelEnable": "Habilitar",
+ "LabelEncodingBackupLocation": "Se guardará una copia de seguridad de tus archivos de audio originales en:",
+ "LabelEncodingChaptersNotEmbedded": "Los capítulos no se incrustan en los audiolibros multipista.",
+ "LabelEncodingClearItemCache": "Asegúrese de purgar periódicamente la caché.",
+ "LabelEncodingFinishedM4B": "El M4B terminado se colocará en su carpeta de audiolibros en:",
+ "LabelEncodingInfoEmbedded": "Los metadatos se integrarán en las pistas de audio dentro de la carpeta de audiolibros.",
+ "LabelEncodingStartedNavigation": "Una vez iniciada la tarea, puedes salir de esta página.",
+ "LabelEncodingTimeWarning": "La codificación puede tardar hasta 30 minutos.",
+ "LabelEncodingWarningAdvancedSettings": "Advertencia: No actualice esta configuración a menos que esté familiarizado con las opciones de codificación de ffmpeg.",
+ "LabelEncodingWatcherDisabled": "Si ha desactivado la supervisión de los archivos, deberá volver a escanear este audiolibro más adelante.",
"LabelEnd": "Fin",
"LabelEndOfChapter": "Fin del capítulo",
"LabelEpisode": "Episodio",
+ "LabelEpisodeNotLinkedToRssFeed": "Episodio no enlazado al feed RSS",
+ "LabelEpisodeNumber": "Episodio #{0}",
"LabelEpisodeTitle": "Titulo de Episodio",
"LabelEpisodeType": "Tipo de Episodio",
+ "LabelEpisodeUrlFromRssFeed": "URL del episodio del feed RSS",
"LabelEpisodes": "Episodios",
+ "LabelEpisodic": "Episodios",
"LabelExample": "Ejemplo",
"LabelExpandSeries": "Ampliar serie",
"LabelExpandSubSeries": "Expandir la subserie",
@@ -334,6 +360,7 @@
"LabelFontScale": "Tamaño de fuente",
"LabelFontStrikethrough": "Tachado",
"LabelFormat": "Formato",
+ "LabelFull": "Completo",
"LabelGenre": "Genero",
"LabelGenres": "Géneros",
"LabelHardDeleteFile": "Eliminar Definitivamente",
@@ -389,6 +416,10 @@
"LabelLowestPriority": "Menor prioridad",
"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.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Número máximo de episodios nuevos que se descargarán por comprobación",
+ "LabelMaxEpisodesToKeep": "Número máximo de episodios que se mantendrán",
+ "LabelMaxEpisodesToKeepHelp": "El valor 0 no establece un límite máximo. Después de que se descargue automáticamente un nuevo episodio, esto eliminará el episodio más antiguo si tiene más de X episodios. Esto solo eliminará 1 episodio por nueva descarga.",
"LabelMediaPlayer": "Reproductor de Medios",
"LabelMediaType": "Tipo de multimedia",
"LabelMetaTag": "Metaetiqueta",
@@ -434,12 +465,14 @@
"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.",
"LabelOpenRSSFeed": "Abrir Fuente RSS",
"LabelOverwrite": "Sobrescribir",
+ "LabelPaginationPageXOfY": "Página {0} de {1}",
"LabelPassword": "Contraseña",
"LabelPath": "Ruta de carpeta",
"LabelPermanent": "Permanente",
"LabelPermissionsAccessAllLibraries": "Puede Accesar a Todas las bibliotecas",
"LabelPermissionsAccessAllTags": "Pueda Accesar a Todas las Etiquetas",
"LabelPermissionsAccessExplicitContent": "Puede Accesar a Contenido Explicito",
+ "LabelPermissionsCreateEreader": "Puede crear un gestor de proyectos",
"LabelPermissionsDelete": "Puede Eliminar",
"LabelPermissionsDownload": "Puede Descargar",
"LabelPermissionsUpdate": "Puede Actualizar",
@@ -459,9 +492,12 @@
"LabelPrimaryEbook": "Ebook principal",
"LabelProgress": "Progreso",
"LabelProvider": "Proveedor",
+ "LabelProviderAuthorizationValue": "Valor del encabezado de autorización",
"LabelPubDate": "Fecha de publicación",
"LabelPublishYear": "Año de publicación",
"LabelPublishedDate": "Publicado {0}",
+ "LabelPublishedDecade": "Década de publicación",
+ "LabelPublishedDecades": "Décadas publicadas",
"LabelPublisher": "Editor",
"LabelPublishers": "Editores",
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado",
@@ -481,21 +517,28 @@
"LabelRedo": "Rehacer",
"LabelRegion": "Región",
"LabelReleaseDate": "Fecha de Estreno",
+ "LabelRemoveAllMetadataAbs": "Eliminar todos los archivos metadata.abs",
+ "LabelRemoveAllMetadataJson": "Eliminar todos los archivos metadata.json",
"LabelRemoveCover": "Remover Portada",
+ "LabelRemoveMetadataFile": "Eliminar archivos de metadatos en carpetas de elementos de biblioteca",
+ "LabelRemoveMetadataFileHelp": "Elimine todos los archivos metadata.json y metadata.abs de sus carpetas {0}.",
"LabelRowsPerPage": "Filas por página",
"LabelSearchTerm": "Buscar Termino",
"LabelSearchTitle": "Buscar Titulo",
"LabelSearchTitleOrASIN": "Buscar Título o ASIN",
"LabelSeason": "Temporada",
+ "LabelSeasonNumber": "Sesión #{0}",
"LabelSelectAll": "Seleccionar todo",
"LabelSelectAllEpisodes": "Seleccionar todos los episodios",
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
"LabelSelectUsers": "Seleccionar usuarios",
"LabelSendEbookToDevice": "Enviar Ebook a...",
"LabelSequence": "Secuencia",
+ "LabelSerial": "Serial",
"LabelSeries": "Series",
"LabelSeriesName": "Nombre de la Serie",
"LabelSeriesProgress": "Progreso de la Serie",
+ "LabelServerLogLevel": "Nivel de registro del servidor",
"LabelServerYearReview": "Resumen del año del servidor ({0})",
"LabelSetEbookAsPrimary": "Establecer como primario",
"LabelSetEbookAsSupplementary": "Establecer como suplementario",
@@ -520,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.",
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "El porcentaje completado es mayor que",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "El tiempo restante es menor a (segundos)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Marcar el archivo multimedia como terminado cuando",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Saltar libros anteriores de la serie Continuada",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "El estante de la página de inicio de Continuar Serie muestra el primer libro no iniciado de una serie que tenga por lo menos un libro finalizado y no tenga libros en progreso. Habilitar esta opción le permitirá continuar series desde el último libro que ha completado en vez del primer libro que no ha empezado.",
"LabelSettingsParseSubtitles": "Extraer Subtítulos",
@@ -584,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} minutos",
"LabelTimeDurationXSeconds": "{0} segundos",
"LabelTimeInMinutes": "Tiempo en minutos",
+ "LabelTimeLeft": "Quedan {0}",
"LabelTimeListened": "Tiempo Escuchando",
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
"LabelTimeRemaining": "{0} restante",
@@ -591,6 +638,7 @@
"LabelTitle": "Título",
"LabelToolsEmbedMetadata": "Incrustar Metadatos",
"LabelToolsEmbedMetadataDescription": "Incrusta metadatos en los archivos de audio, incluyendo la portada y capítulos.",
+ "LabelToolsM4bEncoder": "Codificador M4B",
"LabelToolsMakeM4b": "Hacer Archivo de Audiolibro M4B",
"LabelToolsMakeM4bDescription": "Generar archivo de audiolibro .M4B con metadatos, imágenes de portada y capítulos incorporados.",
"LabelToolsSplitM4b": "Dividir M4B en Archivos MP3",
@@ -603,6 +651,7 @@
"LabelTracksMultiTrack": "Varias pistas",
"LabelTracksNone": "Ninguna pista",
"LabelTracksSingleTrack": "Una pista",
+ "LabelTrailer": "Tráiler",
"LabelType": "Tipo",
"LabelUnabridged": "No Abreviado",
"LabelUndo": "Deshacer",
@@ -616,8 +665,10 @@
"LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
"LabelUploaderDropFiles": "Suelte los Archivos",
"LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente",
+ "LabelUseAdvancedOptions": "Usar opciones avanzadas",
"LabelUseChapterTrack": "Usar pista por capitulo",
"LabelUseFullTrack": "Usar pista completa",
+ "LabelUseZeroForUnlimited": "Utilice 0 para ilimitado",
"LabelUser": "Usuario",
"LabelUsername": "Nombre de Usuario",
"LabelValue": "Valor",
@@ -630,8 +681,8 @@
"LabelWeekdaysToRun": "Correr en Días de la Semana",
"LabelXBooks": "{0} libros",
"LabelXItems": "{0} elementos",
- "LabelYearReviewHide": "Ocultar Year in Review",
- "LabelYearReviewShow": "Ver Year in Review",
+ "LabelYearReviewHide": "Ocultar Resumen del año",
+ "LabelYearReviewShow": "Resumen del año",
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
"LabelYourBookmarks": "Tus Marcadores",
"LabelYourPlaylists": "Tus Listas",
@@ -664,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "¿Estás seguro de que deseas eliminar el proveedor de metadatos personalizado \"{0}\"?",
"MessageConfirmDeleteNotification": "¿Estás seguro de que deseas eliminar esta notificación?",
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "¿Está seguro de que desea incrustar metadatos en {0} archivos de audio?",
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
@@ -675,6 +727,7 @@
"MessageConfirmPurgeCache": "Purgar el caché eliminará el directorio completo ubicado en /metadata/cache
. ¿Está seguro que desea eliminar el directorio del caché?",
"MessageConfirmPurgeItemsCache": "Purgar la caché de los elementos eliminará todo el directorio /metadata/cache/items
. ¿Estás seguro?",
"MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. ¿Deseas continuar?",
+ "MessageConfirmQuickMatchEpisodes": "El reconocimiento rápido de extensiones sobrescribirá los detalles si se encuentra una coincidencia. Se actualizarán las extensiones no reconocidas. ¿Está seguro?",
"MessageConfirmReScanLibraryItems": "¿Estás seguro de querer re escanear {0} elemento(s)?",
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
@@ -682,6 +735,7 @@
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
"MessageConfirmRemoveListeningSessions": "¿Está seguro que desea remover {0} sesiones de escuchar?",
+ "MessageConfirmRemoveMetadataFiles": "¿Está seguro de que desea eliminar todos los archivos de metadatos.{0} en las carpetas de elementos de su biblioteca?",
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
@@ -697,6 +751,7 @@
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas",
"MessageEmbedFailed": "¡Error al insertar!",
"MessageEmbedFinished": "Incrustación Terminada!",
+ "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": "URL de la fuente será {0}",
@@ -724,7 +779,7 @@
"MessageNoBackups": "Sin Respaldos",
"MessageNoBookmarks": "Sin marcadores",
"MessageNoChapters": "Sin capítulos",
- "MessageNoCollections": "Sin Colecciones",
+ "MessageNoCollections": "Sin colecciones",
"MessageNoCoversFound": "Ninguna Portada Encontrada",
"MessageNoDescription": "Sin Descripción",
"MessageNoDevices": "Sin dispositivos",
@@ -741,6 +796,7 @@
"MessageNoLogs": "No hay logs",
"MessageNoMediaProgress": "Multimedia sin Progreso",
"MessageNoNotifications": "Ninguna Notificación",
+ "MessageNoPodcastFeed": "Podcast no válido: Sin feed",
"MessageNoPodcastsFound": "Ningún podcast encontrado",
"MessageNoResults": "Sin Resultados",
"MessageNoSearchResultsFor": "No hay resultados para la búsqueda \"{0}\"",
@@ -757,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Crear una lista de reproducción a partir de una colección",
"MessagePleaseWait": "Por favor, espera...",
"MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar",
+ "MessagePodcastSearchField": "Introduzca el término de búsqueda o la URL de la fuente RSS",
+ "MessageQuickEmbedInProgress": "Integración rápida en proceso",
+ "MessageQuickEmbedQueue": "En cola para inserción rápida ({0} en cola)",
+ "MessageQuickMatchAllEpisodes": "Combina rápidamente todos los episodios",
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.",
"MessageRemoveChapter": "Remover capítulos",
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
@@ -774,6 +834,41 @@
"MessageShareExpiresIn": "Caduduca en {0}",
"MessageShareURLWillBe": "La URL para compartir será {0} ",
"MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?",
+ "MessageTaskAudioFileNotWritable": "El archivo de audio \"{0}\" no se puede grabar",
+ "MessageTaskCanceledByUser": "Tarea cancelada por el usuario",
+ "MessageTaskDownloadingEpisodeDescription": "Descargando el episodio \"{0}\"",
+ "MessageTaskEmbeddingMetadata": "Inserción de metadatos",
+ "MessageTaskEmbeddingMetadataDescription": "Inserción de metadatos en el audiolibro \"{0}\"",
+ "MessageTaskEncodingM4b": "Codificación M4B",
+ "MessageTaskEncodingM4bDescription": "Codificación del audiolibro \"{0}\" en un único archivo m4b",
+ "MessageTaskFailed": "Fallida",
+ "MessageTaskFailedToBackupAudioFile": "Error en la copia de seguridad del archivo de audio \"{0}\"",
+ "MessageTaskFailedToCreateCacheDirectory": "Error al crear el directorio de la caché",
+ "MessageTaskFailedToEmbedMetadataInFile": "Error al incrustar metadatos en el archivo \"{0}\"",
+ "MessageTaskFailedToMergeAudioFiles": "Error al fusionar archivos de audio",
+ "MessageTaskFailedToMoveM4bFile": "Error al mover el archivo m4b",
+ "MessageTaskFailedToWriteMetadataFile": "Error al escribir el archivo de metadatos",
+ "MessageTaskMatchingBooksInLibrary": "Libros coincidentes en la biblioteca \"{0}\"",
+ "MessageTaskNoFilesToScan": "Sin archivos para escanear",
+ "MessageTaskOpmlImport": "Importar OPML",
+ "MessageTaskOpmlImportDescription": "Creando podcasts a partir de {0} fuentes RSS",
+ "MessageTaskOpmlImportFeed": "Feed de importación OPML",
+ "MessageTaskOpmlImportFeedDescription": "Importando el feed RSS \"{0}\"",
+ "MessageTaskOpmlImportFeedFailed": "No se puede obtener el podcast",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Creando podcast \"{0}\"",
+ "MessageTaskOpmlImportFeedPodcastExists": "Podcast ya existe en la ruta",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Error al crear podcast",
+ "MessageTaskOpmlImportFinished": "Añadido {0} podcasts",
+ "MessageTaskOpmlParseFailed": "No se pudo analizar el archivo OPML",
+ "MessageTaskOpmlParseFastFail": "No se encontró la etiqueta del archivo OPML no válido O no se encontró la etiqueta ",
+ "MessageTaskOpmlParseNoneFound": "No se encontraron fuentes en el archivo OPML",
+ "MessageTaskScanItemsAdded": "{0} añadido",
+ "MessageTaskScanItemsMissing": "Falta {0}",
+ "MessageTaskScanItemsUpdated": "{0} actualizado",
+ "MessageTaskScanNoChangesNeeded": "No se necesitan cambios",
+ "MessageTaskScanningFileChanges": "Escaneando cambios en el archivo en \"{0}\"",
+ "MessageTaskScanningLibrary": "Escaneando la biblioteca \"{0}\"",
+ "MessageTaskTargetDirectoryNotWritable": "El directorio de destino no se puede escribir",
"MessageThinking": "Pensando...",
"MessageUploaderItemFailed": "Error al Subir",
"MessageUploaderItemSuccess": "¡Éxito al Subir!",
@@ -791,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.",
"NoteUploaderOnlyAudioFiles": "Si sube solamente archivos de audio, cada archivo se manejará como un audiolibro por separado.",
"NoteUploaderUnsupportedFiles": "Se ignorarán los archivos no soportados. Al elegir o arrastrar una carpeta, los archivos que no estén dentro de una subcarpeta serán ignorados.",
+ "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",
+ "NotificationOnTestDescription": "Evento para probar el sistema de notificaciones",
"PlaceholderNewCollection": "Nuevo nombre de la colección",
"PlaceholderNewFolderPath": "Nueva ruta de carpeta",
"PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción",
@@ -814,14 +913,13 @@
"StatsTopNarrators": "NARRADORES DESTACADOS",
"StatsTotalDuration": "Con una duración total de…",
"StatsYearInReview": "RESEÑA DEL AÑO",
- "ToastAccountUpdateFailed": "Error al actualizar cuenta",
"ToastAccountUpdateSuccess": "Cuenta actualizada",
"ToastAppriseUrlRequired": "Debes ingresar una URL de Apprise",
+ "ToastAsinRequired": "Se requiere ASIN",
"ToastAuthorImageRemoveSuccess": "Se eliminó la imagen del autor",
"ToastAuthorNotFound": "No se encontró el autor \"{0}\"",
"ToastAuthorRemoveSuccess": "Autor eliminado",
"ToastAuthorSearchNotFound": "No se encontró al autor",
- "ToastAuthorUpdateFailed": "Error al actualizar el autor",
"ToastAuthorUpdateMerged": "Autor combinado",
"ToastAuthorUpdateSuccess": "Autor actualizado",
"ToastAuthorUpdateSuccessNoImageFound": "Autor actualizado (Imagen no encontrada)",
@@ -832,29 +930,29 @@
"ToastBackupDeleteSuccess": "Respaldo eliminado",
"ToastBackupInvalidMaxKeep": "Número no válido de copias de seguridad a conservar",
"ToastBackupInvalidMaxSize": "Tamaño máximo de copia de seguridad no válido",
- "ToastBackupPathUpdateFailed": "Error al actualizar la ruta de la copia de seguridad",
"ToastBackupRestoreFailed": "Error al restaurar el respaldo",
"ToastBackupUploadFailed": "Error al subir el respaldo",
"ToastBackupUploadSuccess": "Respaldo cargado",
"ToastBatchDeleteFailed": "Error al eliminar por lotes",
"ToastBatchDeleteSuccess": "Borrado por lotes correcto",
+ "ToastBatchQuickMatchFailed": "¡Error en la sincronización rápida por lotes!",
+ "ToastBatchQuickMatchStarted": "¡Se inició el lote de búsqueda rápida de {0} libros!",
"ToastBatchUpdateFailed": "Subida masiva fallida",
"ToastBatchUpdateSuccess": "Subida masiva exitosa",
"ToastBookmarkCreateFailed": "Error al crear marcador",
"ToastBookmarkCreateSuccess": "Marcador Agregado",
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
- "ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
"ToastBookmarkUpdateSuccess": "Marcador actualizado",
"ToastCachePurgeFailed": "Error al purgar el caché",
"ToastCachePurgeSuccess": "Caché purgado de manera exitosa",
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
"ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título",
"ToastChaptersRemoved": "Capítulos eliminados",
+ "ToastChaptersUpdated": "Capítulos actualizados",
"ToastCollectionItemsAddFailed": "Artículo(s) añadido(s) a la colección fallido(s)",
"ToastCollectionItemsAddSuccess": "Artículo(s) añadido(s) a la colección correctamente",
"ToastCollectionItemsRemoveSuccess": "Elementos(s) removidos de la colección",
"ToastCollectionRemoveSuccess": "Colección removida",
- "ToastCollectionUpdateFailed": "Error al actualizar la colección",
"ToastCollectionUpdateSuccess": "Colección actualizada",
"ToastCoverUpdateFailed": "Error al actualizar la cubierta",
"ToastDeleteFileFailed": "Error el eliminar archivo",
@@ -863,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "Un libro electrónico ya existe con ese nombre",
"ToastDeviceTestEmailFailed": "Error al enviar correo de prueba",
"ToastDeviceTestEmailSuccess": "Correo electrónico de prueba enviado",
- "ToastDeviceUpdateFailed": "Error al actualizar el dispositivo",
- "ToastEmailSettingsUpdateFailed": "Error al actualizar la configuración del correo electrónico",
"ToastEmailSettingsUpdateSuccess": "Configuración del correo electrónico actualizada",
"ToastEncodeCancelFailed": "No se pudo cancelar la codificación",
"ToastEncodeCancelSucces": "Codificación cancelada",
"ToastEpisodeDownloadQueueClearFailed": "No se pudo borrar la cola",
"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",
"ToastFailedToLoadData": "Error al cargar data",
+ "ToastFailedToMatch": "Error al emparejar",
"ToastFailedToShare": "Error al compartir",
- "ToastFailedToUpdateAccount": "Error al actualizar la cuenta",
- "ToastFailedToUpdateUser": "Error al actualizar el usuario",
+ "ToastFailedToUpdate": "Error al actualizar",
"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",
- "ToastItemCoverUpdateFailed": "Error al actualizar la portada del elemento",
"ToastItemCoverUpdateSuccess": "Portada del elemento actualizada",
"ToastItemDeletedFailed": "Error al eliminar el elemento",
"ToastItemDeletedSuccess": "Elemento borrado",
- "ToastItemDetailsUpdateFailed": "Error al actualizar los detalles del elemento",
"ToastItemDetailsUpdateSuccess": "Detalles del Elemento Actualizados",
"ToastItemMarkedAsFinishedFailed": "Error al marcar como terminado",
"ToastItemMarkedAsFinishedSuccess": "Elemento marcado como terminado",
"ToastItemMarkedAsNotFinishedFailed": "No se ha podido marcar como no finalizado",
"ToastItemMarkedAsNotFinishedSuccess": "Elemento marcado como No Terminado",
- "ToastItemUpdateFailed": "Error al actualizar el elemento",
"ToastItemUpdateSuccess": "Elemento actualizado",
"ToastLibraryCreateFailed": "Error al crear biblioteca",
"ToastLibraryCreateSuccess": "Biblioteca \"{0}\" creada",
@@ -895,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Biblioteca eliminada",
"ToastLibraryScanFailedToStart": "Error al iniciar el escaneo",
"ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca",
- "ToastLibraryUpdateFailed": "Error al actualizar la biblioteca",
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada",
- "ToastNameEmailRequired": "Nombre y correo electrónico obligatorios",
+ "ToastMatchAllAuthorsFailed": "No coincide con todos los autores",
+ "ToastMetadataFilesRemovedError": "Error al eliminar metadatos de {0} archivo(s)",
+ "ToastMetadataFilesRemovedNoneFound": "No hay metadatos.{0} archivo(s) encontrado(s) en la biblioteca",
+ "ToastMetadataFilesRemovedNoneRemoved": "Sin metadatos.{0} archivo(s) eliminado(s)",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadatos.{1} archivos eliminados",
+ "ToastMustHaveAtLeastOnePath": "Debe tener al menos una ruta",
+ "ToastNameEmailRequired": "Son obligatorios el nombre y el correo electrónico",
"ToastNameRequired": "Nombre obligatorio",
+ "ToastNewEpisodesFound": "{0} nuevo(s) episodio(s) encontrado(s)",
"ToastNewUserCreatedFailed": "Error al crear la cuenta: \"{0}\"",
"ToastNewUserCreatedSuccess": "Nueva cuenta creada",
"ToastNewUserLibraryError": "Debes seleccionar al menos una biblioteca",
"ToastNewUserPasswordError": "Debes tener una contraseña, solo el usuario root puede estar sin contraseña",
"ToastNewUserTagError": "Debes seleccionar al menos una etiqueta",
"ToastNewUserUsernameError": "Introduce un nombre de usuario",
+ "ToastNoNewEpisodesFound": "No se encontraron nuevos episodios",
"ToastNoUpdatesNecessary": "No es necesario actualizar",
"ToastNotificationCreateFailed": "Error al crear notificación",
"ToastNotificationDeleteFailed": "Error al borrar la notificación",
"ToastNotificationFailedMaximum": "El número máximo de intentos fallidos debe ser ≥ 0",
"ToastNotificationQueueMaximum": "La cola de notificación máxima debe ser ≥ 0",
- "ToastNotificationSettingsUpdateFailed": "Error al actualizar los ajustes de la notificación",
"ToastNotificationSettingsUpdateSuccess": "Ajustes de la notificación actualizados",
"ToastNotificationTestTriggerFailed": "No se ha podido activar la notificación de prueba",
"ToastNotificationTestTriggerSuccess": "Notificación de prueba activada",
- "ToastNotificationUpdateFailed": "No se ha podido actualizar la notificación",
"ToastNotificationUpdateSuccess": "Notificación actualizada",
"ToastPlaylistCreateFailed": "Error al crear la lista de reproducción",
"ToastPlaylistCreateSuccess": "Lista de reproducción creada",
"ToastPlaylistRemoveSuccess": "Lista de reproducción eliminada",
- "ToastPlaylistUpdateFailed": "Error al actualizar la lista de reproducción",
"ToastPlaylistUpdateSuccess": "Lista de reproducción actualizada",
"ToastPodcastCreateFailed": "Error al crear podcast",
"ToastPodcastCreateSuccess": "Podcast creado",
"ToastPodcastGetFeedFailed": "No se puede obtener el podcast",
"ToastPodcastNoEpisodesInFeed": "No se han encontrado episodios en el feed del RSS",
"ToastPodcastNoRssFeed": "El podcast no tiene feed RSS",
+ "ToastProgressIsNotBeingSynced": "El progreso no se sincroniza, reinicia la reproducción",
"ToastProviderCreatedFailed": "Error al añadir el proveedor",
"ToastProviderCreatedSuccess": "Nuevo proveedor añadido",
"ToastProviderNameAndUrlRequired": "Nombre y Url obligatorios",
@@ -948,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "Ebook enviado al dispositivo \"{0}\"",
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
"ToastSeriesUpdateSuccess": "Serie actualizada",
- "ToastServerSettingsUpdateFailed": "Error al actualizar configuración del servidor",
"ToastServerSettingsUpdateSuccess": "Configuración del servidor actualizada",
"ToastSessionCloseFailed": "Error al cerrar la sesión",
"ToastSessionDeleteFailed": "Error al eliminar sesión",
"ToastSessionDeleteSuccess": "Sesión eliminada",
+ "ToastSleepTimerDone": "Temporizador de apagado automático activado... zZzzZz",
"ToastSlugMustChange": "El slug contiene caracteres no válidos",
"ToastSlugRequired": "Slug obligatorio",
"ToastSocketConnected": "Socket conectado",
"ToastSocketDisconnected": "Socket desconectado",
"ToastSocketFailedToConnect": "Error al conectar al Socket",
"ToastSortingPrefixesEmptyError": "Debe tener por lo menos 1 prefijo para ordenar",
- "ToastSortingPrefixesUpdateFailed": "Error al actualizar los prefijos de ordenar",
"ToastSortingPrefixesUpdateSuccess": "Prefijos de ordenar actualizaron ({0} items)",
"ToastTitleRequired": "Título obligatorio",
"ToastUnknownError": "Error desconocido",
diff --git a/client/strings/et.json b/client/strings/et.json
index 45da898959..8d256c0114 100644
--- a/client/strings/et.json
+++ b/client/strings/et.json
@@ -9,6 +9,7 @@
"ButtonApply": "Rakenda",
"ButtonApplyChapters": "Rakenda peatükid",
"ButtonAuthors": "Autorid",
+ "ButtonBack": "Tagasi",
"ButtonBrowseForFolder": "Sirvi kausta",
"ButtonCancel": "Tühista",
"ButtonCancelEncode": "Tühista kodeerimine",
@@ -18,6 +19,7 @@
"ButtonChooseFiles": "Vali failid",
"ButtonClearFilter": "Tühista filter",
"ButtonCloseFeed": "Sulge voog",
+ "ButtonCloseSession": "Sulge avatud sessioon",
"ButtonCollections": "Kogud",
"ButtonConfigureScanner": "Konfigureeri skanner",
"ButtonCreate": "Loo",
@@ -27,6 +29,7 @@
"ButtonEdit": "Muuda",
"ButtonEditChapters": "Muuda peatükke",
"ButtonEditPodcast": "Muuda podcasti",
+ "ButtonEnable": "Aktiveeri",
"ButtonForceReScan": "Sunnitud uuestiskaneerimine",
"ButtonFullPath": "Täielik asukoht",
"ButtonHide": "Peida",
@@ -43,13 +46,18 @@
"ButtonMatchAllAuthors": "Sobita kõik autorid",
"ButtonMatchBooks": "Sobita raamatud",
"ButtonNevermind": "Pole tähtis",
+ "ButtonNext": "Järgmine",
"ButtonNextChapter": "Järgmine peatükk",
+ "ButtonNextItemInQueue": "Järgmine kirje järjekorras",
+ "ButtonOk": "Ok",
"ButtonOpenFeed": "Ava voog",
"ButtonOpenManager": "Ava haldur",
"ButtonPause": "Peata",
"ButtonPlay": "Mängi",
+ "ButtonPlayAll": "Mängi kõik",
"ButtonPlaying": "Mängib",
"ButtonPlaylists": "Esitusloendid",
+ "ButtonPrevious": "Eelmine",
"ButtonPreviousChapter": "Eelmine peatükk",
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
"ButtonPurgeItemsCache": "Tühjenda esemete vahemälu",
@@ -58,6 +66,9 @@
"ButtonQuickMatch": "Kiire sobitamine",
"ButtonReScan": "Uuestiskaneeri",
"ButtonRead": "Loe",
+ "ButtonReadLess": "Loe vähem",
+ "ButtonReadMore": "Loe rohkem",
+ "ButtonRefresh": "Värskenda",
"ButtonRemove": "Eemalda",
"ButtonRemoveAll": "Eemalda kõik",
"ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed",
@@ -211,7 +222,7 @@
"LabelBackupLocation": "Varukoopia asukoht",
"LabelBackupsEnableAutomaticBackups": "Luba automaatsed varukoopiad",
"LabelBackupsEnableAutomaticBackupsHelp": "Varukoopiad salvestatakse /metadata/backups kausta",
- "LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des)",
+ "LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des) (0 lõpmatu suuruse jaoks)",
"LabelBackupsMaxBackupSizeHelp": "Kaitsena valesti seadistamise vastu ebaõnnestuvad varukoopiad, kui need ületavad seadistatud suuruse.",
"LabelBackupsNumberToKeep": "Varukoopiate arv, mida hoida",
"LabelBackupsNumberToKeepHelp": "Ühel ajal eemaldatakse ainult 1 varukoopia, seega kui teil on juba rohkem varukoopiaid kui siin määratud, peaksite need käsitsi eemaldama.",
@@ -449,7 +460,7 @@
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
"LabelSettingsParseSubtitles": "Lugege subtiitreid",
- "LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest. Subtiitrid peavad olema eraldatud \" - \". Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
+ "LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest. Subtiitrid peavad olema eraldatud kasutades \" - \". Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",
"LabelSettingsPreferMatchedMetadataHelp": "Sobitatud andmed kirjutavad Kiir Sobitamise kasutamisel üle üksikasjad.",
"LabelSettingsSkipMatchingBooksWithASIN": "Jätke ASIN-iga sobituvad raamatud vahele",
@@ -682,10 +693,8 @@
"PlaceholderNewPlaylist": "Uue esitusloendi nimi",
"PlaceholderSearch": "Otsi...",
"PlaceholderSearchEpisode": "Otsi episoodi...",
- "ToastAccountUpdateFailed": "Konto värskendamine ebaõnnestus",
"ToastAccountUpdateSuccess": "Konto on värskendatud",
"ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud",
- "ToastAuthorUpdateFailed": "Autori värskendamine ebaõnnestus",
"ToastAuthorUpdateMerged": "Autor liidetud",
"ToastAuthorUpdateSuccess": "Autor värskendatud",
"ToastAuthorUpdateSuccessNoImageFound": "Autor värskendatud (pilti ei leitud)",
@@ -701,17 +710,13 @@
"ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus",
"ToastBookmarkCreateSuccess": "Järjehoidja lisatud",
"ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud",
- "ToastBookmarkUpdateFailed": "Järjehoidja värskendamine ebaõnnestus",
"ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud",
"ToastChaptersHaveErrors": "Peatükkidel on vigu",
"ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri",
"ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist",
"ToastCollectionRemoveSuccess": "Kogum eemaldatud",
- "ToastCollectionUpdateFailed": "Kogumi värskendamine ebaõnnestus",
"ToastCollectionUpdateSuccess": "Kogum värskendatud",
- "ToastItemCoverUpdateFailed": "Üksuse kaane värskendamine ebaõnnestus",
"ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud",
- "ToastItemDetailsUpdateFailed": "Üksuse üksikasjade värskendamine ebaõnnestus",
"ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud",
"ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus",
"ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud",
@@ -723,12 +728,10 @@
"ToastLibraryDeleteSuccess": "Raamatukogu kustutatud",
"ToastLibraryScanFailedToStart": "Skanneerimine ei käivitunud",
"ToastLibraryScanStarted": "Raamatukogu skaneerimine alustatud",
- "ToastLibraryUpdateFailed": "Raamatukogu värskendamine ebaõnnestus",
"ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud",
"ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus",
"ToastPlaylistCreateSuccess": "Esitusloend loodud",
"ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud",
- "ToastPlaylistUpdateFailed": "Esitusloendi värskendamine ebaõnnestus",
"ToastPlaylistUpdateSuccess": "Esitusloend värskendatud",
"ToastPodcastCreateFailed": "Podcasti loomine ebaõnnestus",
"ToastPodcastCreateSuccess": "Podcast loodud edukalt",
diff --git a/client/strings/fi.json b/client/strings/fi.json
index b1840bb062..9795139b40 100644
--- a/client/strings/fi.json
+++ b/client/strings/fi.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Valitse tiedostot",
"ButtonClearFilter": "Poista suodatus",
"ButtonCloseFeed": "Sulje syöte",
+ "ButtonCloseSession": "Sulje Avoin Sessio",
"ButtonCollections": "Kokoelmat",
"ButtonConfigureScanner": "Skannerin asetukset",
"ButtonCreate": "Luo",
@@ -28,6 +29,9 @@
"ButtonEdit": "Muokkaa",
"ButtonEditChapters": "Muokkaa lukuja",
"ButtonEditPodcast": "Muokkaa podcastia",
+ "ButtonEnable": "Aktivoi",
+ "ButtonFireAndFail": "Laukaise ja epäonnistu",
+ "ButtonFireOnTest": "Laukaise onTest tapahtuma",
"ButtonForceReScan": "Pakota uudelleenskannaus",
"ButtonFullPath": "Koko polku",
"ButtonHide": "Piilota",
@@ -46,10 +50,13 @@
"ButtonNevermind": "Ei sittenkään",
"ButtonNext": "Seuraava",
"ButtonNextChapter": "Seuraava luku",
+ "ButtonNextItemInQueue": "Seuraava jonossa",
+ "ButtonOk": "Ok",
"ButtonOpenFeed": "Avaa syöte",
"ButtonOpenManager": "Avaa hallinta",
"ButtonPause": "Pysäytä",
"ButtonPlay": "Toista",
+ "ButtonPlayAll": "Toista kaikki",
"ButtonPlaying": "Toistetaan",
"ButtonPlaylists": "Soittolistat",
"ButtonPrevious": "Edellinen",
@@ -90,6 +97,7 @@
"ButtonStats": "Tilastot",
"ButtonSubmit": "Lähetä",
"ButtonTest": "Testi",
+ "ButtonUnlinkOpenId": "Poista OpenID linkitys",
"ButtonUpload": "Lähetä palvelimelle",
"ButtonUploadBackup": "Lähetä varmuuskopio",
"ButtonUploadCover": "Lähetä kansikuva",
@@ -102,6 +110,7 @@
"ErrorUploadFetchMetadataNoResults": "Metadatan haku epäonnistui, yritä päivittää Teoksen nimi ja/tai Tekijä",
"ErrorUploadLacksTitle": "Pitää sisältää Nimi",
"HeaderAccount": "Tili",
+ "HeaderAddCustomMetadataProvider": "Lisää mukautettu metadata tarjoaja",
"HeaderAdvanced": "Edistynyt",
"HeaderAppriseNotificationSettings": "Apprise-ilmoitusasetukset",
"HeaderAudioTracks": "Ääniraidat",
@@ -126,38 +135,60 @@
"HeaderEreaderDevices": "E-lukijalaitteet",
"HeaderEreaderSettings": "E-lukijan asetukset",
"HeaderFiles": "Tiedostot",
+ "HeaderFindChapters": "Etsi kappaleet",
"HeaderIgnoredFiles": "Ohitetut tiedostot",
+ "HeaderLastListeningSession": "Edellinen kuuntelukerta",
"HeaderLatestEpisodes": "Viimeisimmät jaksot",
"HeaderLibraries": "Kirjastot",
"HeaderLibraryFiles": "Kirjaston tiedostot",
"HeaderLibraryStats": "Kirjaston tilastot",
+ "HeaderListeningSessions": "Kuuntelukerrat",
"HeaderListeningStats": "Kuuntelutilastot",
+ "HeaderLogin": "Kirjaudu",
"HeaderLogs": "Lokit",
+ "HeaderManageGenres": "Hallitse lajityyppejä",
+ "HeaderManageTags": "Hallitse tageja",
+ "HeaderMetadataToEmbed": "Sisällytettävä metadata",
"HeaderNewAccount": "Uusi tili",
"HeaderNewLibrary": "Uusi kirjasto",
+ "HeaderNotificationCreate": "Luo ilmoitus",
+ "HeaderNotificationUpdate": "Päivitä ilmoitus",
"HeaderNotifications": "Ilmoitukset",
"HeaderOpenRSSFeed": "Avaa RSS-syöte",
"HeaderOtherFiles": "Muut tiedostot",
"HeaderPermissions": "Käyttöoikeudet",
+ "HeaderPlayerQueue": "Soittimen jono",
+ "HeaderPlayerSettings": "Soittimen asetukset",
"HeaderPlaylist": "Soittolista",
"HeaderPlaylistItems": "Soittolistan kohteet",
+ "HeaderPodcastsToAdd": "Lisättävät podcastit",
+ "HeaderPreviewCover": "Esikatsele kansikuvaa",
"HeaderRSSFeedGeneral": "RSS yksityiskohdat",
"HeaderRSSFeedIsOpen": "RSS syöte on avoinna",
+ "HeaderRSSFeeds": "RSS syötteet",
"HeaderRemoveEpisode": "Poista jakso",
"HeaderRemoveEpisodes": "Poista {0} jaksoa",
"HeaderSchedule": "Ajoita",
"HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset",
+ "HeaderSession": "Istunto",
"HeaderSetBackupSchedule": "Aseta varmuuskopiointiaikataulu",
"HeaderSettings": "Asetukset",
"HeaderSettingsExperimental": "Kokeelliset ominaisuudet",
+ "HeaderSettingsGeneral": "Yleiset",
"HeaderSleepTimer": "Uniajastin",
"HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)",
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
+ "HeaderStatsTop5Genres": "Top 5 lajityypit",
"HeaderTableOfContents": "Sisällysluettelo",
"HeaderTools": "Työkalut",
+ "HeaderUpdateAccount": "Päivitä tili",
+ "HeaderUpdateAuthor": "Päivitä kirjailija",
+ "HeaderUpdateLibrary": "Päivitä kirjasto",
"HeaderUsers": "Käyttäjät",
"HeaderYourStats": "Tilastosi",
+ "LabelAbridged": "Lyhennetty",
"LabelAccountType": "Tilin tyyppi",
+ "LabelAccountTypeAdmin": "Järjestelmänvalvoja",
"LabelAccountTypeGuest": "Vieras",
"LabelAccountTypeUser": "Käyttäjä",
"LabelActivity": "Toiminta",
@@ -166,22 +197,29 @@
"LabelAddToPlaylist": "Lisää soittolistaan",
"LabelAddToPlaylistBatch": "Lisää {0} kohdetta soittolistaan",
"LabelAddedAt": "Lisätty listalle",
+ "LabelAddedDate": "Lisätty {0}",
+ "LabelAdminUsersOnly": "Vain järjestelmänvalvojat",
"LabelAll": "Kaikki",
"LabelAllUsers": "Kaikki käyttäjät",
"LabelAllUsersExcludingGuests": "Kaikki käyttäjät vieraita lukuun ottamatta",
"LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat",
+ "LabelAlreadyInYourLibrary": "Jo kirjastossasi",
"LabelAuthor": "Tekijä",
"LabelAuthorFirstLast": "Tekijä (Etunimi Sukunimi)",
"LabelAuthorLastFirst": "Tekijä (Sukunimi, Etunimi)",
"LabelAuthors": "Tekijät",
"LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti",
+ "LabelBackToUser": "Takaisin käyttäjään",
+ "LabelBackupLocation": "Varmuuskopiointipaikka",
"LabelBackupsEnableAutomaticBackups": "Ota automaattinen varmuuskopiointi käyttöön",
"LabelBackupsEnableAutomaticBackupsHelp": "Varmuuskopiot tallennettu kansioon /metadata/backups",
"LabelBackupsMaxBackupSize": "Varmuuskopion enimmäiskoko (Gt) (0 rajaton)",
"LabelBackupsNumberToKeep": "Säilytettävien varmuuskopioiden määrä",
+ "LabelBitrate": "Bittinopeus",
"LabelBooks": "Kirjat",
"LabelButtonText": "Painikkeen teksti",
"LabelChangePassword": "Vaihda salasana",
+ "LabelChannels": "Kanavat",
"LabelChapters": "Luvut",
"LabelClickForMoreInfo": "Napsauta saadaksesi lisätietoja",
"LabelClosePlayer": "Sulje soitin",
@@ -196,79 +234,205 @@
"LabelContinueSeries": "Jatka sarjoja",
"LabelCover": "Kansikuva",
"LabelCoverImageURL": "Kansikuvan URL-osoite",
+ "LabelCreatedAt": "Luotu",
"LabelCurrent": "Nykyinen",
+ "LabelDays": "Päivää",
"LabelDescription": "Kuvaus",
"LabelDevice": "Laite",
"LabelDeviceInfo": "Laitteen tiedot",
+ "LabelDiscover": "Löydä",
"LabelDownload": "Lataa",
"LabelDownloadNEpisodes": "Lataa {0} jaksoa",
"LabelDuration": "Kesto",
+ "LabelDurationComparisonLonger": "({0} pidempi)",
+ "LabelDurationComparisonShorter": "({0} lyhyempi)",
"LabelEbook": "E-kirja",
"LabelEbooks": "E-kirjat",
"LabelEdit": "Muokkaa",
"LabelEmail": "Sähköposti",
+ "LabelEmailSettingsTestAddress": "Testiosoite",
+ "LabelEmbeddedCover": "Upotettu kansikuva",
"LabelEnable": "Ota käyttöön",
+ "LabelEnd": "Loppu",
"LabelEndOfChapter": "Luvun loppu",
"LabelEpisode": "Jakso",
+ "LabelEpisodes": "Jaksot",
+ "LabelExample": "Esimerkki",
+ "LabelFeedURL": "Syötteen URL",
"LabelFile": "Tiedosto",
"LabelFileBirthtime": "Tiedoston syntymäaika",
+ "LabelFileBornDate": "Syntynyt {0}",
"LabelFileModified": "Muutettu tiedosto",
+ "LabelFileModifiedDate": "Muokattu {0}",
"LabelFilename": "Tiedostonimi",
+ "LabelFindEpisodes": "Etsi jaksoja",
+ "LabelFinished": "Valmis",
"LabelFolder": "Kansio",
+ "LabelFolders": "Kansiot",
+ "LabelGenre": "Lajityyppi",
+ "LabelGenres": "Lajityypit",
+ "LabelHost": "Isäntä",
+ "LabelHours": "Tunnit",
"LabelInProgress": "Kesken",
"LabelIncomplete": "Keskeneräinen",
+ "LabelIntervalEvery12Hours": "12 tunnin välein",
+ "LabelIntervalEvery15Minutes": "15 minuutin välein",
+ "LabelIntervalEvery2Hours": "2 tunnin välein",
+ "LabelIntervalEvery30Minutes": "30 minuutin välein",
+ "LabelIntervalEvery6Hours": "6 tunnin välein",
+ "LabelIntervalEveryDay": "Joka päivä",
+ "LabelIntervalEveryHour": "Joka tunti",
+ "LabelItem": "Kohde",
"LabelLanguage": "Kieli",
+ "LabelLanguageDefaultServer": "Palvelimen oletuskieli",
+ "LabelLanguages": "Kielet",
+ "LabelLastBookAdded": "Viimeisin lisätty kirja",
+ "LabelLibrary": "Kirjasto",
+ "LabelLineSpacing": "Riviväli",
"LabelListenAgain": "Kuuntele uudelleen",
"LabelMediaType": "Mediatyyppi",
+ "LabelMinute": "Minuutti",
+ "LabelMinutes": "Minuutit",
"LabelMore": "Lisää",
"LabelMoreInfo": "Lisätietoja",
"LabelName": "Nimi",
"LabelNarrator": "Lukija",
"LabelNarrators": "Lukijat",
+ "LabelNew": "Uusi",
+ "LabelNewPassword": "Uusi salasana",
"LabelNewestAuthors": "Uusimmat kirjailijat",
"LabelNewestEpisodes": "Uusimmat jaksot",
+ "LabelNotStarted": "Ei aloitettu",
"LabelPassword": "Salasana",
"LabelPath": "Polku",
+ "LabelPermanent": "Pysyvä",
+ "LabelPermissionsAccessAllLibraries": "Käyttöoikeudet kaikkiin kirjastoihin",
+ "LabelPermissionsDelete": "Voi poistaa",
+ "LabelPermissionsDownload": "Voi ladata",
+ "LabelPermissionsUpdate": "Voi päivittää",
+ "LabelPermissionsUpload": "Voi lähettää",
+ "LabelPlaylists": "Soittolistat",
+ "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastit",
+ "LabelPort": "Portti",
"LabelPublishYear": "Julkaisuvuosi",
+ "LabelPublisher": "Julkaisija",
+ "LabelPublishers": "Julkaisijat",
"LabelRSSFeedPreventIndexing": "Estä indeksointi",
+ "LabelRandomly": "Satunnaisesti",
"LabelRead": "Lue",
"LabelReadAgain": "Lue uudelleen",
"LabelRecentSeries": "Viimeisimmät sarjat",
"LabelRecentlyAdded": "Viimeeksi lisätyt",
+ "LabelRecommended": "Suositeltu",
+ "LabelRegion": "Alue",
+ "LabelRemoveCover": "Poista kansikuva",
"LabelSeason": "Kausi",
+ "LabelSelectAll": "Valitse kaikki",
+ "LabelSelectUsers": "Valitse käyttäjät",
+ "LabelSeries": "Sarja",
+ "LabelSeriesName": "Sarjan nimi",
"LabelSetEbookAsPrimary": "Aseta ensisijaiseksi",
"LabelSetEbookAsSupplementary": "Aseta täydentäväksi",
+ "LabelSettingsAudiobooksOnly": "Vain äänikirjat",
+ "LabelSettingsChromecastSupport": "Chromecast-tuki",
+ "LabelSettingsExperimentalFeatures": "Kokeelliset ominaisuudet",
+ "LabelSettingsFindCovers": "Etsi kansikuvia",
+ "LabelShare": "Jaa",
"LabelShowAll": "Näytä kaikki",
+ "LabelShowSeconds": "Näytä sekunnit",
"LabelSize": "Koko",
"LabelSleepTimer": "Uniajastin",
+ "LabelStart": "Aloita",
+ "LabelStartTime": "Aloitusaika",
+ "LabelStatsAudioTracks": "Ääniraidat",
+ "LabelStatsBestDay": "Paras päivä",
"LabelStatsDailyAverage": "Päivittäinen keskiarvo",
+ "LabelStatsDays": "Päivää",
+ "LabelStatsDaysListened": "Päivää kuunneltu",
+ "LabelStatsHours": "Tunnit",
"LabelStatsInARow": "peräjälkeen",
"LabelStatsMinutes": "minuuttia",
+ "LabelStatsMinutesListening": "Minuuttia kuunneltu",
+ "LabelStatsWeekListening": "Viikon aikana kuunneltu",
+ "LabelTag": "Tägi",
+ "LabelTags": "Tägit",
"LabelTheme": "Teema",
"LabelThemeDark": "Tumma",
"LabelThemeLight": "Kirkas",
"LabelTimeRemaining": "{0} jäljellä",
+ "LabelTitle": "Nimi",
+ "LabelTotalDuration": "Kokonaiskesto",
+ "LabelTracks": "Raidat",
"LabelType": "Tyyppi",
+ "LabelUnknown": "Tuntematon",
+ "LabelUpdateCover": "Päivitä kansikuva",
"LabelUser": "Käyttäjä",
"LabelUsername": "Käyttäjätunnus",
+ "LabelValue": "Arvo",
+ "LabelVersion": "Versio",
"LabelYourBookmarks": "Kirjanmerkkisi",
"LabelYourProgress": "Edistymisesi",
"MessageDownloadingEpisode": "Ladataan jaksoa",
"MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa",
+ "MessageFeedURLWillBe": "Syötteen URL tulee olemaan {0}",
"MessageFetching": "Haetaan...",
"MessageLoading": "Ladataan...",
"MessageMarkAsFinished": "Merkitse valmiiksi",
"MessageNoBookmarks": "Ei kirjanmerkkejä",
+ "MessageNoChapters": "Ei kappaleita",
+ "MessageNoCoversFound": "Kansikuvia ei löydetty",
+ "MessageNoGenres": "Ei lajityyppejä",
"MessageNoItems": "Ei kohteita",
"MessageNoItemsFound": "Kohteita ei löytynyt",
+ "MessageNoListeningSessions": "Ei kuunteluistuntoja",
"MessageNoPodcastsFound": "Podcasteja ei löytynyt",
+ "MessageNoUpdatesWereNecessary": "Päivityksiä ei tarvittu",
"MessageNoUserPlaylists": "Sinulla ei ole soittolistoja",
+ "MessageOr": "tai",
"MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu",
+ "MessageTaskFailed": "Epäonnistunut",
+ "StatsSessions": "istunnot",
+ "ToastAccountUpdateSuccess": "Tili päivitetty",
"ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui",
- "ToastBookmarkUpdateFailed": "Kirjanmerkin päivittäminen epäonnistui",
+ "ToastCoverUpdateFailed": "Kansikuvan päivitys epäonnistui",
+ "ToastItemCoverUpdateSuccess": "Kohteen kansikuva päivitetty",
"ToastItemMarkedAsFinishedFailed": "Valmiiksi merkitseminen epäonnistui",
+ "ToastItemMarkedAsNotFinishedFailed": "Valmiiksi merkitsemisen poisto epäonnistui",
+ "ToastItemUpdateSuccess": "Kohde päivitetty",
+ "ToastLibraryCreateFailed": "Kirjaston luominen epäonnistui",
+ "ToastLibraryCreateSuccess": "Kirjasto \"{0}\" luotu",
+ "ToastLibraryDeleteFailed": "Kirjaston poistaminen epäonnistui",
+ "ToastLibraryDeleteSuccess": "Kirjasto poistettu",
+ "ToastLibraryUpdateSuccess": "Kirjasto \"{0}\" päivitetty",
+ "ToastNewUserCreatedFailed": "Tilin \"{0}\" luominen epäonnistui",
+ "ToastNewUserCreatedSuccess": "Uusi tili luotu",
"ToastPlaylistCreateFailed": "Soittolistan luominen epäonnistui",
+ "ToastPlaylistCreateSuccess": "Soittolista luotu",
+ "ToastPlaylistRemoveSuccess": "Soittolista poistettu",
+ "ToastPlaylistUpdateSuccess": "Soittolista päivitetty",
"ToastPodcastCreateFailed": "Podcastin luominen epäonnistui",
- "ToastPodcastCreateSuccess": "Podcastin luominen onnistui"
+ "ToastPodcastCreateSuccess": "Podcastin luominen onnistui",
+ "ToastRSSFeedCloseFailed": "RSS syötteen sulkeminen epäonnistui",
+ "ToastRSSFeedCloseSuccess": "RSS syöte suljettu",
+ "ToastRemoveFailed": "Poistaminen epäonnistui",
+ "ToastRemoveItemFromCollectionFailed": "Kohteen poistaminen kokoelmasta epäonnistui",
+ "ToastRemoveItemFromCollectionSuccess": "Kohde poistettu kokoelmasta",
+ "ToastRenameFailed": "Uudelleennimeäminen epäonnistui",
+ "ToastSelectAtLeastOneUser": "Valitse ainakin yksi käyttäjä",
+ "ToastServerSettingsUpdateSuccess": "Palvelimen asetukset päivitetty",
+ "ToastSessionCloseFailed": "Istunnon sulkeminen epäonnistui",
+ "ToastSessionDeleteFailed": "Istunnon poistaminen epäonnistui",
+ "ToastSessionDeleteSuccess": "Istunto poistettu",
+ "ToastSocketConnected": "Yhteys saatu",
+ "ToastSocketDisconnected": "Yhteys katkaistu",
+ "ToastSocketFailedToConnect": "Yhteyden muodostus epäonnistui",
+ "ToastTitleRequired": "Otsikko on pakollinen",
+ "ToastUnknownError": "Tuntematon virhe",
+ "ToastUserDeleteFailed": "Käyttäjän poisto epäonnistui",
+ "ToastUserDeleteSuccess": "Käyttäjä poistettu",
+ "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"
}
diff --git a/client/strings/fr.json b/client/strings/fr.json
index a1b5a58e5d..a1f5c2c86a 100644
--- a/client/strings/fr.json
+++ b/client/strings/fr.json
@@ -9,7 +9,7 @@
"ButtonApply": "Appliquer",
"ButtonApplyChapters": "Appliquer aux chapitres",
"ButtonAuthors": "Auteurs",
- "ButtonBack": "Reculer",
+ "ButtonBack": "Retour",
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
"ButtonCancel": "Annuler",
"ButtonCancelEncode": "Annuler l’encodage",
@@ -56,6 +56,7 @@
"ButtonOpenManager": "Ouvrir le gestionnaire",
"ButtonPause": "Pause",
"ButtonPlay": "Lire",
+ "ButtonPlayAll": "Lire tout",
"ButtonPlaying": "En lecture",
"ButtonPlaylists": "Listes de lecture",
"ButtonPrevious": "Précédent",
@@ -65,6 +66,7 @@
"ButtonPurgeItemsCache": "Purger le cache des éléments",
"ButtonQueueAddItem": "Ajouter à la liste de lecture",
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
+ "ButtonQuickEmbed": "Intégration rapide",
"ButtonQuickEmbedMetadata": "Ajouter rapidement des métadonnées",
"ButtonQuickMatch": "Recherche rapide",
"ButtonReScan": "Nouvelle analyse",
@@ -120,7 +122,7 @@
"HeaderBackups": "Sauvegardes",
"HeaderChangePassword": "Modifier le mot de passe",
"HeaderChapters": "Chapitres",
- "HeaderChooseAFolder": "Choisir un dossier",
+ "HeaderChooseAFolder": "Sélectionner un dossier",
"HeaderCollection": "Collection",
"HeaderCollectionItems": "Entrées de la collection",
"HeaderCover": "Couverture",
@@ -161,6 +163,7 @@
"HeaderNotificationUpdate": "Mise à jour de la notification",
"HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "Authentification via OpenID Connect",
+ "HeaderOpenListeningSessions": "Ouvrir les sessions d'écoutes",
"HeaderOpenRSSFeed": "Ouvrir le flux RSS",
"HeaderOtherFiles": "Autres fichiers",
"HeaderPasswordAuthentication": "Authentification par mot de passe",
@@ -178,6 +181,7 @@
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
"HeaderSchedule": "Programmation",
+ "HeaderScheduleEpisodeDownloads": "Programmer des téléchargements automatiques d'épisodes",
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
"HeaderSession": "Session",
"HeaderSetBackupSchedule": "Activer la sauvegarde automatique",
@@ -212,18 +216,22 @@
"LabelAccountTypeUser": "Utilisateur",
"LabelActivity": "Activité",
"LabelAddToCollection": "Ajouter à la collection",
- "LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
+ "LabelAddToCollectionBatch": "Ajout de {0} livres à la collection",
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
"LabelAddedAt": "Date d’ajout",
- "LabelAddedDate": "{0} ajoutés",
+ "LabelAddedDate": "Ajouté le {0}",
"LabelAdminUsersOnly": "Administrateurs uniquement",
"LabelAll": "Tout",
"LabelAllUsers": "Tous les utilisateurs",
"LabelAllUsersExcludingGuests": "Tous les utilisateurs à l’exception des invités",
"LabelAllUsersIncludingGuests": "Tous les utilisateurs, y compris les invités",
"LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque",
+ "LabelApiToken": "Token API",
"LabelAppend": "Ajouter",
+ "LabelAudioBitrate": "Débit audio (par exemple 128k)",
+ "LabelAudioChannels": "Canaux audio (1 ou 2)",
+ "LabelAudioCodec": "Codec audio",
"LabelAuthor": "Auteur",
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
@@ -236,6 +244,7 @@
"LabelAutoRegister": "Enregistrement automatique",
"LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion",
"LabelBackToUser": "Retour à l’utilisateur",
+ "LabelBackupAudioFiles": "Sauvegarder les fichiers audio",
"LabelBackupLocation": "Emplacement de la sauvegarde",
"LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques",
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes enregistrées dans /metadata/backups",
@@ -244,15 +253,18 @@
"LabelBackupsNumberToKeep": "Nombre de sauvegardes à conserver",
"LabelBackupsNumberToKeepHelp": "Seule une sauvegarde sera supprimée à la fois. Si vous avez déjà plus de sauvegardes à effacer, vous devez les supprimer manuellement.",
"LabelBitrate": "Débit binaire",
+ "LabelBonus": "Bonus",
"LabelBooks": "Livres",
"LabelButtonText": "Texte du bouton",
"LabelByAuthor": "par {0}",
"LabelChangePassword": "Modifier le mot de passe",
"LabelChannels": "Canaux",
+ "LabelChapterCount": "{0} Chapitres",
"LabelChapterTitle": "Titre du chapitre",
"LabelChapters": "Chapitres",
"LabelChaptersFound": "chapitres trouvés",
"LabelClickForMoreInfo": "Cliquez ici pour plus d’informations",
+ "LabelClickToUseCurrentValue": "Cliquez pour utiliser la valeur actuelle",
"LabelClosePlayer": "Fermer le lecteur",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Réduire les séries",
@@ -298,16 +310,29 @@
"LabelEmailSettingsRejectUnauthorized": "Rejeter les certificats non autorisés",
"LabelEmailSettingsRejectUnauthorizedHelp": "Désactiver la validation du certificat SSL peut exposer votre connexion à des risques de sécurité, tels que des attaques de type « Attaque de l’homme du milieu ». Ne désactivez cette option que si vous en comprenez les implications et si vous faites confiance au serveur de messagerie auquel vous vous connectez.",
"LabelEmailSettingsSecure": "Sécurisé",
- "LabelEmailSettingsSecureHelp": "Si cette option est activée, la connexion utilisera TLS lors de la connexion au serveur. Si elle est désactivée, TLS sera utilisé uniquement si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, définissez cette valeur sur « true » si vous vous connectez au port 465. Pour les ports 587 ou 25, laissez-la sur « false ». (source : nodemailer.com/smtp/#authentication)",
+ "LabelEmailSettingsSecureHelp": "Si cette option est activée, la connexion utilisera TLS lors de la connexion au serveur. Si elle est désactivée, TLS sera utilisé uniquement si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, définissez cette valeur sur « true » si vous vous connectez au port 465. Pour les ports 587 ou 25, laissez-la sur « false ». (source : nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Adresse de test",
"LabelEmbeddedCover": "Couverture du livre intégrée",
"LabelEnable": "Activer",
+ "LabelEncodingBackupLocation": "Une sauvegarde de vos fichiers audio originaux sera stockée dans :",
+ "LabelEncodingChaptersNotEmbedded": "Les chapitres ne sont pas intégrés dans les livres audio multipistes.",
+ "LabelEncodingClearItemCache": "Assurez-vous de purger périodiquement le cache des éléments.",
+ "LabelEncodingFinishedM4B": "Le fichier M4B terminé sera placé dans votre dossier de livre audio à l'adresse suivante :",
+ "LabelEncodingInfoEmbedded": "Les métadonnées seront intégrées dans les pistes audio de votre dossier de livre audio.",
+ "LabelEncodingStartedNavigation": "Une fois la tâche démarrée, vous pouvez quitter cette page.",
+ "LabelEncodingTimeWarning": "L’encodage peut prendre jusqu’à 30 minutes.",
+ "LabelEncodingWarningAdvancedSettings": "Avertissement : ne mettez pas à jour ces paramètres à moins que vous ne soyez familier avec les options d'encodage « ffmpeg ».",
+ "LabelEncodingWatcherDisabled": "Si l'observateur est désactivé, vous devrez ensuite réanalyser ce livre audio.",
"LabelEnd": "Fin",
"LabelEndOfChapter": "Fin du chapitre",
"LabelEpisode": "Épisode",
+ "LabelEpisodeNotLinkedToRssFeed": "Épisode non lié au flux RSS",
+ "LabelEpisodeNumber": "Épisode n°{0}",
"LabelEpisodeTitle": "Titre de l’épisode",
"LabelEpisodeType": "Type de l’épisode",
+ "LabelEpisodeUrlFromRssFeed": "URL de l’épisode à partir du flux RSS",
"LabelEpisodes": "Épisodes",
+ "LabelEpisodic": "Épisodique",
"LabelExample": "Exemple",
"LabelExpandSeries": "Développer la série",
"LabelExpandSubSeries": "Développer les sous-séries",
@@ -319,7 +344,7 @@
"LabelFetchingMetadata": "Récupération des métadonnées",
"LabelFile": "Fichier",
"LabelFileBirthtime": "Création du fichier",
- "LabelFileBornDate": "Créé {0}",
+ "LabelFileBornDate": "Créé le {0}",
"LabelFileModified": "Modification du fichier",
"LabelFileModifiedDate": "Modifié le {0}",
"LabelFilename": "Nom de fichier",
@@ -335,6 +360,7 @@
"LabelFontScale": "Taille de la police de caractère",
"LabelFontStrikethrough": "Barrer",
"LabelFormat": "Format",
+ "LabelFull": "Complet",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Suppression du fichier",
@@ -390,6 +416,10 @@
"LabelLowestPriority": "Priorité la plus basse",
"LabelMatchExistingUsersBy": "Correspondance avec les utilisateurs existants",
"LabelMatchExistingUsersByDescription": "Utilisé pour connecter les utilisateurs existants. Une fois connectés, les utilisateurs seront associés à un identifiant unique provenant de votre fournisseur SSO",
+ "LabelMaxEpisodesToDownload": "Nombre maximum d’épisodes à télécharger. 0 pour illimité.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Nombre maximum de nouveaux épisodes à télécharger par vérification",
+ "LabelMaxEpisodesToKeep": "Nombre maximum d’épisodes à conserver",
+ "LabelMaxEpisodesToKeepHelp": "La valeur 0 ne définit aucune limite maximale. Une fois qu’un nouvel épisode est téléchargé automatiquement, l’épisode le plus ancien sera supprimé si vous avez plus de X épisodes. Cela ne supprimera qu’un seul épisode par nouveau téléchargement.",
"LabelMediaPlayer": "Lecteur multimédia",
"LabelMediaType": "Type de média",
"LabelMetaTag": "Balise de métadonnée",
@@ -435,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "Nom de la demande OpenID qui contient une liste des groupes de l’utilisateur. Communément appelé groups
. Si elle est configurée , l’application attribuera automatiquement des rôles en fonction de l’appartenance de l’utilisateur à un groupe, à condition que ces groupes soient nommés -sensible à la casse- tel que « admin », « user » ou « guest » dans la demande. Elle doit contenir une liste, et si un utilisateur appartient à plusieurs groupes, l’application attribuera le rôle correspondant au niveau d’accès le plus élevé. Si aucun groupe ne correspond, l’accès sera refusé.",
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
"LabelOverwrite": "Écraser",
+ "LabelPaginationPageXOfY": "Page {0} sur {1}",
"LabelPassword": "Mot de passe",
"LabelPath": "Chemin",
"LabelPermanent": "Permanent",
"LabelPermissionsAccessAllLibraries": "Peut accéder à toutes les bibliothèque",
"LabelPermissionsAccessAllTags": "Peut accéder à toutes les étiquettes",
"LabelPermissionsAccessExplicitContent": "Peut accéder au contenu restreint",
+ "LabelPermissionsCreateEreader": "Peut créer une liseuse",
"LabelPermissionsDelete": "Peut supprimer",
"LabelPermissionsDownload": "Peut télécharger",
"LabelPermissionsUpdate": "Peut mettre à jour",
@@ -463,7 +495,9 @@
"LabelProviderAuthorizationValue": "Valeur de l’en-tête d’autorisation",
"LabelPubDate": "Date de publication",
"LabelPublishYear": "Année de publication",
- "LabelPublishedDate": "{0} publiés",
+ "LabelPublishedDate": "Publié en {0}",
+ "LabelPublishedDecade": "Décennie de publication",
+ "LabelPublishedDecades": "Décennies de publication",
"LabelPublisher": "Éditeur",
"LabelPublishers": "Éditeurs",
"LabelRSSFeedCustomOwnerEmail": "Courriel personnalisée du propriétaire",
@@ -483,21 +517,28 @@
"LabelRedo": "Refaire",
"LabelRegion": "Région",
"LabelReleaseDate": "Date de parution",
+ "LabelRemoveAllMetadataAbs": "Supprimer tous les fichiers metadata.abs",
+ "LabelRemoveAllMetadataJson": "Supprimer tous les fichiers metadata.json",
"LabelRemoveCover": "Supprimer la couverture",
+ "LabelRemoveMetadataFile": "Supprimer les fichiers de métadonnées dans les dossiers des éléments de la bibliothèque",
+ "LabelRemoveMetadataFileHelp": "Supprimer tous les fichiers metadata.json et metadata.abs de vos dossiers {0}.",
"LabelRowsPerPage": "Lignes par page",
"LabelSearchTerm": "Terme de recherche",
"LabelSearchTitle": "Titre de recherche",
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
"LabelSeason": "Saison",
+ "LabelSeasonNumber": "Saison n°{0}",
"LabelSelectAll": "Tout sélectionner",
"LabelSelectAllEpisodes": "Sélectionner tous les épisodes",
- "LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
+ "LabelSelectEpisodesShowing": "Sélectionner {0} épisode(s) en cours",
"LabelSelectUsers": "Sélectionner les utilisateurs",
"LabelSendEbookToDevice": "Envoyer le livre numérique à…",
"LabelSequence": "Séquence",
+ "LabelSerial": "N° de série",
"LabelSeries": "Séries",
"LabelSeriesName": "Nom de la série",
"LabelSeriesProgress": "Progression de séries",
+ "LabelServerLogLevel": "Niveau de journalisation du serveur",
"LabelServerYearReview": "Bilan de l’année du serveur ({0})",
"LabelSetEbookAsPrimary": "Définir comme principale",
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
@@ -522,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.",
"LabelSettingsHomePageBookshelfView": "Utiliser la vue étagère sur la page d’accueil",
"LabelSettingsLibraryBookshelfView": "Utiliser la vue étagère pour la bibliothèque",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Le pourcentage d'achèvement est supérieur à",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Le temps restant est inférieur à (secondes)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Marquer l’élément multimédia comme terminé lorsque",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Sauter les livres précédents dans « Continuer la série »",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "L’étagère de la page d’accueil « Continuer la série » affiche le premier livre non commencé dans les séries dont au moins un livre est terminé et aucun livre n’est en cours. L’activation de ce paramètre permet de poursuivre la série à partir du dernier livre terminé au lieu du premier livre non commencé.",
"LabelSettingsParseSubtitles": "Analyser les sous-titres",
@@ -586,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} minutes",
"LabelTimeDurationXSeconds": "{0} secondes",
"LabelTimeInMinutes": "Temps en minutes",
+ "LabelTimeLeft": "{0} restant",
"LabelTimeListened": "Temps d’écoute",
"LabelTimeListenedToday": "Nombres d’écoutes aujourd’hui",
"LabelTimeRemaining": "{0} restantes",
@@ -593,6 +638,7 @@
"LabelTitle": "Titre",
"LabelToolsEmbedMetadata": "Métadonnées intégrées",
"LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.",
+ "LabelToolsM4bEncoder": "Encodeur M4B",
"LabelToolsMakeM4b": "Créer un fichier livre audio M4B",
"LabelToolsMakeM4bDescription": "Générer un fichier de livre audio .M4B avec des métadonnées intégrées, une image de couverture et des chapitres.",
"LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3",
@@ -605,6 +651,7 @@
"LabelTracksMultiTrack": "Piste multiple",
"LabelTracksNone": "Aucune piste",
"LabelTracksSingleTrack": "Piste simple",
+ "LabelTrailer": "Bande-annonce",
"LabelType": "Type",
"LabelUnabridged": "Version intégrale",
"LabelUndo": "Annuler",
@@ -618,8 +665,10 @@
"LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers",
"LabelUploaderDropFiles": "Déposer des fichiers",
"LabelUploaderItemFetchMetadataHelp": "Récupérer automatiquement le titre, l’auteur et la série",
+ "LabelUseAdvancedOptions": "Utiliser les options avancées",
"LabelUseChapterTrack": "Utiliser la piste du chapitre",
"LabelUseFullTrack": "Utiliser la piste complète",
+ "LabelUseZeroForUnlimited": "0 pour illimité",
"LabelUser": "Utilisateur",
"LabelUsername": "Nom d’utilisateur",
"LabelValue": "Valeur",
@@ -666,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "Êtes-vous sûr·e de vouloir supprimer le fournisseur de métadonnées personnalisées « {0} » ?",
"MessageConfirmDeleteNotification": "Êtes-vous sûr·e de vouloir supprimer cette notification ?",
"MessageConfirmDeleteSession": "Êtes-vous sûr·e de vouloir supprimer cette session ?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "Êtes-vous sûr·e de vouloir intégrer des métadonnées dans {0} fichiers audio ?",
"MessageConfirmForceReScan": "Êtes-vous sûr·e de vouloir lancer une analyse forcée ?",
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr·e de marquer tous les épisodes comme terminés ?",
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr·e de vouloir marquer tous les épisodes comme non terminés ?",
@@ -676,7 +726,8 @@
"MessageConfirmNotificationTestTrigger": "Déclencher cette notification avec des données de test ?",
"MessageConfirmPurgeCache": "La purge du cache supprimera l’intégralité du répertoire à /metadata/cache
. Êtes-vous sûr·e de vouloir supprimer le répertoire de cache ?",
"MessageConfirmPurgeItemsCache": "Purger le cache des éléments supprimera l'ensemble du répertoire /metadata/cache/items
. Êtes-vous sûr ?",
- "MessageConfirmQuickEmbed": "Attention ! L'intégration rapide ne permet pas de sauvegarder vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio. Souhaitez-vous continuer ?",
+ "MessageConfirmQuickEmbed": "Attention ! L'intégration rapide ne permet pas de sauvegarder vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio. Êtes-vous sûr·e de vouloir continuer ?",
+ "MessageConfirmQuickMatchEpisodes": "Les épisodes correspondants seront écrasés si une correspondance est trouvée. Seuls les épisodes non correspondants seront mis à jour. Êtes-vous sûr·e ?",
"MessageConfirmReScanLibraryItems": "Êtes-vous sûr·e de vouloir réanalyser {0} éléments ?",
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr·e de vouloir supprimer tous les chapitres ?",
"MessageConfirmRemoveAuthor": "Êtes-vous sûr·e de vouloir supprimer l’auteur « {0} » ?",
@@ -684,6 +735,7 @@
"MessageConfirmRemoveEpisode": "Êtes-vous sûr·e de vouloir supprimer l’épisode « {0} » ?",
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr·e de vouloir supprimer {0} épisodes ?",
"MessageConfirmRemoveListeningSessions": "Êtes-vous sûr·e de vouloir supprimer {0} sessions d’écoute ?",
+ "MessageConfirmRemoveMetadataFiles": "Êtes-vous sûr·e de vouloir supprimer tous les fichiers « metatadata.{0} » des dossiers d’éléments de votre bibliothèque ?",
"MessageConfirmRemoveNarrator": "Êtes-vous sûr·e de vouloir supprimer le narrateur « {0} » ?",
"MessageConfirmRemovePlaylist": "Êtes-vous sûr·e de vouloir supprimer la liste de lecture « {0} » ?",
"MessageConfirmRenameGenre": "Êtes-vous sûr·e de vouloir renommer le genre « {0} » en « {1} » pour tous les éléments ?",
@@ -699,6 +751,7 @@
"MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes",
"MessageEmbedFailed": "Échec de l’intégration !",
"MessageEmbedFinished": "Intégration terminée !",
+ "MessageEmbedQueue": "En file d'attente pour l'intégration des métadonnées ({0} dans la file d'attente)",
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
"MessageEreaderDevices": "Pour garantir l’envoi des livres électroniques, vous devrez peut-être ajouter le courriel ci-dessus comme expéditeur valide pour chaque appareil répertorié ci-dessous.",
"MessageFeedURLWillBe": "L’URL du flux sera {0}",
@@ -743,6 +796,7 @@
"MessageNoLogs": "Aucun journaux",
"MessageNoMediaProgress": "Aucun média en cours",
"MessageNoNotifications": "Aucune notification",
+ "MessageNoPodcastFeed": "Podcast invalide : pas de flux",
"MessageNoPodcastsFound": "Aucun podcast trouvé",
"MessageNoResults": "Aucun résultat",
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
@@ -759,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
"MessagePleaseWait": "Merci de patienter…",
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n’a pas d’URL de flux RSS à utiliser pour la correspondance",
+ "MessagePodcastSearchField": "Saisissez le terme de recherche ou l'URL du flux RSS",
+ "MessageQuickEmbedInProgress": "Intégration rapide en cours",
+ "MessageQuickEmbedQueue": "En file d'attente pour une intégration rapide ({0} dans la file d'attente)",
+ "MessageQuickMatchAllEpisodes": "Associer rapidement tous les épisodes",
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de « {0} ». N’écrase pas les données présentes à moins que le paramètre « Préférer les Métadonnées par correspondance » soit activé.",
"MessageRemoveChapter": "Supprimer le chapitre",
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
@@ -776,6 +834,41 @@
"MessageShareExpiresIn": "Expire dans {0}",
"MessageShareURLWillBe": "L’adresse de partage sera {0} ",
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
+ "MessageTaskAudioFileNotWritable": "Le fichier audio « {0} » n’est pas accessible en écriture",
+ "MessageTaskCanceledByUser": "Tâche annulée par l’utilisateur",
+ "MessageTaskDownloadingEpisodeDescription": "Téléchargement de l'épisode « {0} »",
+ "MessageTaskEmbeddingMetadata": "Intégration de métadonnées",
+ "MessageTaskEmbeddingMetadataDescription": "Intégration de métadonnées dans le livre audio « {0} »",
+ "MessageTaskEncodingM4b": "Encodage M4B",
+ "MessageTaskEncodingM4bDescription": "Encodage du livre audio « {0} » dans un seul fichier M4B",
+ "MessageTaskFailed": "Échec",
+ "MessageTaskFailedToBackupAudioFile": "Échec de la sauvegarde du fichier audio « {0} »",
+ "MessageTaskFailedToCreateCacheDirectory": "Échec de la création du répertoire de cache",
+ "MessageTaskFailedToEmbedMetadataInFile": "Échec de l'intégration des métadonnées dans le fichier « {0} »",
+ "MessageTaskFailedToMergeAudioFiles": "Échec de la fusion des fichiers audio",
+ "MessageTaskFailedToMoveM4bFile": "Échec du déplacement du fichier M4B",
+ "MessageTaskFailedToWriteMetadataFile": "Échec de l’écriture du fichier de métadonnées",
+ "MessageTaskMatchingBooksInLibrary": "Livres correspondants dans la bibliothèque « {0} »",
+ "MessageTaskNoFilesToScan": "Aucun fichier à analyser",
+ "MessageTaskOpmlImport": "Importation OPML",
+ "MessageTaskOpmlImportDescription": "Création de podcasts à partir de {0} flux RSS",
+ "MessageTaskOpmlImportFeed": "Flux d’importation OPML",
+ "MessageTaskOpmlImportFeedDescription": "Importation du flux RSS « {0} »",
+ "MessageTaskOpmlImportFeedFailed": "Échec de l’obtention du flux de podcast",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Création du podcast « {0} »",
+ "MessageTaskOpmlImportFeedPodcastExists": "Le podcast existe déjà à cet emplacement",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Échec de la création du podcast",
+ "MessageTaskOpmlImportFinished": "Ajout de {0} podcasts",
+ "MessageTaskOpmlParseFailed": "Échec de l'analyse du fichier OPML",
+ "MessageTaskOpmlParseFastFail": "Balise de fichier OPML non valide introuvable OU une balise n’a pas été trouvée",
+ "MessageTaskOpmlParseNoneFound": "Aucun flux trouvé dans le fichier OPML",
+ "MessageTaskScanItemsAdded": "{0} ajouté",
+ "MessageTaskScanItemsMissing": "{0} manquant",
+ "MessageTaskScanItemsUpdated": "{0} mis à jour",
+ "MessageTaskScanNoChangesNeeded": "Aucun changement nécessaire",
+ "MessageTaskScanningFileChanges": "Analyse des modifications du fichier dans « {0} »",
+ "MessageTaskScanningLibrary": "Analyse de la bibliothèque « {0} »",
+ "MessageTaskTargetDirectoryNotWritable": "Le répertoire cible n’est pas accessible en écriture",
"MessageThinking": "Je cherche…",
"MessageUploaderItemFailed": "Échec du téléversement",
"MessageUploaderItemSuccess": "Téléversement effectué !",
@@ -793,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
"NoteUploaderOnlyAudioFiles": "Si vous téléversez uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d’élément sont ignorés.",
+ "NotificationOnBackupCompletedDescription": "Déclenché lorsqu’une sauvegarde est terminée",
+ "NotificationOnBackupFailedDescription": "Déclenché lorsqu'une sauvegarde échoue",
+ "NotificationOnEpisodeDownloadedDescription": "Déclenché lorsqu’un épisode de podcast est téléchargé automatiquement",
+ "NotificationOnTestDescription": "Événement pour tester le système de notification",
"PlaceholderNewCollection": "Nom de la nouvelle collection",
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
@@ -816,14 +913,13 @@
"StatsTopNarrators": "TOP NARRATEURS",
"StatsTotalDuration": "Pour une durée totale de…",
"StatsYearInReview": "BILAN DE L’ANNÉE",
- "ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
"ToastAccountUpdateSuccess": "Compte mis à jour",
"ToastAppriseUrlRequired": "Vous devez entrer une URL Apprise",
+ "ToastAsinRequired": "ASIN requis",
"ToastAuthorImageRemoveSuccess": "Image de l’auteur supprimée",
"ToastAuthorNotFound": "Auteur \"{0}\" non trouvé",
"ToastAuthorRemoveSuccess": "Auteur supprimé",
"ToastAuthorSearchNotFound": "Auteur non trouvé",
- "ToastAuthorUpdateFailed": "Échec de la mise à jour de l’auteur",
"ToastAuthorUpdateMerged": "Auteur fusionné",
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (aucune image trouvée)",
@@ -834,29 +930,29 @@
"ToastBackupDeleteSuccess": "Sauvegarde supprimée",
"ToastBackupInvalidMaxKeep": "Nombre de sauvegardes à conserver invalide",
"ToastBackupInvalidMaxSize": "Taille maximale de sauvegarde invalide",
- "ToastBackupPathUpdateFailed": "Échec de la mise à jour du chemin de sauvegarde",
"ToastBackupRestoreFailed": "Échec de la restauration de sauvegarde",
"ToastBackupUploadFailed": "Échec du téléversement de sauvegarde",
"ToastBackupUploadSuccess": "Sauvegarde téléversée",
"ToastBatchDeleteFailed": "Échec de la suppression par lot",
"ToastBatchDeleteSuccess": "Suppression par lot réussie",
+ "ToastBatchQuickMatchFailed": "Échec de la correspondance rapide par lot !",
+ "ToastBatchQuickMatchStarted": "La correspondance rapide par lots de {0} livres a commencé !",
"ToastBatchUpdateFailed": "Échec de la mise à jour par lot",
"ToastBatchUpdateSuccess": "Mise à jour par lot terminée",
"ToastBookmarkCreateFailed": "Échec de la création de signet",
"ToastBookmarkCreateSuccess": "Signet ajouté",
"ToastBookmarkRemoveSuccess": "Signet supprimé",
- "ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet",
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
"ToastCachePurgeFailed": "Échec de la purge du cache",
"ToastCachePurgeSuccess": "Cache purgé avec succès",
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
"ToastChaptersRemoved": "Chapitres supprimés",
+ "ToastChaptersUpdated": "Chapitres mis à jour",
"ToastCollectionItemsAddFailed": "Échec de l’ajout de(s) élément(s) à la collection",
"ToastCollectionItemsAddSuccess": "Ajout de(s) élément(s) à la collection réussi",
"ToastCollectionItemsRemoveSuccess": "Élément(s) supprimé(s) de la collection",
"ToastCollectionRemoveSuccess": "Collection supprimée",
- "ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
"ToastCollectionUpdateSuccess": "Collection mise à jour",
"ToastCoverUpdateFailed": "Échec de la mise à jour de la couverture",
"ToastDeleteFileFailed": "Échec de la suppression du fichier",
@@ -865,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "Un appareil de lecture avec ce nom existe déjà",
"ToastDeviceTestEmailFailed": "Échec de l’envoi du courriel de test",
"ToastDeviceTestEmailSuccess": "Courriel de test envoyé",
- "ToastDeviceUpdateFailed": "Échec de la mise à jour",
- "ToastEmailSettingsUpdateFailed": "Échec de la mise à jour des paramètres de messagerie",
"ToastEmailSettingsUpdateSuccess": "Paramètres de messagerie mis à jour",
"ToastEncodeCancelFailed": "Échec de l’annulation de l’encodage",
"ToastEncodeCancelSucces": "Encodage annulé",
"ToastEpisodeDownloadQueueClearFailed": "Échec de la suppression de la file d'attente",
"ToastEpisodeDownloadQueueClearSuccess": "File d’attente de téléchargement des épisodes effacée",
+ "ToastEpisodeUpdateSuccess": "{0} épisodes mis à jour",
"ToastErrorCannotShare": "Impossible de partager nativement sur cet appareil",
"ToastFailedToLoadData": "Échec du chargement des données",
+ "ToastFailedToMatch": "Échec de la correspondance",
"ToastFailedToShare": "Échec du partage",
- "ToastFailedToUpdateAccount": "Échec de la mise à jour du compte",
- "ToastFailedToUpdateUser": "La mise a jour de l'utilisateur à échouée",
+ "ToastFailedToUpdate": "Échec de la mise à jour",
"ToastInvalidImageUrl": "URL de l'image invalide",
+ "ToastInvalidMaxEpisodesToDownload": "Nombre maximum d’épisodes à télécharger non valide",
"ToastInvalidUrl": "URL invalide",
- "ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’élément",
"ToastItemCoverUpdateSuccess": "Couverture mise à jour",
"ToastItemDeletedFailed": "La suppression de l'élément à échouée",
"ToastItemDeletedSuccess": "Élément supprimé",
- "ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’élément",
"ToastItemDetailsUpdateSuccess": "Détails de l’élément mis à jour",
"ToastItemMarkedAsFinishedFailed": "Échec de l’annotation terminée",
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
"ToastItemMarkedAsNotFinishedFailed": "Échec de l’annotation non-terminée",
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
- "ToastItemUpdateFailed": "La mise a jour de l’élément à échoué",
"ToastItemUpdateSuccess": "Élément mis a jour",
"ToastLibraryCreateFailed": "Échec de la création de bibliothèque",
"ToastLibraryCreateSuccess": "Bibliothèque « {0} » créée",
@@ -897,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
"ToastLibraryScanFailedToStart": "Échec du démarrage de l’analyse",
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
- "ToastLibraryUpdateFailed": "Échec de la mise à jour de la bibliothèque",
"ToastLibraryUpdateSuccess": "Bibliothèque « {0} » mise à jour",
+ "ToastMatchAllAuthorsFailed": "Tous les auteurs et autrices n’ont pas pu être classés",
+ "ToastMetadataFilesRemovedError": "Erreur lors de la suppression des fichiers « metadata.{0} »",
+ "ToastMetadataFilesRemovedNoneFound": "Aucun fichier « metadata.{0} » trouvé dans la bibliothèque",
+ "ToastMetadataFilesRemovedNoneRemoved": "Aucun fichier « metadata.{0} » n’a été supprimé",
+ "ToastMetadataFilesRemovedSuccess": "{0} fichiers metadata.{1} supprimés",
+ "ToastMustHaveAtLeastOnePath": "Doit avoir au moins un chemin",
"ToastNameEmailRequired": "Le nom et le courriel sont requis",
"ToastNameRequired": "Le nom est requis",
+ "ToastNewEpisodesFound": "{0} nouveaux épisodes trouvés",
"ToastNewUserCreatedFailed": "La création du compte à échouée : « {0} »",
"ToastNewUserCreatedSuccess": "Nouveau compte créé",
"ToastNewUserLibraryError": "Au moins une bibliothèque est requise",
"ToastNewUserPasswordError": "Un mot de passe est requis, seul l’utilisateur root peut avoir un mot de passe vide",
"ToastNewUserTagError": "Au moins une étiquette est requise",
"ToastNewUserUsernameError": "Entrez un nom d’utilisateur",
+ "ToastNoNewEpisodesFound": "Aucun nouvel épisode trouvé",
"ToastNoUpdatesNecessary": "Aucune mise à jour nécessaire",
"ToastNotificationCreateFailed": "La création de la notification à échouée",
"ToastNotificationDeleteFailed": "La suppression de la notification à échouée",
"ToastNotificationFailedMaximum": "Le nombre maximum de tentatives échouées doit être >= 0",
"ToastNotificationQueueMaximum": "Le nombre de notification maximum doit être >= 0",
- "ToastNotificationSettingsUpdateFailed": "La mise a jour des paramètres de notification a échouée",
"ToastNotificationSettingsUpdateSuccess": "Paramètres de notification mis à jour",
"ToastNotificationTestTriggerFailed": "L'envoi de la notification de test à échoué",
"ToastNotificationTestTriggerSuccess": "Notification de test déclenchée",
- "ToastNotificationUpdateFailed": "Échec de la mise à jour de la notification",
"ToastNotificationUpdateSuccess": "Notification mise à jour",
"ToastPlaylistCreateFailed": "Échec de la création de la liste de lecture",
"ToastPlaylistCreateSuccess": "Liste de lecture créée",
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
- "ToastPlaylistUpdateFailed": "Échec de la mise à jour de la liste de lecture",
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
"ToastPodcastCreateFailed": "Échec de la création du podcast",
"ToastPodcastCreateSuccess": "Podcast créé avec succès",
"ToastPodcastGetFeedFailed": "Échec de la récupération du flux du podcast",
"ToastPodcastNoEpisodesInFeed": "Aucun épisode trouvé dans le flux RSS",
"ToastPodcastNoRssFeed": "Le podcast n’a pas de flux RSS",
+ "ToastProgressIsNotBeingSynced": "La progression n’est pas synchronisée, redémarrez la lecture",
"ToastProviderCreatedFailed": "Échec de l’ajout du fournisseur",
"ToastProviderCreatedSuccess": "Nouveau fournisseur ajouté",
"ToastProviderNameAndUrlRequired": "Nom et URL requis",
@@ -950,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à l’appareil : {0}",
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
- "ToastServerSettingsUpdateFailed": "Échec de la mise à jour des paramètres du serveur",
"ToastServerSettingsUpdateSuccess": "Mise à jour des paramètres du serveur",
"ToastSessionCloseFailed": "Échec de la fermeture de la session",
"ToastSessionDeleteFailed": "Échec de la suppression de session",
"ToastSessionDeleteSuccess": "Session supprimée",
+ "ToastSleepTimerDone": "Minuterie de mise en veille terminée… zZzzZz",
"ToastSlugMustChange": "L’identifiant d’URL contient des caractères invalides",
"ToastSlugRequired": "L’identifiant d’URL est requis",
"ToastSocketConnected": "WebSocket connecté",
"ToastSocketDisconnected": "WebSocket déconnecté",
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
"ToastSortingPrefixesEmptyError": "Doit avoir au moins 1 préfixe de tri",
- "ToastSortingPrefixesUpdateFailed": "Échec de la mise à jour des préfixes de tri",
"ToastSortingPrefixesUpdateSuccess": "Mise à jour des préfixes de tri ({0} élément)",
"ToastTitleRequired": "Le titre est requis",
"ToastUnknownError": "Erreur inconnue",
diff --git a/client/strings/he.json b/client/strings/he.json
index 0b584f916f..a93d3f0584 100644
--- a/client/strings/he.json
+++ b/client/strings/he.json
@@ -8,17 +8,18 @@
"ButtonAddYourFirstLibrary": "הוסף את הספרייה הראשונה שלך",
"ButtonApply": "החל",
"ButtonApplyChapters": "החל פרקים",
- "ButtonAuthors": "יוצרים",
+ "ButtonAuthors": "סופרים",
"ButtonBack": "חזור",
"ButtonBrowseForFolder": "עיין בתיקייה",
- "ButtonCancel": "בטל",
+ "ButtonCancel": "ביטול",
"ButtonCancelEncode": "בטל קידוד",
"ButtonChangeRootPassword": "שנה סיסמת root",
"ButtonCheckAndDownloadNewEpisodes": "בדוק והורד פרקים חדשים",
"ButtonChooseAFolder": "בחר תיקייה",
"ButtonChooseFiles": "בחר קבצים",
"ButtonClearFilter": "נקה סינון",
- "ButtonCloseFeed": "סגור פיד",
+ "ButtonCloseFeed": "סגור ערוץ",
+ "ButtonCloseSession": "סגור סשן פתוח",
"ButtonCollections": "אוספים",
"ButtonConfigureScanner": "הגדר סורק",
"ButtonCreate": "צור",
@@ -28,6 +29,7 @@
"ButtonEdit": "ערוך",
"ButtonEditChapters": "ערוך פרקים",
"ButtonEditPodcast": "ערוך פודקאסט",
+ "ButtonEnable": "הפעל",
"ButtonForceReScan": "סרוק מחדש בכוח",
"ButtonFullPath": "נתיב מלא",
"ButtonHide": "הסתר",
@@ -46,19 +48,24 @@
"ButtonNevermind": "לא משנה",
"ButtonNext": "הבא",
"ButtonNextChapter": "פרק הבא",
+ "ButtonNextItemInQueue": "פריט הבא בתור",
"ButtonOk": "אישור",
"ButtonOpenFeed": "פתח פיד",
"ButtonOpenManager": "פתח מנהל",
"ButtonPause": "השהה",
"ButtonPlay": "נגן",
+ "ButtonPlayAll": "נגן הכל",
"ButtonPlaying": "מנגן",
"ButtonPlaylists": "רשימות השמעה",
"ButtonPrevious": "קודם",
"ButtonPreviousChapter": "פרק קודם",
+ "ButtonProbeAudioFile": "בדוק קובץ אודיו",
"ButtonPurgeAllCache": "נקה את כל המטמון",
"ButtonPurgeItemsCache": "נקה את מטמון הפריטים",
"ButtonQueueAddItem": "הוסף לתור",
"ButtonQueueRemoveItem": "הסר מהתור",
+ "ButtonQuickEmbed": "הטמעה מהירה",
+ "ButtonQuickEmbedMetadata": "הטמעת מטא נתונים מהירה",
"ButtonQuickMatch": "התאמה מהירה",
"ButtonReScan": "סרוק מחדש",
"ButtonRead": "קרא",
@@ -88,8 +95,10 @@
"ButtonShow": "הצג",
"ButtonStartM4BEncode": "התחל קידוד M4B",
"ButtonStartMetadataEmbed": "התחל הטמעת מטא-נתונים",
+ "ButtonStats": "סטטיסטיקות",
"ButtonSubmit": "שלח",
"ButtonTest": "בדיקה",
+ "ButtonUnlinkOpenId": "נתק OpenID",
"ButtonUpload": "העלה",
"ButtonUploadBackup": "העלה גיבוי",
"ButtonUploadCover": "העלה כריכה",
@@ -102,6 +111,7 @@
"ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטא-נתונים - נסה לעדכן כותרת ו/או יוצר",
"ErrorUploadLacksTitle": "חובה לתת כותרת",
"HeaderAccount": "חשבון",
+ "HeaderAddCustomMetadataProvider": "הוסף ספק מטא-נתונים מותאם אישית",
"HeaderAdvanced": "מתקדם",
"HeaderAppriseNotificationSettings": "הגדרות התראות של Apprise",
"HeaderAudioTracks": "רצועות קול",
@@ -147,13 +157,17 @@
"HeaderMetadataToEmbed": "מטא-נתונים להטמעה",
"HeaderNewAccount": "חשבון חדש",
"HeaderNewLibrary": "ספרייה חדשה",
+ "HeaderNotificationCreate": "צור התראה",
+ "HeaderNotificationUpdate": "עדכון התראה",
"HeaderNotifications": "התראות",
"HeaderOpenIDConnectAuthentication": "אימות OpenID Connect",
+ "HeaderOpenListeningSessions": "פתח הפעלות האזנה",
"HeaderOpenRSSFeed": "פתח ערוץ RSS",
"HeaderOtherFiles": "קבצים אחרים",
"HeaderPasswordAuthentication": "אימות סיסמה",
"HeaderPermissions": "הרשאות",
"HeaderPlayerQueue": "תור ניגון",
+ "HeaderPlayerSettings": "הגדרות נגן",
"HeaderPlaylist": "רשימת השמעה",
"HeaderPlaylistItems": "פריטי רשימת השמעה",
"HeaderPodcastsToAdd": "פודקאסטים להוספה",
@@ -165,6 +179,7 @@
"HeaderRemoveEpisodes": "הסר {0} פרקים",
"HeaderSavedMediaProgress": "התקדמות מדיה שמורה",
"HeaderSchedule": "תיזמון",
+ "HeaderScheduleEpisodeDownloads": "תזמן הורדת פרקים אוטומטית",
"HeaderScheduleLibraryScans": "קבע סריקות ספרייה אוטומטיות",
"HeaderSession": "הפעלה",
"HeaderSetBackupSchedule": "קבע לוח זמנים לגיבוי",
@@ -190,6 +205,9 @@
"HeaderYearReview": "שנת {0} בסקירה",
"HeaderYourStats": "הסטטיסטיקות שלך",
"LabelAbridged": "מקוצר",
+ "LabelAbridgedChecked": "מקוצר (מסומן)",
+ "LabelAbridgedUnchecked": "בלתי מקוצר (לא מסומן)",
+ "LabelAccessibleBy": "נגיש על ידי",
"LabelAccountType": "סוג חשבון",
"LabelAccountTypeAdmin": "מנהל",
"LabelAccountTypeGuest": "אורח",
@@ -200,13 +218,18 @@
"LabelAddToPlaylist": "הוסף לרשימת השמעה",
"LabelAddToPlaylistBatch": "הוסף {0} פריטים לרשימת השמעה",
"LabelAddedAt": "נוסף בתאריך",
+ "LabelAddedDate": "נוסף ב-{0}",
"LabelAdminUsersOnly": "רק מנהלים",
"LabelAll": "הכל",
"LabelAllUsers": "כל המשתמשים",
"LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים",
"LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים",
"LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך",
+ "LabelApiToken": "טוקן API",
"LabelAppend": "הוסף לסוף",
+ "LabelAudioBitrate": "קצב סיביות (לדוגמא 128k)",
+ "LabelAudioChannels": "ערוצי קול (1 או 2)",
+ "LabelAudioCodec": "קידוד קול",
"LabelAuthor": "יוצר",
"LabelAuthorFirstLast": "יוצר (שם פרטי שם משפחה)",
"LabelAuthorLastFirst": "יוצר (שם משפחה, שם פרטי)",
@@ -701,10 +724,8 @@
"PlaceholderNewPlaylist": "שם רשימת השמעה חדשה",
"PlaceholderSearch": "חיפוש..",
"PlaceholderSearchEpisode": "חיפוש פרק..",
- "ToastAccountUpdateFailed": "עדכון חשבון נכשל",
"ToastAccountUpdateSuccess": "חשבון עודכן בהצלחה",
"ToastAuthorImageRemoveSuccess": "תמונת המחבר הוסרה בהצלחה",
- "ToastAuthorUpdateFailed": "עדכון המחבר נכשל",
"ToastAuthorUpdateMerged": "המחבר מוזג",
"ToastAuthorUpdateSuccess": "המחבר עודכן בהצלחה",
"ToastAuthorUpdateSuccessNoImageFound": "המחבר עודכן (תמונה לא נמצאה)",
@@ -720,17 +741,13 @@
"ToastBookmarkCreateFailed": "יצירת סימניה נכשלה",
"ToastBookmarkCreateSuccess": "הסימניה נוספה בהצלחה",
"ToastBookmarkRemoveSuccess": "הסימניה הוסרה בהצלחה",
- "ToastBookmarkUpdateFailed": "עדכון הסימניה נכשל",
"ToastBookmarkUpdateSuccess": "הסימניה עודכנה בהצלחה",
"ToastChaptersHaveErrors": "פרקים מכילים שגיאות",
"ToastChaptersMustHaveTitles": "פרקים חייבים לכלול כותרות",
"ToastCollectionItemsRemoveSuccess": "הפריט(ים) הוסרו מהאוסף בהצלחה",
"ToastCollectionRemoveSuccess": "האוסף הוסר בהצלחה",
- "ToastCollectionUpdateFailed": "עדכון האוסף נכשל",
"ToastCollectionUpdateSuccess": "האוסף עודכן בהצלחה",
- "ToastItemCoverUpdateFailed": "עדכון כריכת הפריט נכשל",
"ToastItemCoverUpdateSuccess": "כריכת הפריט עודכנה בהצלחה",
- "ToastItemDetailsUpdateFailed": "עדכון פרטי הפריט נכשל",
"ToastItemDetailsUpdateSuccess": "פרטי הפריט עודכנו בהצלחה",
"ToastItemMarkedAsFinishedFailed": "סימון כפריט כהושלם נכשל",
"ToastItemMarkedAsFinishedSuccess": "הפריט סומן כהושלם בהצלחה",
@@ -742,12 +759,10 @@
"ToastLibraryDeleteSuccess": "הספרייה נמחקה בהצלחה",
"ToastLibraryScanFailedToStart": "הפעלת הסריקה נכשלה",
"ToastLibraryScanStarted": "הסריקה של הספרייה החלה",
- "ToastLibraryUpdateFailed": "עדכון הספרייה נכשל",
"ToastLibraryUpdateSuccess": "הספרייה \"{0}\" עודכנה בהצלחה",
"ToastPlaylistCreateFailed": "יצירת רשימת השמעה נכשלה",
"ToastPlaylistCreateSuccess": "רשימת השמעה נוצרה בהצלחה",
"ToastPlaylistRemoveSuccess": "רשימת השמעה הוסרה בהצלחה",
- "ToastPlaylistUpdateFailed": "עדכון רשימת השמעה נכשל",
"ToastPlaylistUpdateSuccess": "רשימת השמעה עודכנה בהצלחה",
"ToastPodcastCreateFailed": "יצירת הפודקאסט נכשלה",
"ToastPodcastCreateSuccess": "הפודקאסט נוצר בהצלחה",
@@ -759,7 +774,6 @@
"ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"",
"ToastSeriesUpdateFailed": "עדכון הסדרה נכשל",
"ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה",
- "ToastServerSettingsUpdateFailed": "כשל בעדכון הגדרות שרת",
"ToastServerSettingsUpdateSuccess": "הגדרות שרת עודכנו בהצלחה",
"ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה",
"ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה",
diff --git a/client/strings/hr.json b/client/strings/hr.json
index 0b6fc2285d..502973c441 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -56,6 +56,7 @@
"ButtonOpenManager": "Otvori Upravitelja",
"ButtonPause": "Pauziraj",
"ButtonPlay": "Reproduciraj",
+ "ButtonPlayAll": "Reproduciraj sve",
"ButtonPlaying": "Izvodi se",
"ButtonPlaylists": "Popisi za izvođenje",
"ButtonPrevious": "Prethodno",
@@ -65,6 +66,7 @@
"ButtonPurgeItemsCache": "Isprazni predmemoriju stavki",
"ButtonQueueAddItem": "Dodaj u red",
"ButtonQueueRemoveItem": "Ukloni iz reda",
+ "ButtonQuickEmbed": "Brzo ugrađivanje",
"ButtonQuickEmbedMetadata": "Brzo ugrađivanje meta-podataka",
"ButtonQuickMatch": "Brzo prepoznavanje",
"ButtonReScan": "Ponovno skeniraj",
@@ -80,7 +82,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
"ButtonReset": "Poništi",
"ButtonResetToDefault": "Vrati na početne postavke",
- "ButtonRestore": "Povrati",
+ "ButtonRestore": "Vraćanje",
"ButtonSave": "Spremi",
"ButtonSaveAndClose": "Spremi i zatvori",
"ButtonSaveTracklist": "Spremi popis zvučnih zapisa",
@@ -96,7 +98,7 @@
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
"ButtonStartMetadataEmbed": "Pokreni ugradnju meta-podataka",
"ButtonStats": "Statistika",
- "ButtonSubmit": "Podnesi",
+ "ButtonSubmit": "Pošalji",
"ButtonTest": "Test",
"ButtonUnlinkOpenId": "Prekini vezu s OpenID-jem",
"ButtonUpload": "Učitaj",
@@ -161,6 +163,7 @@
"HeaderNotificationUpdate": "Ažuriraj obavijest",
"HeaderNotifications": "Obavijesti",
"HeaderOpenIDConnectAuthentication": "Prijava na OpenID Connect",
+ "HeaderOpenListeningSessions": "Otvorene sesije slušanja",
"HeaderOpenRSSFeed": "Otvori RSS izvor",
"HeaderOtherFiles": "Druge datoteke",
"HeaderPasswordAuthentication": "Provjera autentičnosti zaporkom",
@@ -178,6 +181,7 @@
"HeaderRemoveEpisodes": "Ukloni {0} nastavaka",
"HeaderSavedMediaProgress": "Spremljen napredak medija",
"HeaderSchedule": "Zakazivanje",
+ "HeaderScheduleEpisodeDownloads": "Zakazivanje automatskog preuzimanja nastavaka",
"HeaderScheduleLibraryScans": "Zakaži automatsko skeniranje knjižnice",
"HeaderSession": "Sesija",
"HeaderSetBackupSchedule": "Zakazivanje sigurnosne pohrane",
@@ -223,7 +227,11 @@
"LabelAllUsersExcludingGuests": "Svi korisnici osim gostiju",
"LabelAllUsersIncludingGuests": "Svi korisnici uključujući i goste",
"LabelAlreadyInYourLibrary": "Već u vašoj knjižnici",
+ "LabelApiToken": "API Token",
"LabelAppend": "Pridodaj",
+ "LabelAudioBitrate": "Kvaliteta zvučnog zapisa (npr. 128k)",
+ "LabelAudioChannels": "Broj zvučnih kanala (1 ili 2)",
+ "LabelAudioCodec": "Zvučni kodek",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Ime Prezime)",
"LabelAuthorLastFirst": "Autor (Prezime, Ime)",
@@ -236,29 +244,33 @@
"LabelAutoRegister": "Automatska registracija",
"LabelAutoRegisterDescription": "Automatski izradi nove korisnike nakon prijave",
"LabelBackToUser": "Povratak na korisnika",
+ "LabelBackupAudioFiles": "Sigurnosno kopiranje zvučnih datoteka",
"LabelBackupLocation": "Lokacija sigurnosnih kopija",
- "LabelBackupsEnableAutomaticBackups": "Uključi automatsku izradu sigurnosnih kopija",
+ "LabelBackupsEnableAutomaticBackups": "Omogući automatsku izradu sigurnosnih kopija",
"LabelBackupsEnableAutomaticBackupsHelp": "Sigurnosne kopije spremaju se u /metadata/backups",
"LabelBackupsMaxBackupSize": "Maksimalna veličina sigurnosne kopije (u GB) (0 za neograničeno)",
"LabelBackupsMaxBackupSizeHelp": "U svrhu sprečavanja izrade krive konfiguracije, sigurnosne kopije neće se izraditi ako su veće od zadane veličine.",
"LabelBackupsNumberToKeep": "Broj sigurnosnih kopija za čuvanje",
"LabelBackupsNumberToKeepHelp": "Moguće je izbrisati samo jednu po jednu sigurnosnu kopiju, ako ih već imate više trebat ćete ih ručno ukloniti.",
"LabelBitrate": "Protok",
- "LabelBooks": "knjiga/e",
+ "LabelBonus": "Bonus",
+ "LabelBooks": "Knjige",
"LabelButtonText": "Tekst gumba",
- "LabelByAuthor": "po {0}",
+ "LabelByAuthor": "autor: {0}",
"LabelChangePassword": "Promijeni zaporku",
"LabelChannels": "Kanali",
+ "LabelChapterCount": "{0} Poglavlje/a",
"LabelChapterTitle": "Naslov poglavlja",
"LabelChapters": "Poglavlja",
"LabelChaptersFound": "poglavlja pronađeno",
"LabelClickForMoreInfo": "Kliknite za više informacija",
+ "LabelClickToUseCurrentValue": "Kliknite za trenutnu vrijednost",
"LabelClosePlayer": "Zatvori reproduktor",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Serijale prikaži sažeto",
"LabelCollapseSubSeries": "Podserijale prikaži sažeto",
"LabelCollection": "Zbirka",
- "LabelCollections": "Zbirka/i",
+ "LabelCollections": "Zbirke",
"LabelComplete": "Dovršeno",
"LabelConfirmPassword": "Potvrda zaporke",
"LabelContinueListening": "Nastavi slušati",
@@ -266,7 +278,7 @@
"LabelContinueSeries": "Nastavi serijal",
"LabelCover": "Naslovnica",
"LabelCoverImageURL": "URL naslovnice",
- "LabelCreatedAt": "Stvoreno",
+ "LabelCreatedAt": "Izrađen",
"LabelCronExpression": "Cron izraz",
"LabelCurrent": "Trenutan",
"LabelCurrently": "Trenutno:",
@@ -302,12 +314,25 @@
"LabelEmailSettingsTestAddress": "Probna adresa",
"LabelEmbeddedCover": "Ugrađena naslovnica",
"LabelEnable": "Omogući",
+ "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:",
+ "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.",
+ "LabelEncodingWarningAdvancedSettings": "Pažnja: Ne mijenjajte ove postavke ako niste temeljito upoznati s opcijama kodiranja u ffmpegu.",
+ "LabelEncodingWatcherDisabled": "Ako vam je onemogućeno praćenje mape, ovu ćete zvučnu knjigu poslije morati ponovno skenirati.",
"LabelEnd": "Kraj",
"LabelEndOfChapter": "Kraj poglavlja",
"LabelEpisode": "Nastavak",
+ "LabelEpisodeNotLinkedToRssFeed": "Nastavak nije povezan s RSS izvorom",
+ "LabelEpisodeNumber": "{0}. nastavak",
"LabelEpisodeTitle": "Naslov nastavka",
"LabelEpisodeType": "Vrsta nastavka",
+ "LabelEpisodeUrlFromRssFeed": "URL nastavka iz RSS izvora",
"LabelEpisodes": "Nastavci",
+ "LabelEpisodic": "U nastavcima",
"LabelExample": "Primjer",
"LabelExpandSeries": "Serijal prikaži prošireno",
"LabelExpandSubSeries": "Podserijal prikaži prošireno",
@@ -335,6 +360,7 @@
"LabelFontScale": "Veličina slova",
"LabelFontStrikethrough": "Precrtano",
"LabelFormat": "Format",
+ "LabelFull": "Cijeli",
"LabelGenre": "Žanr",
"LabelGenres": "Žanrovi",
"LabelHardDeleteFile": "Obriši datoteku zauvijek",
@@ -368,7 +394,7 @@
"LabelLanguages": "Jezici",
"LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja ažurirana knjiga",
- "LabelLastSeen": "Zadnje pogledano",
+ "LabelLastSeen": "Zadnji puta viđen",
"LabelLastTime": "Zadnji puta",
"LabelLastUpdate": "Zadnje ažuriranje",
"LabelLayout": "Prikaz",
@@ -390,6 +416,10 @@
"LabelLowestPriority": "Najniži prioritet",
"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",
+ "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",
"LabelMetaTag": "Meta oznaka",
@@ -411,8 +441,8 @@
"LabelNew": "Novo",
"LabelNewPassword": "Nova zaporka",
"LabelNewestAuthors": "Najnoviji autori",
- "LabelNewestEpisodes": "Najnoviji nastavci",
- "LabelNextBackupDate": "Sljedeće izrada sigurnosne kopije",
+ "LabelNewestEpisodes": "Najnovije epizode",
+ "LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije",
"LabelNextScheduledRun": "Sljedeće zakazano izvođenje",
"LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka",
"LabelNoEpisodesSelected": "Nema odabranih nastavaka",
@@ -435,12 +465,14 @@
"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.",
"LabelOpenRSSFeed": "Otvori RSS Feed",
"LabelOverwrite": "Prepiši",
+ "LabelPaginationPageXOfY": "Stranica {0} od {1}",
"LabelPassword": "Zaporka",
"LabelPath": "Putanja",
"LabelPermanent": "Trajno",
"LabelPermissionsAccessAllLibraries": "Ima pristup svim knjižnicama",
"LabelPermissionsAccessAllTags": "Ima pristup svim oznakama",
- "LabelPermissionsAccessExplicitContent": "Ima pristup eksplicitnom sadržzaju",
+ "LabelPermissionsAccessExplicitContent": "Ima pristup eksplicitnom sadržaju",
+ "LabelPermissionsCreateEreader": "Može stvoriti e-čitač",
"LabelPermissionsDelete": "Smije brisati",
"LabelPermissionsDownload": "Smije preuzimati",
"LabelPermissionsUpdate": "Smije ažurirati",
@@ -462,8 +494,10 @@
"LabelProvider": "Dobavljač",
"LabelProviderAuthorizationValue": "Vrijednost autorizacijskog zaglavlja",
"LabelPubDate": "Datum izdavanja",
- "LabelPublishYear": "Godina izdavanja",
+ "LabelPublishYear": "Godina objavljivanja",
"LabelPublishedDate": "Objavljeno {0}",
+ "LabelPublishedDecade": "Desetljeće izdanja",
+ "LabelPublishedDecades": "Desetljeća izdanja",
"LabelPublisher": "Izdavač",
"LabelPublishers": "Izdavači",
"LabelRSSFeedCustomOwnerEmail": "Prilagođena adresa e-pošte vlasnika",
@@ -483,21 +517,28 @@
"LabelRedo": "Ponovi",
"LabelRegion": "Regija",
"LabelReleaseDate": "Datum izlaska",
+ "LabelRemoveAllMetadataAbs": "Ukloni sve datoteke metadata.abs",
+ "LabelRemoveAllMetadataJson": "Ukloni sve datoteke metadata.json",
"LabelRemoveCover": "Ukloni naslovnicu",
+ "LabelRemoveMetadataFile": "Ukloni datoteke s meta-podatcima iz mapa knjižničkih stavki",
+ "LabelRemoveMetadataFileHelp": "Uklanjanje svih datoteka metadata.json i metadata.abs u vaših {0} mapa.",
"LabelRowsPerPage": "Redaka po stranici",
"LabelSearchTerm": "Traži pojam",
"LabelSearchTitle": "Traži naslov",
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
"LabelSeason": "Sezona",
+ "LabelSeasonNumber": "{0}. sezona",
"LabelSelectAll": "Označi sve",
"LabelSelectAllEpisodes": "Označi sve nastavke",
"LabelSelectEpisodesShowing": "Prikazujem {0} odabranih nastavaka",
"LabelSelectUsers": "Označi korisnike",
"LabelSendEbookToDevice": "Pošalji e-knjigu",
"LabelSequence": "Slijed",
- "LabelSeries": "Serijal/a",
+ "LabelSerial": "Serijal",
+ "LabelSeries": "Serijal",
"LabelSeriesName": "Ime serijala",
"LabelSeriesProgress": "Napredak u serijalu",
+ "LabelServerLogLevel": "Razina zapisa poslužitelja",
"LabelServerYearReview": "Godišnji pregled poslužitelja ({0})",
"LabelSetEbookAsPrimary": "Postavi kao primarno",
"LabelSetEbookAsSupplementary": "Postavi kao dopunsko",
@@ -522,6 +563,9 @@
"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",
"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 uključite ovu opciju, serijal će vam se nastaviti od zadnje dovršene knjige umjesto od prve nezapočete knjige.",
"LabelSettingsParseSubtitles": "Raščlani podnaslove",
@@ -586,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} minuta",
"LabelTimeDurationXSeconds": "{0} sekundi",
"LabelTimeInMinutes": "Vrijeme u minutama",
+ "LabelTimeLeft": "{0} preostalo",
"LabelTimeListened": "Vremena odslušano",
"LabelTimeListenedToday": "Vremena odslušano danas",
"LabelTimeRemaining": "{0} preostalo",
@@ -593,6 +638,7 @@
"LabelTitle": "Naslov",
"LabelToolsEmbedMetadata": "Ugradi meta-podatke",
"LabelToolsEmbedMetadataDescription": "Ugradi meta-podatke u zvučne datoteke zajedno s naslovnicom i poglavljima.",
+ "LabelToolsM4bEncoder": "M4B kodiranje",
"LabelToolsMakeM4b": "Stvori M4B datoteku audioknjige",
"LabelToolsMakeM4bDescription": "Izrađuje zvučnu knjigu u .M4B formatu s ugrađenim meta-podatcima, naslovnicom i poglavljima.",
"LabelToolsSplitM4b": "Podijeli M4B datoteke u MP3 datoteke",
@@ -601,10 +647,11 @@
"LabelTotalTimeListened": "Sveukupno vrijeme slušanja",
"LabelTrackFromFilename": "Naslov iz imena datoteke",
"LabelTrackFromMetadata": "Naslov iz meta-podataka",
- "LabelTracks": "Naslovi",
+ "LabelTracks": "Zvučni zapisi",
"LabelTracksMultiTrack": "Više zvučnih zapisa",
"LabelTracksNone": "Nema zapisa",
"LabelTracksSingleTrack": "Jedan zvučni zapis",
+ "LabelTrailer": "Najava",
"LabelType": "Vrsta",
"LabelUnabridged": "Neskraćeno",
"LabelUndo": "Vrati",
@@ -618,8 +665,10 @@
"LabelUploaderDragAndDrop": "Pritisni i prevuci datoteke ili mape",
"LabelUploaderDropFiles": "Ispusti datoteke",
"LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
+ "LabelUseAdvancedOptions": "Koristi se naprednim opcijama",
"LabelUseChapterTrack": "Koristi zvučni zapis poglavlja",
"LabelUseFullTrack": "Koristi cijeli zvučni zapis",
+ "LabelUseZeroForUnlimited": "0 za neograničeno",
"LabelUser": "Korisnik",
"LabelUsername": "Korisničko ime",
"LabelValue": "Vrijednost",
@@ -640,7 +689,7 @@
"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
.",
- "MessageBackupsDescription": "Backups uključuju korisnike, korisnikov napredak, detalje stavki iz biblioteke, postavke server i slike iz /metadata/items
& /metadata/authors
. Backups ne uključuju nijedne datoteke koje su u folderima biblioteke.",
+ "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.",
"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",
@@ -654,7 +703,7 @@
"MessageChapterErrorFirstNotZero": "Prvo poglavlje mora započeti u 0",
"MessageChapterErrorStartGteDuration": "Netočno vrijeme početka, mora biti manje od trajanja zvučne knjige",
"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.",
+ "MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja zvučne knjige",
"MessageCheckingCron": "Provjeravam cron...",
"MessageConfirmCloseFeed": "Sigurno želite zatvoriti ovaj izvor?",
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
@@ -666,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "Sigurno želite izbrisati prilagođenog pružatelja meta-podataka \"{0}\"?",
"MessageConfirmDeleteNotification": "Sigurno želite izbrisati ovu obavijest?",
"MessageConfirmDeleteSession": "Sigurno želite obrisati 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?",
@@ -677,6 +727,7 @@
"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?",
"MessageConfirmQuickEmbed": "Pažnja! Funkcija brzog ugrađivanja ne stvara sigurnosne kopije vaših zvučnih datoteka. Provjerite imate li sigurnosnu kopiju. Želite li nastaviti?",
+ "MessageConfirmQuickMatchEpisodes": "Brzo prepoznavanje nastavaka prepisat će pojedinosti ukoliko se pronađe podudaranje. Neprepoznati nastavci će se ažurirati. Jeste li sigurni?",
"MessageConfirmReScanLibraryItems": "Sigurno želite ponovno skenirati {0} stavki?",
"MessageConfirmRemoveAllChapters": "Sigurno želite ukloniti sva poglavlja?",
"MessageConfirmRemoveAuthor": "Sigurno želite ukloniti autora \"{0}\"?",
@@ -684,6 +735,7 @@
"MessageConfirmRemoveEpisode": "Sigurno želite ukloniti nastavak \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Sigurno želite ukloniti {0} nastavaka?",
"MessageConfirmRemoveListeningSessions": "Sigurno želite ukloniti {0} sesija slušanja?",
+ "MessageConfirmRemoveMetadataFiles": "Sigurno želite ukloniti sve datoteke metadata.{0} u mapama vaših knjižničkih stavki?",
"MessageConfirmRemoveNarrator": "Sigurno želite ukloniti pripovjedača \"{0}\"?",
"MessageConfirmRemovePlaylist": "Sigurno želite ukloniti vaš popis za izvođenje \"{0}\"?",
"MessageConfirmRenameGenre": "Sigurno želite preimenovati žanr \"{0}\" u \"{1}\" za sve stavke?",
@@ -696,9 +748,10 @@
"MessageConfirmSendEbookToDevice": "Sigurno želite poslati {0} e-knjiga/u \"{1}\" na uređaj \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Sigurno želite odspojiti ovog korisnika s OpenID-ja?",
"MessageDownloadingEpisode": "Preuzimam nastavak",
- "MessageDragFilesIntoTrackOrder": "Ispravi redoslijed zapisa prevlačenje datoteka",
+ "MessageDragFilesIntoTrackOrder": "Prevlačenjem datoteka složite pravilan redoslijed",
"MessageEmbedFailed": "Ugrađivanje nije uspjelo!",
"MessageEmbedFinished": "Ugrađivanje je dovrš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}",
@@ -743,6 +796,7 @@
"MessageNoLogs": "Nema zapisnika",
"MessageNoMediaProgress": "Nema podataka o započetim medijima",
"MessageNoNotifications": "Nema obavijesti",
+ "MessageNoPodcastFeed": "Neispravan podcast: Nema izvora",
"MessageNoPodcastsFound": "Nije pronađen niti jedan podcast",
"MessageNoResults": "Nema rezultata",
"MessageNoSearchResultsFor": "Nema rezultata pretrage za \"{0}\"",
@@ -759,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Stvori popis za izvođenje od zbirke",
"MessagePleaseWait": "Molimo 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'.",
"MessageRemoveChapter": "Ukloni poglavlje",
"MessageRemoveEpisodes": "Ukloni {0} nastavaka",
@@ -776,6 +834,41 @@
"MessageShareExpiresIn": "Istječe za {0}",
"MessageShareURLWillBe": "URL za dijeljenje bit će {0} ",
"MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?",
+ "MessageTaskAudioFileNotWritable": "U zvučnu datoteku \"{0}\" nije moguće pisati",
+ "MessageTaskCanceledByUser": "Korisnik je otkazao izvršavanje zadatka",
+ "MessageTaskDownloadingEpisodeDescription": "Preuzimanje nastavka \"{0}\"",
+ "MessageTaskEmbeddingMetadata": "Ugrađivanje meta-podataka",
+ "MessageTaskEmbeddingMetadataDescription": "Ugrađivanje meta-podataka u zvučnu knjigu \"{0}\"",
+ "MessageTaskEncodingM4b": "Kodiranje M4B datoteke",
+ "MessageTaskEncodingM4bDescription": "Kodiranje zvučne knjige \"{0}\" u jedinstvenu m4b datoteku",
+ "MessageTaskFailed": "Nije uspjelo",
+ "MessageTaskFailedToBackupAudioFile": "Izrada sigurnosne kopije zvučne datoteke \"{0}\" nije uspjela",
+ "MessageTaskFailedToCreateCacheDirectory": "Izrada mape predmemorije nije uspjela",
+ "MessageTaskFailedToEmbedMetadataInFile": "Ugradnja meta-podataka u datoteku \"{0}\" nije uspjela",
+ "MessageTaskFailedToMergeAudioFiles": "Spajanje zvučnih datoteka nije uspjelo",
+ "MessageTaskFailedToMoveM4bFile": "Premještanje m4b datoteke nije uspjelo",
+ "MessageTaskFailedToWriteMetadataFile": "Pisanje datoteke s meta-podatcima nije uspjelo",
+ "MessageTaskMatchingBooksInLibrary": "Prepoznavanje knjiga u knjižnici \"{0}\"",
+ "MessageTaskNoFilesToScan": "Nema datoteka za skeniranje",
+ "MessageTaskOpmlImport": "Uvoz OPML-a",
+ "MessageTaskOpmlImportDescription": "Stvaram podcaste od {0} RSS izvora",
+ "MessageTaskOpmlImportFeed": "Uvoz OPML izvora",
+ "MessageTaskOpmlImportFeedDescription": "Uvoz RSS izvora \"{0}\"",
+ "MessageTaskOpmlImportFeedFailed": "Izvor podcasta nije dohvaćen",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Stvaranje podcasta \"{0}\"",
+ "MessageTaskOpmlImportFeedPodcastExists": "Podcast već postoji u putanji",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Stvaranje podcasta nije uspjelo",
+ "MessageTaskOpmlImportFinished": "Dodano {0} podcasta",
+ "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",
+ "MessageTaskScanItemsAdded": "{0} dodan(o)",
+ "MessageTaskScanItemsMissing": "{0} nedostaje",
+ "MessageTaskScanItemsUpdated": "{0} ažurirano",
+ "MessageTaskScanNoChangesNeeded": "Nisu potrebne izmjene",
+ "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!",
@@ -793,6 +886,10 @@
"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.",
+ "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",
+ "NotificationOnTestDescription": "Događaj za testiranje sustava obavijesti",
"PlaceholderNewCollection": "Ime nove zbirke",
"PlaceholderNewFolderPath": "Nova putanja mape",
"PlaceholderNewPlaylist": "Naziv novog popisa za izvođenje",
@@ -816,14 +913,13 @@
"StatsTopNarrators": "NAJPOPULARNIJI PRIPOVJEDAČI",
"StatsTotalDuration": "S ukupnim trajanjem od…",
"StatsYearInReview": "PREGLED GODINE",
- "ToastAccountUpdateFailed": "Ažuriranje računa nije uspjelo",
"ToastAccountUpdateSuccess": "Račun ažuriran",
"ToastAppriseUrlRequired": "Obavezno upisati Apprise URL",
+ "ToastAsinRequired": "ASIN je obvezan",
"ToastAuthorImageRemoveSuccess": "Slika autora uklonjena",
"ToastAuthorNotFound": "Autor \"{0}\" nije pronađen",
"ToastAuthorRemoveSuccess": "Autor uklonjen",
"ToastAuthorSearchNotFound": "Autor nije pronađen",
- "ToastAuthorUpdateFailed": "Ažuriranje autora nije uspjelo",
"ToastAuthorUpdateMerged": "Autor pripojen",
"ToastAuthorUpdateSuccess": "Autor ažuriran",
"ToastAuthorUpdateSuccessNoImageFound": "Autor ažuriran (slika nije pronađena)",
@@ -834,29 +930,29 @@
"ToastBackupDeleteSuccess": "Sigurnosna kopija izbrisana",
"ToastBackupInvalidMaxKeep": "Neispravan broj sigurnosnih kopija za čuvanje",
"ToastBackupInvalidMaxSize": "Neispravna najveća veličina sigurnosne kopije",
- "ToastBackupPathUpdateFailed": "Ažuriranje putanje za sigurnosne kopije nije uspjelo",
"ToastBackupRestoreFailed": "Vraćanje sigurnosne kopije nije uspjelo",
"ToastBackupUploadFailed": "Učitavanje sigurnosne kopije nije uspjelo",
"ToastBackupUploadSuccess": "Sigurnosna kopija učitana",
"ToastBatchDeleteFailed": "Grupno brisanje nije uspjelo",
"ToastBatchDeleteSuccess": "Grupno brisanje je uspješno dovršeno",
+ "ToastBatchQuickMatchFailed": "Grupno brzo prepoznavanje nije uspjelo!",
+ "ToastBatchQuickMatchStarted": "Započelo je brzo prepoznavanje {0} knjiga!",
"ToastBatchUpdateFailed": "Skupno ažuriranje nije uspjelo",
"ToastBatchUpdateSuccess": "Skupno ažuriranje uspješno dovršeno",
"ToastBookmarkCreateFailed": "Izrada knjižne oznake nije uspjela",
"ToastBookmarkCreateSuccess": "Knjižna oznaka dodana",
"ToastBookmarkRemoveSuccess": "Knjižna oznaka uklonjena",
- "ToastBookmarkUpdateFailed": "Ažuriranje knjižne oznake nije uspjelo",
"ToastBookmarkUpdateSuccess": "Knjižna oznaka ažurirana",
"ToastCachePurgeFailed": "Čišćenje predmemorije nije uspjelo",
"ToastCachePurgeSuccess": "Predmemorija uspješno očišćena",
"ToastChaptersHaveErrors": "Poglavlja imaju pogreške",
"ToastChaptersMustHaveTitles": "Poglavlja moraju imati naslove",
"ToastChaptersRemoved": "Poglavlja uklonjena",
+ "ToastChaptersUpdated": "Poglavlja su ažurirana",
"ToastCollectionItemsAddFailed": "Neuspješno dodavanje stavki u zbirku",
"ToastCollectionItemsAddSuccess": "Uspješno dodavanje stavki u zbirku",
"ToastCollectionItemsRemoveSuccess": "Stavke izbrisane iz zbirke",
"ToastCollectionRemoveSuccess": "Zbirka izbrisana",
- "ToastCollectionUpdateFailed": "Ažuriranje zbirke nije uspjelo",
"ToastCollectionUpdateSuccess": "Zbirka ažurirana",
"ToastCoverUpdateFailed": "Ažuriranje naslovnice nije uspjelo",
"ToastDeleteFileFailed": "Brisanje datoteke nije uspjelo",
@@ -865,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "E-čitač s tim nazivom već postoji",
"ToastDeviceTestEmailFailed": "Slanje probne poruke e-pošte nije uspjelo",
"ToastDeviceTestEmailSuccess": "Probna poruka e-pošte poslana",
- "ToastDeviceUpdateFailed": "Ažuriranje uređaja nije uspjelo",
- "ToastEmailSettingsUpdateFailed": "Ažuriranje postavki e-pošte nije uspjelo",
"ToastEmailSettingsUpdateSuccess": "Postavke e-pošte ažurirane",
"ToastEncodeCancelFailed": "Kodiranje nije uspješno otkazano",
"ToastEncodeCancelSucces": "Kodiranje otkazano",
"ToastEpisodeDownloadQueueClearFailed": "Redoslijed izvođenja nije uspješno očišćen",
"ToastEpisodeDownloadQueueClearSuccess": "Redoslijed preuzimanja nastavaka očišćen",
+ "ToastEpisodeUpdateSuccess": "{0} nastavak/a ažurirano",
"ToastErrorCannotShare": "Dijeljenje na ovaj uređaj nije moguće",
"ToastFailedToLoadData": "Učitavanje podataka nije uspjelo",
+ "ToastFailedToMatch": "Nije prepoznato",
"ToastFailedToShare": "Dijeljenje nije uspjelo",
- "ToastFailedToUpdateAccount": "Ažuriranje računa nije uspjelo",
- "ToastFailedToUpdateUser": "Ažuriranje korisnika nije uspjelo",
+ "ToastFailedToUpdate": "Ažuriranje nije uspjelo",
"ToastInvalidImageUrl": "Neispravan URL slike",
+ "ToastInvalidMaxEpisodesToDownload": "Neispravan unos maksimalnog broja nastavaka",
"ToastInvalidUrl": "Neispravan URL",
- "ToastItemCoverUpdateFailed": "Ažuriranje naslovnice stavke nije uspjelo",
"ToastItemCoverUpdateSuccess": "Naslovnica stavke ažurirana",
"ToastItemDeletedFailed": "Brisanje stavke nije uspjelo",
"ToastItemDeletedSuccess": "Stavka je izbrisana",
- "ToastItemDetailsUpdateFailed": "Ažuriranje podataka stavke nije uspjelo",
"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",
- "ToastItemUpdateFailed": "Ažuriranje stavke nije uspjelo",
"ToastItemUpdateSuccess": "Stavka ažurirana",
"ToastLibraryCreateFailed": "Stvaranje knjižnice nije uspjelo",
"ToastLibraryCreateSuccess": "Knjižnica \"{0}\" stvorena",
@@ -897,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Knjižnica izbrisana",
"ToastLibraryScanFailedToStart": "Skeniranje nije uspjelo",
"ToastLibraryScanStarted": "Skeniranje knjižnice započelo",
- "ToastLibraryUpdateFailed": "Ažuriranje knjižnice nije uspjelo",
"ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" ažurirana",
+ "ToastMatchAllAuthorsFailed": "Nisu prepoznati svi autori",
+ "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",
"ToastNameEmailRequired": "Ime i adresa e-pošte su obavezni",
"ToastNameRequired": "Ime je obavezno",
+ "ToastNewEpisodesFound": "pronađeno {0} novih nastavaka",
"ToastNewUserCreatedFailed": "Račun \"{0}\" nije uspješno izrađen",
"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",
"ToastNewUserTagError": "Potrebno je odabrati najmanje jednu oznaku",
"ToastNewUserUsernameError": "Upišite korisničko ime",
+ "ToastNoNewEpisodesFound": "Nisu pronađeni novi nastavci",
"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",
"ToastNotificationQueueMaximum": "Najveći broj obavijesti u redu mora biti >= 0",
- "ToastNotificationSettingsUpdateFailed": "Ažuriranje postavki obavijesti nije uspjelo",
"ToastNotificationSettingsUpdateSuccess": "Postavke obavijesti ažurirane",
"ToastNotificationTestTriggerFailed": "Okidanje probne obavijesti nije uspjelo",
"ToastNotificationTestTriggerSuccess": "Okinuta je probna obavijest",
- "ToastNotificationUpdateFailed": "Ažuriranje obavijesti nije uspjelo",
"ToastNotificationUpdateSuccess": "Obavijest ažurirana",
"ToastPlaylistCreateFailed": "Popis za izvođenje nije izrađen",
"ToastPlaylistCreateSuccess": "Popis za izvođenje izrađen",
"ToastPlaylistRemoveSuccess": "Popis za izvođenje uklonjen",
- "ToastPlaylistUpdateFailed": "Ažuriranje popisa za izvođenje nije uspjelo",
"ToastPlaylistUpdateSuccess": "Popis za izvođenje ažuriran",
"ToastPodcastCreateFailed": "Podcast nije izrađen",
"ToastPodcastCreateSuccess": "Podcast uspješno izrađen",
"ToastPodcastGetFeedFailed": "Dohvat izvora podcasta nije uspio",
"ToastPodcastNoEpisodesInFeed": "U RSS izvoru nisu pronađeni nastavci",
"ToastPodcastNoRssFeed": "Podcast nema RSS izvor",
+ "ToastProgressIsNotBeingSynced": "Napredak se ne sinkronizira, ponovno pokrenite reprodukciju",
"ToastProviderCreatedFailed": "Dodavanje pružatelja nije uspjelo",
"ToastProviderCreatedSuccess": "Novi pružatelj dodan",
"ToastProviderNameAndUrlRequired": "Ime i URL su obavezni",
@@ -950,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "E-knjiga poslana uređaju \"{0}\"",
"ToastSeriesUpdateFailed": "Ažuriranje serijala nije uspjelo",
"ToastSeriesUpdateSuccess": "Serijal uspješno ažuriran",
- "ToastServerSettingsUpdateFailed": "Ažuriranje postavki poslužitelja nije uspjelo",
"ToastServerSettingsUpdateSuccess": "Postavke poslužitelja ažurirane",
"ToastSessionCloseFailed": "Zatvaranje sesije nije uspjelo",
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
"ToastSessionDeleteSuccess": "Sesija obrisana",
+ "ToastSleepTimerDone": "Timer za spavanje istječe... zZzzZz",
"ToastSlugMustChange": "Slug sadrži nedozvoljene znakove",
"ToastSlugRequired": "Slug je obavezan",
"ToastSocketConnected": "Socket priključen",
"ToastSocketDisconnected": "Veza sa socketom je prekinuta",
"ToastSocketFailedToConnect": "Priključivanje na socket nije uspjelo",
"ToastSortingPrefixesEmptyError": "Mora imati najmanje jedan prefiks za sortiranje",
- "ToastSortingPrefixesUpdateFailed": "Ažuriranje prefiksa za sortiranje nije uspjelo",
"ToastSortingPrefixesUpdateSuccess": "Prefiksi za sortiranje ažurirani ({0} stavki)",
"ToastTitleRequired": "Naslov je obavezan",
"ToastUnknownError": "Nepoznata pogreška",
diff --git a/client/strings/hu.json b/client/strings/hu.json
index a50fce1b3b..8ab6be9e99 100644
--- a/client/strings/hu.json
+++ b/client/strings/hu.json
@@ -9,6 +9,7 @@
"ButtonApply": "Alkalmaz",
"ButtonApplyChapters": "Fejezetek alkalmazása",
"ButtonAuthors": "Szerzők",
+ "ButtonBack": "Vissza",
"ButtonBrowseForFolder": "Mappa keresése",
"ButtonCancel": "Mégse",
"ButtonCancelEncode": "Kódolás megszakítása",
@@ -18,7 +19,8 @@
"ButtonChooseFiles": "Fájlok kiválasztása",
"ButtonClearFilter": "Szűrő törlése",
"ButtonCloseFeed": "Hírcsatorna bezárása",
- "ButtonCollections": "Gyűjtemény",
+ "ButtonCloseSession": "Nyitott munkamenet bezárása",
+ "ButtonCollections": "Gyűjtemények",
"ButtonConfigureScanner": "Szkenner konfigurálása",
"ButtonCreate": "Létrehozás",
"ButtonCreateBackup": "Biztonsági másolat készítése",
@@ -27,6 +29,9 @@
"ButtonEdit": "Szerkesztés",
"ButtonEditChapters": "Fejezetek szerkesztése",
"ButtonEditPodcast": "Podcast szerkesztése",
+ "ButtonEnable": "Engedélyezés",
+ "ButtonFireAndFail": "Küldés és összeomlás",
+ "ButtonFireOnTest": "onTest esemény küldése",
"ButtonForceReScan": "Újraszkennelés kényszerítése",
"ButtonFullPath": "Teljes útvonal",
"ButtonHide": "Elrejtés",
@@ -43,22 +48,32 @@
"ButtonMatchAllAuthors": "Minden szerző egyeztetése",
"ButtonMatchBooks": "Könyvek egyeztetése",
"ButtonNevermind": "Mindegy",
+ "ButtonNext": "Következő",
"ButtonNextChapter": "Következő fejezet",
+ "ButtonNextItemInQueue": "Következő elem a sorban",
"ButtonOk": "Oké",
"ButtonOpenFeed": "Hírcsatorna megnyitása",
"ButtonOpenManager": "Kezelő megnyitása",
"ButtonPause": "Szünet",
"ButtonPlay": "Lejátszás",
+ "ButtonPlayAll": "Összes lejátszása",
"ButtonPlaying": "Lejátszás folyamatban",
"ButtonPlaylists": "Lejátszási listák",
+ "ButtonPrevious": "Előző",
"ButtonPreviousChapter": "Előző fejezet",
+ "ButtonProbeAudioFile": "Hangfájl vizsgálata",
"ButtonPurgeAllCache": "Összes gyorsítótár törlése",
"ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése",
"ButtonQueueAddItem": "Hozzáadás a sorhoz",
"ButtonQueueRemoveItem": "Eltávolítás a sorból",
+ "ButtonQuickEmbed": "Gyors beágyazás",
+ "ButtonQuickEmbedMetadata": "Metaadat gyors beágyazása",
"ButtonQuickMatch": "Gyors egyeztetés",
"ButtonReScan": "Újraszkennelés",
"ButtonRead": "Olvasás",
+ "ButtonReadLess": "Kevesebb mutatása",
+ "ButtonReadMore": "Mutass többet",
+ "ButtonRefresh": "Frissítés",
"ButtonRemove": "Eltávolítás",
"ButtonRemoveAll": "Összes eltávolítása",
"ButtonRemoveAllLibraryItems": "Összes könyvtárelem eltávolítása",
@@ -77,12 +92,15 @@
"ButtonSelectFolderPath": "Mappa útvonalának kiválasztása",
"ButtonSeries": "Sorozatok",
"ButtonSetChaptersFromTracks": "Fejezetek beállítása sávokból",
+ "ButtonShare": "Megosztás",
"ButtonShiftTimes": "Idők eltolása",
"ButtonShow": "Megjelenítés",
"ButtonStartM4BEncode": "M4B kódolás indítása",
"ButtonStartMetadataEmbed": "Metaadatok beágyazásának indítása",
+ "ButtonStats": "Statisztikák",
"ButtonSubmit": "Beküldés",
"ButtonTest": "Teszt",
+ "ButtonUnlinkOpenId": "OpenID szétkapcsolása",
"ButtonUpload": "Feltöltés",
"ButtonUploadBackup": "Biztonsági másolat feltöltése",
"ButtonUploadCover": "Borító feltöltése",
@@ -95,6 +113,7 @@
"ErrorUploadFetchMetadataNoResults": "Nem sikerült a metaadatok lekérése - próbálja meg frissíteni a címet és/vagy a szerzőt",
"ErrorUploadLacksTitle": "Cím szükséges",
"HeaderAccount": "Fiók",
+ "HeaderAddCustomMetadataProvider": "Egyedi metaadat szolgáltató hozzáadása",
"HeaderAdvanced": "Haladó",
"HeaderAppriseNotificationSettings": "Apprise értesítési beállítások",
"HeaderAudioTracks": "Audiósávok",
@@ -108,6 +127,7 @@
"HeaderCollectionItems": "Gyűjtemény elemek",
"HeaderCover": "Borító",
"HeaderCurrentDownloads": "Jelenlegi letöltések",
+ "HeaderCustomMessageOnLogin": "Egyedi üzenet bejelentkezéskor",
"HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók",
"HeaderDetails": "Részletek",
"HeaderDownloadQueue": "Letöltési sor",
@@ -139,6 +159,8 @@
"HeaderMetadataToEmbed": "Beágyazandó metaadatok",
"HeaderNewAccount": "Új fiók",
"HeaderNewLibrary": "Új könyvtár",
+ "HeaderNotificationCreate": "Értesítés készítése",
+ "HeaderNotificationUpdate": "Értesítés frissítése",
"HeaderNotifications": "Értesítések",
"HeaderOpenIDConnectAuthentication": "OpenID Connect hitelesítés",
"HeaderOpenRSSFeed": "RSS hírcsatorna megnyitása",
@@ -146,12 +168,13 @@
"HeaderPasswordAuthentication": "Jelszó hitelesítés",
"HeaderPermissions": "Engedélyek",
"HeaderPlayerQueue": "Lejátszó sor",
+ "HeaderPlayerSettings": "Lejátszó beállításai",
"HeaderPlaylist": "Lejátszási lista",
"HeaderPlaylistItems": "Lejátszási lista elemek",
"HeaderPodcastsToAdd": "Hozzáadandó podcastok",
"HeaderPreviewCover": "Borító előnézete",
"HeaderRSSFeedGeneral": "RSS részletek",
- "HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva",
+ "HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva van",
"HeaderRSSFeeds": "RSS hírcsatornák",
"HeaderRemoveEpisode": "Epizód eltávolítása",
"HeaderRemoveEpisodes": "{0} epizód eltávolítása",
@@ -168,7 +191,7 @@
"HeaderSleepTimer": "Alvásidőzítő",
"HeaderStatsLargestItems": "Legnagyobb elemek",
"HeaderStatsLongestItems": "Leghosszabb elemek (órákban)",
- "HeaderStatsMinutesListeningChart": "Hallgatási percek (az utolsó 7 napban)",
+ "HeaderStatsMinutesListeningChart": "Hallgatási grafikon percekben (az elmúlt 7 napból)",
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
"HeaderStatsTop10Authors": "Top 10 szerzők",
"HeaderStatsTop5Genres": "Top 5 műfajok",
@@ -179,9 +202,14 @@
"HeaderUpdateDetails": "Részletek frissítése",
"HeaderUpdateLibrary": "Könyvtár frissítése",
"HeaderUsers": "Felhasználók",
+ "HeaderYearReview": "{0} év áttekintése",
"HeaderYourStats": "Saját statisztikák",
"LabelAbridged": "Tömörített",
+ "LabelAbridgedChecked": "Rövidített (ellenőrizve)",
+ "LabelAbridgedUnchecked": "Teljes (nem ellenőrzött)",
+ "LabelAccessibleBy": "Hozzáférhető",
"LabelAccountType": "Fióktípus",
+ "LabelAccountTypeAdmin": "Adminisztrátor",
"LabelAccountTypeGuest": "Vendég",
"LabelAccountTypeUser": "Felhasználó",
"LabelActivity": "Tevékenység",
@@ -190,8 +218,9 @@
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
"LabelAddToPlaylistBatch": "{0} elem hozzáadása a lejátszási listához",
"LabelAddedAt": "Hozzáadás ideje",
+ "LabelAddedDate": "{0} Hozzáadva",
"LabelAdminUsersOnly": "Csak admin felhasználók",
- "LabelAll": "Minden",
+ "LabelAll": "Összes",
"LabelAllUsers": "Minden felhasználó",
"LabelAllUsersExcludingGuests": "Minden felhasználó, vendégek kivételével",
"LabelAllUsersIncludingGuests": "Minden felhasználó, beleértve a vendégeket is",
@@ -212,13 +241,14 @@
"LabelBackupLocation": "Biztonsági másolat helye",
"LabelBackupsEnableAutomaticBackups": "Automatikus biztonsági másolatok engedélyezése",
"LabelBackupsEnableAutomaticBackupsHelp": "Biztonsági másolatok mentése a /metadata/backups mappába",
- "LabelBackupsMaxBackupSize": "Maximális biztonsági másolat méret (GB-ban)",
+ "LabelBackupsMaxBackupSize": "Maximális biztonsági másolat méret (GB-ban) (0-tól végtelenig)",
"LabelBackupsMaxBackupSizeHelp": "A rossz konfiguráció elleni védelem érdekében a biztonsági másolatok meghiúsulnak, ha meghaladják a beállított méretet.",
"LabelBackupsNumberToKeep": "Megtartandó biztonsági másolatok száma",
"LabelBackupsNumberToKeepHelp": "Egyszerre csak 1 biztonsági másolat kerül eltávolításra, tehát ha már több biztonsági másolat van, mint ez a szám, akkor manuálisan kell eltávolítani őket.",
"LabelBitrate": "Bitráta",
"LabelBooks": "Könyvek",
"LabelButtonText": "Gomb szövege",
+ "LabelByAuthor": "{} által",
"LabelChangePassword": "Jelszó megváltoztatása",
"LabelChannels": "Csatornák",
"LabelChapterTitle": "Fejezet címe",
@@ -228,9 +258,10 @@
"LabelClosePlayer": "Lejátszó bezárása",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Sorozat összecsukása",
+ "LabelCollapseSubSeries": "Alszéria összecsukása",
"LabelCollection": "Gyűjtemény",
"LabelCollections": "Gyűjtemények",
- "LabelComplete": "Teljes",
+ "LabelComplete": "Kész",
"LabelConfirmPassword": "Jelszó megerősítése",
"LabelContinueListening": "Hallgatás folytatása",
"LabelContinueReading": "Olvasás folytatása",
@@ -243,6 +274,7 @@
"LabelCurrently": "Jelenleg:",
"LabelCustomCronExpression": "Egyéni Cron kifejezés:",
"LabelDatetime": "Dátumidő",
+ "LabelDays": "Napok",
"LabelDeleteFromFileSystemCheckbox": "Törlés a fájlrendszerről (ne jelölje be, ha csak az adatbázisból szeretné eltávolítani)",
"LabelDescription": "Leírás",
"LabelDeselectAll": "Minden kijelölés megszüntetése",
@@ -256,27 +288,42 @@
"LabelDownload": "Letöltés",
"LabelDownloadNEpisodes": "{0} epizód letöltése",
"LabelDuration": "Időtartam",
+ "LabelDurationComparisonExactMatch": "(pontos egyezés)",
+ "LabelDurationComparisonLonger": "({0}-val hosszabb)",
+ "LabelDurationComparisonShorter": "({0}-val rövidebb)",
"LabelDurationFound": "Megtalált időtartam:",
"LabelEbook": "E-könyv",
"LabelEbooks": "E-könyvek",
"LabelEdit": "Szerkesztés",
"LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Feladó címe",
+ "LabelEmailSettingsRejectUnauthorized": "Nem hitelesített tanúsítványok elutasítása",
+ "LabelEmailSettingsRejectUnauthorizedHelp": "Az SSL tanúsítványok érvényesítésének letiltása biztonsági kockázatoknak, például man-in-the-middle támadásoknak teheti ki a kapcsolatot. Csak akkor tiltsd le ezt az opciót, ha tisztában vagy a következményekkel, és megbízol az e-mail szerverben, amelyhez csatlakozol.",
"LabelEmailSettingsSecure": "Biztonságos",
"LabelEmailSettingsSecureHelp": "Ha igaz, a kapcsolat TLS-t használ a szerverhez való csatlakozáskor. Ha hamis, akkor TLS-t használ, ha a szerver támogatja a STARTTLS kiterjesztést. A legtöbb esetben állítsa ezt az értéket igazra, ha a 465-ös portra csatlakozik. A 587-es vagy 25-ös port esetében tartsa hamis értéken. (a nodemailer.com/smtp/#authentication oldalról)",
"LabelEmailSettingsTestAddress": "Teszt cím",
"LabelEmbeddedCover": "Beágyazott borító",
"LabelEnable": "Engedélyezés",
"LabelEnd": "Vége",
+ "LabelEndOfChapter": "Fejezet vége",
"LabelEpisode": "Epizód",
"LabelEpisodeTitle": "Epizód címe",
"LabelEpisodeType": "Epizód típusa",
+ "LabelEpisodes": "Epizódok",
"LabelExample": "Példa",
+ "LabelExpandSeries": "Sorozat kinyitása",
+ "LabelExpandSubSeries": "Alsorozat kinyitása",
+ "LabelExplicit": "Explicit",
+ "LabelExplicitChecked": "Explicit (ellenőrizve)",
+ "LabelExplicitUnchecked": "Nem explicit (nem ellenőrzött)",
+ "LabelExportOPML": "OPML exportálása",
"LabelFeedURL": "Hírcsatorna URL",
"LabelFetchingMetadata": "Metaadatok lekérése",
"LabelFile": "Fájl",
"LabelFileBirthtime": "Fájl létrehozásának ideje",
+ "LabelFileBornDate": "Született {0}",
"LabelFileModified": "Fájl módosításának ideje",
+ "LabelFileModifiedDate": "Módosítva {0}",
"LabelFilename": "Fájlnév",
"LabelFilterByUser": "Szűrés felhasználó szerint",
"LabelFindEpisodes": "Epizódok keresése",
@@ -284,6 +331,7 @@
"LabelFolder": "Mappa",
"LabelFolders": "Mappák",
"LabelFontBold": "Félkövér",
+ "LabelFontBoldness": "Betű vastagság",
"LabelFontFamily": "Betűtípus család",
"LabelFontItalic": "Dőlt",
"LabelFontScale": "Betűméret skála",
@@ -294,9 +342,11 @@
"LabelHardDeleteFile": "Fájl végleges törlése",
"LabelHasEbook": "Van e-könyve",
"LabelHasSupplementaryEbook": "Van kiegészítő e-könyve",
+ "LabelHideSubtitles": "Alcím elrejtése",
"LabelHighestPriority": "Legmagasabb prioritás",
- "LabelHost": "Hoszt",
+ "LabelHost": "Kiszolgáló",
"LabelHour": "Óra",
+ "LabelHours": "Órák",
"LabelIcon": "Ikon",
"LabelImageURLFromTheWeb": "Kép URL a weben",
"LabelInProgress": "Folyamatban",
@@ -313,8 +363,11 @@
"LabelIntervalEveryHour": "Minden órában",
"LabelInvert": "Megfordítás",
"LabelItem": "Elem",
+ "LabelJumpBackwardAmount": "Visszafelé ugrás mennyisége",
+ "LabelJumpForwardAmount": "Előre ugrás mennyisége",
"LabelLanguage": "Nyelv",
"LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve",
+ "LabelLanguages": "Nyelvek",
"LabelLastBookAdded": "Utolsó hozzáadott könyv",
"LabelLastBookUpdated": "Utolsó frissített könyv",
"LabelLastSeen": "Utolsó látogatás",
@@ -326,11 +379,13 @@
"LabelLess": "Kevesebb",
"LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak",
"LabelLibrary": "Könyvtár",
+ "LabelLibraryFilterSublistEmpty": "Nem {0}",
"LabelLibraryItem": "Könyvtári elem",
"LabelLibraryName": "Könyvtár neve",
"LabelLimit": "Korlát",
"LabelLineSpacing": "Sorköz",
"LabelListenAgain": "Újrahallgatás",
+ "LabelLogLevelDebug": "Hibakeresés",
"LabelLogLevelInfo": "Információ",
"LabelLogLevelWarn": "Figyelmeztetés",
"LabelLookForNewEpisodesAfterDate": "Új epizódok keresése ezen a dátum után",
@@ -344,7 +399,10 @@
"LabelMetadataOrderOfPrecedenceDescription": "A magasabb prioritású metaadat-források felülírják az alacsonyabb prioritásúakat",
"LabelMetadataProvider": "Metaadat-szolgáltató",
"LabelMinute": "Perc",
+ "LabelMinutes": "Percek",
"LabelMissing": "Hiányzó",
+ "LabelMissingEbook": "Nincs e-könyve",
+ "LabelMissingSupplementaryEbook": "Nincs kiegészítő e-könyve",
"LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k",
"LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett audiobookshelf://oauth
, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (*
), akkor bármely URI engedélyezett.",
"LabelMore": "Több",
@@ -358,6 +416,7 @@
"LabelNewestEpisodes": "Legújabb epizódok",
"LabelNextBackupDate": "Következő biztonsági másolat dátuma",
"LabelNextScheduledRun": "Következő ütemezett futtatás",
+ "LabelNoCustomMetadataProviders": "Nincsenek egyedi metaadat szolgáltatók",
"LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok",
"LabelNotFinished": "Nem befejezett",
"LabelNotStarted": "Nem indult el",
@@ -373,10 +432,14 @@
"LabelNotificationsMaxQueueSizeHelp": "Az események korlátozva vannak, hogy másodpercenként 1-szer történjenek. Ha a sor maximális méretű, akkor az események figyelmen kívül lesznek hagyva. Ez megakadályozza az értesítések spamelését.",
"LabelNumberOfBooks": "Könyvek száma",
"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.",
"LabelOpenRSSFeed": "RSS hírcsatorna megnyitása",
"LabelOverwrite": "Felülírás",
"LabelPassword": "Jelszó",
"LabelPath": "Útvonal",
+ "LabelPermanent": "Végleges",
"LabelPermissionsAccessAllLibraries": "Hozzáférhet az összes könyvtárhoz",
"LabelPermissionsAccessAllTags": "Hozzáférhet az összes címkéhez",
"LabelPermissionsAccessExplicitContent": "Hozzáférhet explicit tartalomhoz",
@@ -384,26 +447,34 @@
"LabelPermissionsDownload": "Letölthet",
"LabelPermissionsUpdate": "Frissíthet",
"LabelPermissionsUpload": "Feltölthet",
+ "LabelPersonalYearReview": "Az éved áttekintése ({0})",
"LabelPhotoPathURL": "Fénykép útvonal/URL",
"LabelPlayMethod": "Lejátszási módszer",
+ "LabelPlayerChapterNumberMarker": "{0} a {1} -ből",
"LabelPlaylists": "Lejátszási listák",
+ "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcast keresési régió",
"LabelPodcastType": "Podcast típus",
"LabelPodcasts": "Podcastok",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Figyelmen kívül hagyandó előtagok (nem érzékeny a kis- és nagybetűkre)",
- "LabelPreventIndexing": "A hírcsatorna indexelésének megakadályozása az iTunes és a Google podcast könyvtáraiban",
+ "LabelPreventIndexing": "Megakadályozza a hírcsatornájának indexelését az iTunes és a Google podcast könyvtárakban",
"LabelPrimaryEbook": "Elsődleges e-könyv",
"LabelProgress": "Haladás",
"LabelProvider": "Szolgáltató",
+ "LabelProviderAuthorizationValue": "Authorization fejléc értéke",
"LabelPubDate": "Kiadás dátuma",
"LabelPublishYear": "Kiadás éve",
+ "LabelPublishedDate": "Kiadva {0}",
"LabelPublisher": "Kiadó",
+ "LabelPublishers": "Kiadók",
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
"LabelRSSFeedOpen": "RSS hírcsatorna nyitva",
"LabelRSSFeedPreventIndexing": "Indexelés megakadályozása",
"LabelRSSFeedSlug": "RSS hírcsatorna slug",
"LabelRSSFeedURL": "RSS hírcsatorna URL",
+ "LabelRandomly": "Véletlenszerűen",
"LabelRead": "Olvasás",
"LabelReadAgain": "Újraolvasás",
"LabelReadEbookWithoutProgress": "E-könyv olvasása haladás nélkül",
@@ -592,9 +663,9 @@
"MessageDownloadingEpisode": "Epizód letöltése",
"MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe",
"MessageEmbedFinished": "Beágyazás befejeződött!",
- "MessageEpisodesQueuedForDownload": "{0} Epizód letöltésre várakozik",
+ "MessageEpisodesQueuedForDownload": "{0} epizód letöltésre vár",
"MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz",
- "MessageFetching": "Lekérés...",
+ "MessageFetching": "Lekérdezés...",
"MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.",
"MessageImportantNotice": "Fontos közlemény!",
"MessageInsertChapterBelow": "Fejezet beszúrása alulra",
@@ -628,7 +699,7 @@
"MessageNoGenres": "Nincsenek műfajok",
"MessageNoIssues": "Nincsenek problémák",
"MessageNoItems": "Nincsenek elemek",
- "MessageNoItemsFound": "Nem találhatóak elemek",
+ "MessageNoItemsFound": "Nincs találat",
"MessageNoListeningSessions": "Nincsenek hallgatási munkamenetek",
"MessageNoLogs": "Nincsenek naplók",
"MessageNoMediaProgress": "Nincs előrehaladás a médialejátszásban",
@@ -683,10 +754,8 @@
"PlaceholderNewPlaylist": "Új lejátszási lista neve",
"PlaceholderSearch": "Keresés..",
"PlaceholderSearchEpisode": "Epizód keresése..",
- "ToastAccountUpdateFailed": "A fiók frissítése sikertelen",
"ToastAccountUpdateSuccess": "Fiók frissítve",
"ToastAuthorImageRemoveSuccess": "Szerző képe eltávolítva",
- "ToastAuthorUpdateFailed": "A szerző frissítése sikertelen",
"ToastAuthorUpdateMerged": "Szerző összevonva",
"ToastAuthorUpdateSuccess": "Szerző frissítve",
"ToastAuthorUpdateSuccessNoImageFound": "Szerző frissítve (nem található kép)",
@@ -702,21 +771,17 @@
"ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen",
"ToastBookmarkCreateSuccess": "Könyvjelző hozzáadva",
"ToastBookmarkRemoveSuccess": "Könyvjelző eltávolítva",
- "ToastBookmarkUpdateFailed": "Könyvjelző frissítése sikertelen",
"ToastBookmarkUpdateSuccess": "Könyvjelző frissítve",
"ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak",
"ToastChaptersMustHaveTitles": "A fejezeteknek címekkel kell rendelkezniük",
"ToastCollectionItemsRemoveSuccess": "Elem(ek) eltávolítva a gyűjteményből",
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
- "ToastCollectionUpdateFailed": "Gyűjtemény frissítése sikertelen",
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
- "ToastItemCoverUpdateFailed": "Elem borítójának frissítése sikertelen",
"ToastItemCoverUpdateSuccess": "Elem borítója frissítve",
- "ToastItemDetailsUpdateFailed": "Elem részleteinek frissítése sikertelen",
"ToastItemDetailsUpdateSuccess": "Elem részletei frissítve",
"ToastItemMarkedAsFinishedFailed": "Megjelölés Befejezettként sikertelen",
"ToastItemMarkedAsFinishedSuccess": "Elem megjelölve Befejezettként",
- "ToastItemMarkedAsNotFinishedFailed": "Nem sikerült Nem Befejezettként megjelölni az elemet",
+ "ToastItemMarkedAsNotFinishedFailed": "Az elem befejezetlennek jelölése sikertelen",
"ToastItemMarkedAsNotFinishedSuccess": "Elem megjelölve Nem Befejezettként",
"ToastLibraryCreateFailed": "Könyvtár létrehozása sikertelen",
"ToastLibraryCreateSuccess": "\"{0}\" könyvtár létrehozva",
@@ -724,16 +789,14 @@
"ToastLibraryDeleteSuccess": "Könyvtár törölve",
"ToastLibraryScanFailedToStart": "A beolvasás elindítása sikertelen",
"ToastLibraryScanStarted": "Könyvtár beolvasása elindítva",
- "ToastLibraryUpdateFailed": "Könyvtár frissítése sikertelen",
"ToastLibraryUpdateSuccess": "\"{0}\" könyvtár frissítve",
"ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen",
"ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva",
"ToastPlaylistRemoveSuccess": "Lejátszási lista eltávolítva",
- "ToastPlaylistUpdateFailed": "Lejátszási lista frissítése sikertelen",
"ToastPlaylistUpdateSuccess": "Lejátszási lista frissítve",
"ToastPodcastCreateFailed": "Podcast létrehozása sikertelen",
- "ToastPodcastCreateSuccess": "Podcast sikeresen létrehozva",
- "ToastRSSFeedCloseFailed": "RSS feed bezárása sikertelen",
+ "ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva",
+ "ToastRSSFeedCloseFailed": "Az RSS hírcsatorna bezárása sikertelen",
"ToastRSSFeedCloseSuccess": "RSS feed bezárva",
"ToastRemoveItemFromCollectionFailed": "Tétel eltávolítása a gyűjteményből sikertelen",
"ToastRemoveItemFromCollectionSuccess": "Tétel eltávolítva a gyűjteményből",
diff --git a/client/strings/it.json b/client/strings/it.json
index ac0bec4e77..70490e3b08 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -30,6 +30,8 @@
"ButtonEditChapters": "Modifica Capitoli",
"ButtonEditPodcast": "Modifica Podcast",
"ButtonEnable": "Abilita",
+ "ButtonFireAndFail": "Fire and Fail",
+ "ButtonFireOnTest": "Fire onTest event",
"ButtonForceReScan": "Forza Re-Scan",
"ButtonFullPath": "Percorso Completo",
"ButtonHide": "Nascondi",
@@ -54,6 +56,7 @@
"ButtonOpenManager": "Apri Manager",
"ButtonPause": "Pausa",
"ButtonPlay": "Riproduci",
+ "ButtonPlayAll": "Riproduci tutto",
"ButtonPlaying": "In riproduzione",
"ButtonPlaylists": "Playlist",
"ButtonPrevious": "Precendente",
@@ -63,12 +66,13 @@
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
"ButtonQueueAddItem": "Aggiungi alla Coda",
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
+ "ButtonQuickEmbed": "Incorporazione Rapida",
"ButtonQuickEmbedMetadata": "Incorporamento rapido Metadati",
"ButtonQuickMatch": "Controlla Metadata Auto",
"ButtonReScan": "Ri-scansiona",
"ButtonRead": "Leggi",
- "ButtonReadLess": "Leggi di Meno",
- "ButtonReadMore": "Leggi di Più",
+ "ButtonReadLess": "Riduci",
+ "ButtonReadMore": "Espandi",
"ButtonRefresh": "Aggiorna",
"ButtonRemove": "Rimuovi",
"ButtonRemoveAll": "Rimuovi Tutto",
@@ -96,6 +100,7 @@
"ButtonStats": "Statistische",
"ButtonSubmit": "Invia",
"ButtonTest": "Test",
+ "ButtonUnlinkOpenId": "Disattiva OpenID",
"ButtonUpload": "Carica",
"ButtonUploadBackup": "Carica il backup",
"ButtonUploadCover": "Carica una copertina",
@@ -158,6 +163,7 @@
"HeaderNotificationUpdate": "Aggiornamento della notifica",
"HeaderNotifications": "Notifiche",
"HeaderOpenIDConnectAuthentication": "Autenticazione OpenID Connect",
+ "HeaderOpenListeningSessions": "Apri sessioni di ascolto",
"HeaderOpenRSSFeed": "Apri il flusso RSS",
"HeaderOtherFiles": "Altri File",
"HeaderPasswordAuthentication": "Autenticazione della password",
@@ -175,6 +181,7 @@
"HeaderRemoveEpisodes": "Rimuovi {0} Episodi",
"HeaderSavedMediaProgress": "Progressi salvati",
"HeaderSchedule": "Schedula",
+ "HeaderScheduleEpisodeDownloads": "Imposta il download automatico degli episodi",
"HeaderScheduleLibraryScans": "Schedula la scansione della libreria",
"HeaderSession": "Sessione",
"HeaderSetBackupSchedule": "Imposta programmazione Backup",
@@ -184,7 +191,7 @@
"HeaderSettingsGeneral": "Generale",
"HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Sveglia",
- "HeaderStatsLargestItems": "Oggetti Grandi",
+ "HeaderStatsLargestItems": "File pesanti",
"HeaderStatsLongestItems": "libri più lunghi (ore)",
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
"HeaderStatsRecentSessions": "Sessioni Recenti",
@@ -213,14 +220,18 @@
"LabelAddToPlaylist": "Aggiungi alla playlist",
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
"LabelAddedAt": "Aggiunto il",
- "LabelAddedDate": "{0} aggiunti",
+ "LabelAddedDate": "Aggiunti {0}",
"LabelAdminUsersOnly": "Solo utenti Amministratori",
"LabelAll": "Tutti",
"LabelAllUsers": "Tutti gli Utenti",
"LabelAllUsersExcludingGuests": "Tutti gli Utenti Esclusi gli ospiti",
"LabelAllUsersIncludingGuests": "Tutti gli Utenti Inclusi gli ospiti",
"LabelAlreadyInYourLibrary": "Già esistente nella libreria",
+ "LabelApiToken": "API Token",
"LabelAppend": "Appese",
+ "LabelAudioBitrate": "Audio Bitrate (es. 128k)",
+ "LabelAudioChannels": "Canali Audio (1 o 2)",
+ "LabelAudioCodec": "Codec Audio",
"LabelAuthor": "Autore",
"LabelAuthorFirstLast": "Autore (Per Nome)",
"LabelAuthorLastFirst": "Autori (Per Cognome)",
@@ -233,6 +244,7 @@
"LabelAutoRegister": "Auto Registrazione",
"LabelAutoRegisterDescription": "Crea automaticamente nuovi utenti dopo aver effettuato l'accesso",
"LabelBackToUser": "Torna a Utenti",
+ "LabelBackupAudioFiles": "Backup file Audio",
"LabelBackupLocation": "Percorso del Backup",
"LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico",
"LabelBackupsEnableAutomaticBackupsHelp": "I Backup saranno salvati in /metadata/backups",
@@ -241,18 +253,22 @@
"LabelBackupsNumberToKeep": "Numero di backup da mantenere",
"LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.",
"LabelBitrate": "Velocità di trasmissione",
+ "LabelBonus": "Bonus",
"LabelBooks": "Libri",
"LabelButtonText": "Buttone Testo",
"LabelByAuthor": "da {0}",
"LabelChangePassword": "Cambia Password",
"LabelChannels": "Canali",
+ "LabelChapterCount": "{0} Capitoli",
"LabelChapterTitle": "Titoli dei Capitoli",
"LabelChapters": "Capitoli",
"LabelChaptersFound": "Capitoli Trovati",
"LabelClickForMoreInfo": "Click per altre Info",
+ "LabelClickToUseCurrentValue": "Clicca per usare il valore corrente",
"LabelClosePlayer": "Chiudi player",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Comprimi Serie",
+ "LabelCollapseSubSeries": "Comprimi subserie",
"LabelCollection": "Raccolta",
"LabelCollections": "Raccolte",
"LabelComplete": "Completo",
@@ -293,19 +309,33 @@
"LabelEmailSettingsFromAddress": "Da Indirizzo",
"LabelEmailSettingsRejectUnauthorized": "Rifiuta i certificati non autorizzati",
"LabelEmailSettingsRejectUnauthorizedHelp": "La disattivazione della convalida del certificato SSL può esporre la tua connessione a rischi per la sicurezza, come attacchi man-in-the-middle. Disattiva questa opzione solo se ne comprendi le implicazioni e ti fidi del server di posta a cui ti stai connettendo.",
- "LabelEmailSettingsSecure": "Sicuro",
+ "LabelEmailSettingsSecure": "SSL",
"LabelEmailSettingsSecureHelp": "Se vero, la connessione utilizzerà TLS durante la connessione al server. Se false, viene utilizzato TLS se il server supporta l'estensione STARTTLS. Nella maggior parte dei casi impostare questo valore su true se ci si connette alla porta 465. Per la porta 587 o 25 mantenerlo false. (da nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Indirizzo di test",
"LabelEmbeddedCover": "Cover Integrata",
"LabelEnable": "Abilita",
+ "LabelEncodingBackupLocation": "il backup dei file audio verrà archiviato in:",
+ "LabelEncodingChaptersNotEmbedded": "Negli audiolibri multitraccia i capitoli non sono incorporati.",
+ "LabelEncodingClearItemCache": "Assicurati di svuotare periodicamente la cache degli oggetti.",
+ "LabelEncodingFinishedM4B": "L'M4B completato verrà inserito nella cartella:",
+ "LabelEncodingInfoEmbedded": "I metadati verranno incorporati nelle tracce audio all'interno della cartella dell'audiolibro.",
+ "LabelEncodingStartedNavigation": "Una volta avviata l'attività, è possibile uscire da questa pagina.",
+ "LabelEncodingTimeWarning": "La codifica può richiedere fino a 30 minuti.",
+ "LabelEncodingWarningAdvancedSettings": "Attenzione: non aggiornare queste impostazioni se non hai familiarità con le opzioni di codifica ffmpeg.",
+ "LabelEncodingWatcherDisabled": "Se hai disabilitato l'opzione Watcher, dovrai eseguire nuovamente la scansione dell'audiolibro in seguito.",
"LabelEnd": "Fine",
"LabelEndOfChapter": "Fine Capitolo",
"LabelEpisode": "Episodio",
+ "LabelEpisodeNotLinkedToRssFeed": "Episode non linkati nel RSS feed",
+ "LabelEpisodeNumber": "Episodio #{0}",
"LabelEpisodeTitle": "Titolo Episodio",
"LabelEpisodeType": "Tipo Episodio",
+ "LabelEpisodeUrlFromRssFeed": "URL dell'episodio dal RSS feed",
"LabelEpisodes": "Episodi",
+ "LabelEpisodic": "Episodico",
"LabelExample": "Esempio",
"LabelExpandSeries": "Espandi Serie",
+ "LabelExpandSubSeries": "Espandi Sub Serie",
"LabelExplicit": "Esplicito",
"LabelExplicitChecked": "Esplicito (selezionato)",
"LabelExplicitUnchecked": "Non Esplicito (selezionato)",
@@ -330,6 +360,7 @@
"LabelFontScale": "Dimensione font",
"LabelFontStrikethrough": "Barrato",
"LabelFormat": "Formato",
+ "LabelFull": "Pieno",
"LabelGenre": "Genere",
"LabelGenres": "Generi",
"LabelHardDeleteFile": "Elimina Definitivamente",
@@ -385,6 +416,10 @@
"LabelLowestPriority": "Priorità Minima",
"LabelMatchExistingUsersBy": "Abbina gli utenti esistenti per",
"LabelMatchExistingUsersByDescription": "Utilizzato per connettere gli utenti esistenti. Una volta connessi, gli utenti verranno abbinati a un ID univoco dal tuo provider SSO",
+ "LabelMaxEpisodesToDownload": "Max # di episodi da scaricare. Usa 0 per illimitati.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Massimo # di nuovi episodi da scaricare per il controllo",
+ "LabelMaxEpisodesToKeep": "Massimo # di episodi da tenere",
+ "LabelMaxEpisodesToKeepHelp": "Il valore 0 non imposta alcun limite massimo. Dopo che un nuovo episodio è stato scaricato automaticamente, questo eliminerà l'episodio più vecchio se hai più di X episodi. Questo eliminerà solo 1 episodio per ogni nuovo download.",
"LabelMediaPlayer": "Media Player",
"LabelMediaType": "Tipo Media",
"LabelMetaTag": "Meta Tag",
@@ -430,12 +465,14 @@
"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",
"LabelOverwrite": "Sovrascrivi",
+ "LabelPaginationPageXOfY": "Pagina {0} di {1}",
"LabelPassword": "Password",
"LabelPath": "Percorso",
"LabelPermanent": "Permanente",
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
"LabelPermissionsAccessAllTags": "Può accedere a tutti i tag",
"LabelPermissionsAccessExplicitContent": "Può accedere a contenuti espliciti",
+ "LabelPermissionsCreateEreader": "Può creare un e-reader",
"LabelPermissionsDelete": "Può Cancellare",
"LabelPermissionsDownload": "Può Scaricare",
"LabelPermissionsUpdate": "Può Aggiornare",
@@ -458,7 +495,9 @@
"LabelProviderAuthorizationValue": "Authorization Header Value",
"LabelPubDate": "Data di pubblicazione",
"LabelPublishYear": "Anno di pubblicazione",
- "LabelPublishedDate": "{0} pubblicati",
+ "LabelPublishedDate": "Pubblicati {0}",
+ "LabelPublishedDecade": "Decennio di pubblicazione",
+ "LabelPublishedDecades": "Decenni di pubblicazione",
"LabelPublisher": "Editore",
"LabelPublishers": "Editori",
"LabelRSSFeedCustomOwnerEmail": "E-mail del proprietario personalizzato",
@@ -478,21 +517,28 @@
"LabelRedo": "Rifai",
"LabelRegion": "Regione",
"LabelReleaseDate": "Data Release",
+ "LabelRemoveAllMetadataAbs": "Remuovi tutti i metadata.abs files",
+ "LabelRemoveAllMetadataJson": "Rimuovi tutti i metadata.json files",
"LabelRemoveCover": "Rimuovi cover",
+ "LabelRemoveMetadataFile": "Rimuovi i file metadata nella cartella della libreria",
+ "LabelRemoveMetadataFileHelp": "Rimuovi tutti i file metadata.json e i file metadata.abs nelle tue {0} cartelle.",
"LabelRowsPerPage": "Righe per pagina",
"LabelSearchTerm": "Ricerca",
"LabelSearchTitle": "Cerca Titolo",
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
"LabelSeason": "Stagione",
+ "LabelSeasonNumber": "Stagione #{0}",
"LabelSelectAll": "Seleziona tutto",
"LabelSelectAllEpisodes": "Seleziona tutti gli Episodi",
"LabelSelectEpisodesShowing": "Selezionati {0} episodi da visualizzare",
"LabelSelectUsers": "Selezione Utenti",
"LabelSendEbookToDevice": "Invia il libro a...",
"LabelSequence": "Sequenza",
+ "LabelSerial": "Seriale",
"LabelSeries": "Serie",
"LabelSeriesName": "Nome Serie",
"LabelSeriesProgress": "Cominciato",
+ "LabelServerLogLevel": "Server Log Level",
"LabelServerYearReview": "Anno del server in sintesi({0})",
"LabelSetEbookAsPrimary": "Imposta come primario",
"LabelSetEbookAsSupplementary": "Imposta come suplementare",
@@ -517,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.",
"LabelSettingsHomePageBookshelfView": "Home page con sfondo legno",
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "La percentuale di completamento è maggiore di",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Il tempo rimanente è inferiore a (secondi)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Contrassegna l'elemento multimediale come terminato quando",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Salta i libri precedenti nella serie Continua",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Lo scaffale della home page Continua serie mostra il primo libro non iniziato della serie che ha almeno un libro finito e nessun libro in corso. Abilitando questa impostazione le serie continueranno dal libro completato più lontano invece che dal primo libro non iniziato.",
"LabelSettingsParseSubtitles": "Analizza sottotitoli",
@@ -581,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} minuti",
"LabelTimeDurationXSeconds": "{0} secondi",
"LabelTimeInMinutes": "Tempo in minuti",
+ "LabelTimeLeft": "{0} sinistra",
"LabelTimeListened": "Tempo di Ascolto",
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
"LabelTimeRemaining": "{0} rimanente",
@@ -588,6 +638,7 @@
"LabelTitle": "Titolo",
"LabelToolsEmbedMetadata": "Incorpora Metadata",
"LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.",
+ "LabelToolsM4bEncoder": "M4B Encoder",
"LabelToolsMakeM4b": "Crea un file M4B",
"LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.",
"LabelToolsSplitM4b": "Converti M4B in MP3",
@@ -600,6 +651,7 @@
"LabelTracksMultiTrack": "Multi-traccia",
"LabelTracksNone": "Nessuna traccia",
"LabelTracksSingleTrack": "Traccia-singola",
+ "LabelTrailer": "Trailer",
"LabelType": "Tipo",
"LabelUnabridged": "Integrale",
"LabelUndo": "Annulla",
@@ -613,8 +665,10 @@
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
"LabelUploaderDropFiles": "Elimina file",
"LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
+ "LabelUseAdvancedOptions": "Usa le opzioni avanzate",
"LabelUseChapterTrack": "Usa il Capitolo della Traccia",
"LabelUseFullTrack": "Usa la traccia totale",
+ "LabelUseZeroForUnlimited": "Usa 0 per illimitato",
"LabelUser": "Utente",
"LabelUsername": "Nome utente",
"LabelValue": "Valore",
@@ -628,7 +682,7 @@
"LabelXBooks": "{0} libri",
"LabelXItems": "{0} oggetti",
"LabelYearReviewHide": "Nascondi Anno in rassegna",
- "LabelYearReviewShow": "Vedi Anno in rassegna",
+ "LabelYearReviewShow": "Mostra Anno in rassegna",
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
"LabelYourBookmarks": "I tuoi preferiti",
"LabelYourPlaylists": "le tue Playlist",
@@ -661,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "Sei sicuro/sicura di voler eliminare il fornitore di metadati personalizzato {0}?",
"MessageConfirmDeleteNotification": "Sei sicuro/sicura di voler eliminare questa notifica?",
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "Sei sicuro di voler incorporare i metadati nei file audio {0}?",
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
"MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?",
"MessageConfirmMarkAllEpisodesNotFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come non completati?",
@@ -672,6 +727,7 @@
"MessageConfirmPurgeCache": "L'eliminazione della cache eliminerà l'intera directory dei /metadata/cache
. Sei sicuro di voler rimuovere la directory della cache?",
"MessageConfirmPurgeItemsCache": "L'eliminazione della cache degli elementi eliminerà l'intera directory /metadata/cache/oggetti
. Sei sicuro?",
"MessageConfirmQuickEmbed": "Attenzione! L'incorporamento rapido non eseguirà il backup dei file audio. Assicurati di avere un backup dei tuoi file audio. Vuoi Continuare?",
+ "MessageConfirmQuickMatchEpisodes": "Gli episodi di corrispondenza rapida sovrascriveranno i dettagli se viene trovata una corrispondenza. Saranno aggiornati solo gli episodi non corrispondenti. Sei sicuro?",
"MessageConfirmReScanLibraryItems": "Sei sicuro di voler ripetere la scansione? {0} oggetti?",
"MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?",
"MessageConfirmRemoveAuthor": "Sei sicuro di voler rimuovere l'autore? \"{0}\"?",
@@ -679,6 +735,7 @@
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
"MessageConfirmRemoveListeningSessions": "Sei sicuro di voler rimuovere {0} sessioni di Ascolto?",
+ "MessageConfirmRemoveMetadataFiles": "Vuoi davvero rimuovere tutti i metadati.{0} file nelle cartelle degli elementi della tua libreria?",
"MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
@@ -694,6 +751,7 @@
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
"MessageEmbedFailed": "Incorporamento non riuscito!",
"MessageEmbedFinished": "Incorporamento finito!",
+ "MessageEmbedQueue": "In coda per l'incorporamento dei metadati ({0} in coda)",
"MessageEpisodesQueuedForDownload": "{0} episodio(i) in coda per lo scaricamento",
"MessageEreaderDevices": "Per garantire la consegna dei libri digitali, potrebbe essere necessario aggiungere l'indirizzo e-mail sopra indicato come mittente valido per ciascun dispositivo elencato di seguito.",
"MessageFeedURLWillBe": "l’URL del flusso sarà {0}",
@@ -721,7 +779,7 @@
"MessageNoBackups": "Nessun Backup",
"MessageNoBookmarks": "Nessun preferito",
"MessageNoChapters": "Nessun capitolo",
- "MessageNoCollections": "Nessuna Raccolta",
+ "MessageNoCollections": "Nessuna Collezione",
"MessageNoCoversFound": "Nessuna Cover Trovata",
"MessageNoDescription": "Nessuna descrizione",
"MessageNoDevices": "nessun dispositivo",
@@ -738,6 +796,7 @@
"MessageNoLogs": "Nessun Logs",
"MessageNoMediaProgress": "Nessun progresso multimediale",
"MessageNoNotifications": "Nessuna notifica",
+ "MessageNoPodcastFeed": "Podcast non valido: nessun feed",
"MessageNoPodcastsFound": "Nessun podcast trovato",
"MessageNoResults": "Nessun Risultato",
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
@@ -754,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Crea playlist da una Raccolta",
"MessagePleaseWait": "Attendi...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
+ "MessagePodcastSearchField": "Inserisci il termine di ricerca o l'URL del feed RSS",
+ "MessageQuickEmbedInProgress": "Incorporamento rapido in corso",
+ "MessageQuickEmbedQueue": "In coda per incorporamento rapido ({0} in coda)",
+ "MessageQuickMatchAllEpisodes": "Associamento veloce di Tutti gli episodi",
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
"MessageRemoveChapter": "Rimuovi Capitolo",
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
@@ -771,6 +834,41 @@
"MessageShareExpiresIn": "Scade in {0}",
"MessageShareURLWillBe": "L'indirizzo sarà: {0} ",
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
+ "MessageTaskAudioFileNotWritable": "Il file audio «{0}» non è scrivibile",
+ "MessageTaskCanceledByUser": "Attività annullata dall'utente",
+ "MessageTaskDownloadingEpisodeDescription": "Scaricamento dell'episodio «{0}»",
+ "MessageTaskEmbeddingMetadata": "Metadati integrati",
+ "MessageTaskEmbeddingMetadataDescription": "Integrazione dei metadati nell'audiolibro «{0}»",
+ "MessageTaskEncodingM4b": "Codifica M4B",
+ "MessageTaskEncodingM4bDescription": "Codifica dell'audiolibro «{0}» in un singolo file m4b",
+ "MessageTaskFailed": "Fallimento",
+ "MessageTaskFailedToBackupAudioFile": "Non riuscita a eseguire il backup del file audio «{0}»",
+ "MessageTaskFailedToCreateCacheDirectory": "Non riuscita a creare la cartella della cache",
+ "MessageTaskFailedToEmbedMetadataInFile": "Non ha inserito i metadati nel file «{0}»",
+ "MessageTaskFailedToMergeAudioFiles": "Non è riuscito a fondere i file audio",
+ "MessageTaskFailedToMoveM4bFile": "Non è riuscito a spostare il file m4b",
+ "MessageTaskFailedToWriteMetadataFile": "Non è riuscito a scrivere file di metadati",
+ "MessageTaskMatchingBooksInLibrary": "Libri di corrispondenza in biblioteca «{0}»",
+ "MessageTaskNoFilesToScan": "Nessun file per la scansione",
+ "MessageTaskOpmlImport": "Importazione OPML",
+ "MessageTaskOpmlImportDescription": "Creazione di podcast da {0} flusso RSS",
+ "MessageTaskOpmlImportFeed": "Flusso di importazione OPML",
+ "MessageTaskOpmlImportFeedDescription": "Importazione del flusso RSS «{0}»",
+ "MessageTaskOpmlImportFeedFailed": "Impossibile ottenere il flusso del podcast",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Creazione di podcast «{0}»",
+ "MessageTaskOpmlImportFeedPodcastExists": "Il podcast esiste già nel percorso",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Errore durante la creazione del podcast",
+ "MessageTaskOpmlImportFinished": "{0} podcast aggiunti",
+ "MessageTaskOpmlParseFailed": "Impossibile analizzare il file OPML",
+ "MessageTaskOpmlParseFastFail": "File OPML non valido. Tag non trovato OPPURE non è stato trovato un tag ",
+ "MessageTaskOpmlParseNoneFound": "Nessun feed trovato nel file OPML",
+ "MessageTaskScanItemsAdded": "{0} aggiunti",
+ "MessageTaskScanItemsMissing": "{0} mancanti",
+ "MessageTaskScanItemsUpdated": "{0} aggiornati",
+ "MessageTaskScanNoChangesNeeded": "Nessuna modifica necessaria",
+ "MessageTaskScanningFileChanges": "Cambiamenti di file di scansione in «{0}»",
+ "MessageTaskScanningLibrary": "Scansione della biblioteca «{0}»",
+ "MessageTaskTargetDirectoryNotWritable": "La cartella di destinazione non è scrivibile",
"MessageThinking": "Elaborazione...",
"MessageUploaderItemFailed": "Caricamento Fallito",
"MessageUploaderItemSuccess": "Caricato con successo!",
@@ -788,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Le cartelle con file multimediali verranno gestite come elementi della libreria separati.",
"NoteUploaderOnlyAudioFiles": "Se carichi solo file audio, ogni file audio verrà gestito come un audiolibro separato.",
"NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.",
+ "NotificationOnBackupCompletedDescription": "Attivato al completamento di un backup",
+ "NotificationOnBackupFailedDescription": "Attivato quando un backup fallisce",
+ "NotificationOnEpisodeDownloadedDescription": "Attivato quando un episodio di podcast viene scaricato automaticamente",
+ "NotificationOnTestDescription": "test il sistema di notifica",
"PlaceholderNewCollection": "Nome Nuova Raccolta",
"PlaceholderNewFolderPath": "Nuovo Percorso Cartella",
"PlaceholderNewPlaylist": "Nome nuova playlist",
@@ -798,27 +900,26 @@
"StatsBooksAdditional": "Alcune aggiunte includono…",
"StatsBooksFinished": "Libri Finiti",
"StatsBooksFinishedThisYear": "Alcuni libri terminati quest'anno…",
- "StatsBooksListenedTo": "libri ascoltati",
- "StatsCollectionGrewTo": "La tua collezione di libri è cresciuta fino a…",
- "StatsSessions": "sessioni",
- "StatsSpentListening": "trascorso ad ascoltare",
+ "StatsBooksListenedTo": "Libri ascoltati",
+ "StatsCollectionGrewTo": "La tua collezione è aumentata di…",
+ "StatsSessions": "Sessioni",
+ "StatsSpentListening": "Tempo di Ascolto",
"StatsTopAuthor": "MIGLIOR AUTORE",
- "StatsTopAuthors": "MIGLIORI AUTORI",
+ "StatsTopAuthors": "AUTORI MIGLIORI",
"StatsTopGenre": "MIGLIOR GENERE",
- "StatsTopGenres": "MIGLIORI GENERI",
+ "StatsTopGenres": "GENERI MIGLIORI",
"StatsTopMonth": "MIGLIOR MESE",
"StatsTopNarrator": "MIGLIOR NARRATORE",
- "StatsTopNarrators": "MIGLIORI NARRATORI",
- "StatsTotalDuration": "Con una durata totale di…",
+ "StatsTopNarrators": "NARRATORI MIGLIORI",
+ "StatsTotalDuration": "Per una durata totale di…",
"StatsYearInReview": "ANNO IN RASSEGNA",
- "ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
"ToastAccountUpdateSuccess": "Account Aggiornato",
"ToastAppriseUrlRequired": "È necessario immettere un indirizzo Apprise",
+ "ToastAsinRequired": "L'ASIN è obbligatorio",
"ToastAuthorImageRemoveSuccess": "Immagine Autore Rimossa",
"ToastAuthorNotFound": "Autore\"{0}\" non trovato",
"ToastAuthorRemoveSuccess": "Autore rimosso",
"ToastAuthorSearchNotFound": "Autore non trovato",
- "ToastAuthorUpdateFailed": "Aggiornamento Autore Fallito",
"ToastAuthorUpdateMerged": "Autore unito",
"ToastAuthorUpdateSuccess": "Autore aggiornato",
"ToastAuthorUpdateSuccessNoImageFound": "Autore aggiornato (nessuna immagine trovata)",
@@ -829,29 +930,29 @@
"ToastBackupDeleteSuccess": "backup Eliminato",
"ToastBackupInvalidMaxKeep": "Numero non valido di backup da conservare",
"ToastBackupInvalidMaxSize": "Dimensione massima del backup non valida",
- "ToastBackupPathUpdateFailed": "Impossibile aggiornare il percorso di backup",
"ToastBackupRestoreFailed": "Ripristino fallito",
"ToastBackupUploadFailed": "Caricamento backup fallito",
"ToastBackupUploadSuccess": "Backup caricato",
"ToastBatchDeleteFailed": "Eliminazione batch non riuscita",
"ToastBatchDeleteSuccess": "Eliminazione batch riuscita",
+ "ToastBatchQuickMatchFailed": "Batch Quick Match non riuscito!",
+ "ToastBatchQuickMatchStarted": "Avviata la ricerca rapida in batch di {0} libri!",
"ToastBatchUpdateFailed": "Batch di aggiornamento fallito",
"ToastBatchUpdateSuccess": "Batch di aggiornamento finito",
"ToastBookmarkCreateFailed": "Creazione segnalibro fallita",
"ToastBookmarkCreateSuccess": "Segnalibro creato",
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
- "ToastBookmarkUpdateFailed": "Aggiornamento segnalibro fallito",
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
"ToastCachePurgeFailed": "Impossibile eliminare la cache",
"ToastCachePurgeSuccess": "Cache eliminata correttamente",
"ToastChaptersHaveErrors": "I capitoli contengono errori",
"ToastChaptersMustHaveTitles": "I capitoli devono avere titoli",
"ToastChaptersRemoved": "Capitoli rimossi",
+ "ToastChaptersUpdated": "Capitoli aggiornati",
"ToastCollectionItemsAddFailed": "l'aggiunta dell'elemento(i) alla raccolta non è riuscito",
"ToastCollectionItemsAddSuccess": "L'aggiunta dell'elemento(i) alla raccolta è riuscito",
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla Raccolta",
"ToastCollectionRemoveSuccess": "Collezione rimossa",
- "ToastCollectionUpdateFailed": "Errore aggiornamento Raccolta",
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
"ToastDeleteFileFailed": "Impossibile eliminare il file",
@@ -860,49 +961,113 @@
"ToastDeviceNameAlreadyExists": "Esiste già un dispositivo e-reader con quel nome",
"ToastDeviceTestEmailFailed": "Impossibile inviare l'e-mail di prova",
"ToastDeviceTestEmailSuccess": "Test invio mail completato",
+ "ToastEmailSettingsUpdateSuccess": "Impostazioni e-mail aggiornate",
+ "ToastEncodeCancelFailed": "Impossibile annullare la codifica",
+ "ToastEncodeCancelSucces": "Codifica annullata",
+ "ToastEpisodeDownloadQueueClearFailed": "Impossibile cancellare la coda",
+ "ToastEpisodeDownloadQueueClearSuccess": "Coda di download degli episodi cancellata",
+ "ToastEpisodeUpdateSuccess": "{0} episodi aggiornati",
"ToastErrorCannotShare": "Impossibile condividere in modo nativo su questo dispositivo",
"ToastFailedToLoadData": "Impossibile caricare i dati",
- "ToastItemCoverUpdateFailed": "Errore Aggiornamento cover",
+ "ToastFailedToMatch": "Impossibile abbinare",
+ "ToastFailedToShare": "Impossibile condividere",
+ "ToastFailedToUpdate": "Non aggiornato",
+ "ToastInvalidImageUrl": "URL dell'immagine non valido",
+ "ToastInvalidMaxEpisodesToDownload": "Numero massimo di episodi non valido da scaricare",
+ "ToastInvalidUrl": "URL non valido",
"ToastItemCoverUpdateSuccess": "Cover aggiornata",
- "ToastItemDetailsUpdateFailed": "Errore Aggiornamento dettagli file",
+ "ToastItemDeletedFailed": "Impossibile eliminare l'elemento",
+ "ToastItemDeletedSuccess": "Elemento eliminato",
"ToastItemDetailsUpdateSuccess": "Dettagli file Aggiornata",
"ToastItemMarkedAsFinishedFailed": "Errore nel segnare il file come finito",
"ToastItemMarkedAsFinishedSuccess": "File segnato come finito",
"ToastItemMarkedAsNotFinishedFailed": "Errore nel segnare il file come non completo",
"ToastItemMarkedAsNotFinishedSuccess": "File segnato come non completo",
+ "ToastItemUpdateSuccess": "Articolo aggiornato",
"ToastLibraryCreateFailed": "Errore creazione libreria",
"ToastLibraryCreateSuccess": "Libreria \"{0}\" creata",
"ToastLibraryDeleteFailed": "Errore cancellazione libreria",
"ToastLibraryDeleteSuccess": "Libreria Cancellata",
"ToastLibraryScanFailedToStart": "Errore inizio scansione",
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
- "ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
+ "ToastMatchAllAuthorsFailed": "Tutti gli autori non sono potuti essere classificati",
+ "ToastMetadataFilesRemovedError": "Errore durante la rimozione dei metadati. {0} file",
+ "ToastMetadataFilesRemovedNoneFound": "Nessun metadato. {0} file trovati nella libreria",
+ "ToastMetadataFilesRemovedNoneRemoved": "Nessun metadato. {0} file rimossi",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadati.{1} file rimossi",
+ "ToastMustHaveAtLeastOnePath": "Deve avere almeno un percorso",
+ "ToastNameEmailRequired": "Nome ed email sono obbligatori",
+ "ToastNameRequired": "Il nome è obbligatorio",
+ "ToastNewEpisodesFound": "{0} nuovi episodi trovati",
+ "ToastNewUserCreatedFailed": "Impossibile creare l'account: \"{0}\"",
+ "ToastNewUserCreatedSuccess": "Nuovo account creato",
+ "ToastNewUserLibraryError": "È necessario selezionare almeno una libreria",
+ "ToastNewUserPasswordError": "Deve avere una password, solo l'utente root può avere una password vuota",
+ "ToastNewUserTagError": "Devi selezionare almeno un tag",
+ "ToastNewUserUsernameError": "Inserisci un nome utente",
+ "ToastNoNewEpisodesFound": "Nessun nuovo episodio trovato",
+ "ToastNoUpdatesNecessary": "Nessun aggiornamento necessario",
+ "ToastNotificationCreateFailed": "Impossibile creare la notifica",
+ "ToastNotificationDeleteFailed": "Impossibile eliminare la notifica",
+ "ToastNotificationFailedMaximum": "Il numero massimo di tentativi falliti deve essere >= 0",
+ "ToastNotificationQueueMaximum": "La coda di notifica massima deve essere >= 0",
+ "ToastNotificationSettingsUpdateSuccess": "Impostazioni di notifica aggiornate",
+ "ToastNotificationTestTriggerFailed": "Impossibile attivare la notifica del test",
+ "ToastNotificationTestTriggerSuccess": "Notifica di test attivata",
+ "ToastNotificationUpdateSuccess": "Notifica aggiornata",
"ToastPlaylistCreateFailed": "Errore creazione playlist",
"ToastPlaylistCreateSuccess": "Playlist creata",
"ToastPlaylistRemoveSuccess": "Playlist rimossa",
- "ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
"ToastPodcastCreateFailed": "Errore creazione podcast",
"ToastPodcastCreateSuccess": "Podcast creato correttamente",
+ "ToastPodcastGetFeedFailed": "Impossibile ottenere il feed del podcast",
+ "ToastPodcastNoEpisodesInFeed": "Nessun episodio trovato nel feed RSS",
+ "ToastPodcastNoRssFeed": "Il podcast non ha un feed RSS",
+ "ToastProgressIsNotBeingSynced": "L'avanzamento non è sincronizzato, riavviare la riproduzione",
+ "ToastProviderCreatedFailed": "Impossibile aggiungere il provider",
+ "ToastProviderCreatedSuccess": "Aggiunto nuovo provider",
+ "ToastProviderNameAndUrlRequired": "Nome e URL richiesti",
+ "ToastProviderRemoveSuccess": "Provider rimosso",
"ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS",
"ToastRSSFeedCloseSuccess": "Flusso RSS chiuso",
+ "ToastRemoveFailed": "Impossibile rimuovere",
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
+ "ToastRemoveItemsWithIssuesFailed": "Impossibile rimuovere gli elementi della libreria con problemi",
+ "ToastRemoveItemsWithIssuesSuccess": "Rimossi gli elementi della libreria con problemi",
+ "ToastRenameFailed": "Impossibile rinominare",
+ "ToastRescanFailed": "Nuova scansione non riuscita per {0}",
+ "ToastRescanRemoved": "L'articolo completo di Re-Scan è stato rimosso",
+ "ToastRescanUpToDate": "La nuova scansione dell'articolo completo è stata aggiornata",
+ "ToastRescanUpdated": "L'articolo completo di Re-Scan è stato aggiornato",
+ "ToastScanFailed": "Impossibile eseguire la scansione dell'elemento della libreria",
+ "ToastSelectAtLeastOneUser": "Seleziona almeno un utente",
"ToastSendEbookToDeviceFailed": "Impossibile inviare il libro al dispositivo",
"ToastSendEbookToDeviceSuccess": "Libro inviato al dispositivo «{0}»",
"ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito",
"ToastSeriesUpdateSuccess": "Serie Aggiornate",
- "ToastServerSettingsUpdateFailed": "Impossibile aggiornare le impostazioni del server",
"ToastServerSettingsUpdateSuccess": "Impostazioni del server aggiornate",
+ "ToastSessionCloseFailed": "Disconnessione Fallita",
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
"ToastSessionDeleteSuccess": "Sessione cancellata",
+ "ToastSleepTimerDone": "Timer di spegnimento eseguito... zZzzZz",
+ "ToastSlugMustChange": "Lo slug contiene caratteri non validi",
+ "ToastSlugRequired": "È richiesto lo slug",
"ToastSocketConnected": "Socket connesso",
"ToastSocketDisconnected": "Socket disconnesso",
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
"ToastSortingPrefixesEmptyError": "Deve avere almeno 1 prefisso di ordinamento",
- "ToastSortingPrefixesUpdateFailed": "Impossibile aggiornare i prefissi di ordinamento",
"ToastSortingPrefixesUpdateSuccess": "Prefissi di ordinamento aggiornati ({0} items)",
+ "ToastTitleRequired": "Il titolo è obbligatorio",
+ "ToastUnknownError": "Errore sconosciuto",
+ "ToastUnlinkOpenIdFailed": "Impossibile scollegare l'utente da OpenID",
+ "ToastUnlinkOpenIdSuccess": "Utente scollegato da OpenID",
"ToastUserDeleteFailed": "Errore eliminazione utente",
- "ToastUserDeleteSuccess": "Utente eliminato"
+ "ToastUserDeleteSuccess": "Utente eliminato",
+ "ToastUserPasswordChangeSuccess": "Password modificata con successo",
+ "ToastUserPasswordMismatch": "Le password non corrispondono",
+ "ToastUserPasswordMustChange": "La nuova password non può corrispondere alla vecchia password",
+ "ToastUserRootRequireName": "È necessario immettere un nome utente root"
}
diff --git a/client/strings/lt.json b/client/strings/lt.json
index f9b765d459..9fe65e3afb 100644
--- a/client/strings/lt.json
+++ b/client/strings/lt.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Pasirinkite failus",
"ButtonClearFilter": "Valyti filtrą",
"ButtonCloseFeed": "Uždaryti srautą",
+ "ButtonCloseSession": "Uždaryti Atidarytą sesiją",
"ButtonCollections": "Kolekcijos",
"ButtonConfigureScanner": "Konfigūruoti skenerį",
"ButtonCreate": "Kurti",
@@ -28,11 +29,14 @@
"ButtonEdit": "Redaguoti",
"ButtonEditChapters": "Redaguoti skyrius",
"ButtonEditPodcast": "Redaguoti tinklalaidę",
+ "ButtonEnable": "Įjungti",
"ButtonForceReScan": "Priverstinai nuskaityti iš naujo",
"ButtonFullPath": "Visas kelias",
"ButtonHide": "Slėpti",
"ButtonHome": "Pradžia",
"ButtonIssues": "Problemos",
+ "ButtonJumpBackward": "Peršokti atgal",
+ "ButtonJumpForward": "Peršokti į priekį",
"ButtonLatest": "Naujausias",
"ButtonLibrary": "Biblioteka",
"ButtonLogout": "Atsijungti",
@@ -42,12 +46,19 @@
"ButtonMatchAllAuthors": "Pritaikyti visus autorius",
"ButtonMatchBooks": "Pritaikyti knygas",
"ButtonNevermind": "Nesvarbu",
+ "ButtonNext": "Kitas",
"ButtonNextChapter": "Kitas Skyrius",
+ "ButtonNextItemInQueue": "Kitas eilėje",
+ "ButtonOk": "Ok",
"ButtonOpenFeed": "Atidaryti srautą",
"ButtonOpenManager": "Atidaryti tvarkyklę",
+ "ButtonPause": "Pauzė",
"ButtonPlay": "Groti",
+ "ButtonPlayAll": "Groti Visus",
"ButtonPlaying": "Grojama",
"ButtonPlaylists": "Grojaraščiai",
+ "ButtonPrevious": "Praeitas",
+ "ButtonPreviousChapter": "Praeitas Skyrius",
"ButtonPurgeAllCache": "Valyti visą saugyklą",
"ButtonPurgeItemsCache": "Valyti elementų saugyklą",
"ButtonQueueAddItem": "Pridėti į eilę",
@@ -55,6 +66,9 @@
"ButtonQuickMatch": "Greitas pritaikymas",
"ButtonReScan": "Iš naujo nuskaityti",
"ButtonRead": "Skaityti",
+ "ButtonReadLess": "Mažiau",
+ "ButtonReadMore": "Daugiau",
+ "ButtonRefresh": "Atnaujinti",
"ButtonRemove": "Pašalinti",
"ButtonRemoveAll": "Pašalinti viską",
"ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus",
@@ -72,12 +86,15 @@
"ButtonSelectFolderPath": "Pasirinkti aplanko kelią",
"ButtonSeries": "Serijos",
"ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių",
+ "ButtonShare": "Dalintis",
"ButtonShiftTimes": "Perstumti laikus",
"ButtonShow": "Rodyti",
"ButtonStartM4BEncode": "Pradėti M4B kodavimą",
"ButtonStartMetadataEmbed": "Pradėti metaduomenų įterpimą",
+ "ButtonStats": "Statistika",
"ButtonSubmit": "Pateikti",
"ButtonTest": "Testuoti",
+ "ButtonUnlinkOpenId": "Atsieti OpenID",
"ButtonUpload": "Įkelti",
"ButtonUploadBackup": "Įkelti atsarginę kopiją",
"ButtonUploadCover": "Įkelti viršelį",
@@ -86,11 +103,15 @@
"ButtonUserEdit": "Redaguoti naudotoją {0}",
"ButtonViewAll": "Peržiūrėti visus",
"ButtonYes": "Taip",
+ "ErrorUploadFetchMetadataAPI": "Klaida gaunant metaduomenis",
+ "ErrorUploadFetchMetadataNoResults": "Nepavyko gauti metaduomenų - pabandykite atnaujinti pavadinimą ir/ar autorių.",
+ "ErrorUploadLacksTitle": "Pavadinimas yra privalomas",
"HeaderAccount": "Paskyra",
"HeaderAdvanced": "Papildomi",
"HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai",
"HeaderAudioTracks": "Garso takeliai",
"HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai",
+ "HeaderAuthentication": "Autentifikacija",
"HeaderBackups": "Atsarginės kopijos",
"HeaderChangePassword": "Pakeisti slaptažodį",
"HeaderChapters": "Skyriai",
@@ -99,6 +120,7 @@
"HeaderCollectionItems": "Kolekcijos elementai",
"HeaderCover": "Viršelis",
"HeaderCurrentDownloads": "Dabartiniai parsisiuntimai",
+ "HeaderCustomMessageOnLogin": "Pritaikyta prisijungimo žinutė",
"HeaderDetails": "Detalės",
"HeaderDownloadQueue": "Parsisiuntimo eilė",
"HeaderEbookFiles": "Eknygos failai",
@@ -189,7 +211,7 @@
"LabelBackToUser": "Grįžti į naudotoją",
"LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą",
"LabelBackupsEnableAutomaticBackupsHelp": "Atsarginės kopijos bus išsaugotos /metadata/backups aplanke",
- "LabelBackupsMaxBackupSize": "Maksimalus atsarginių kopijų dydis (GB)",
+ "LabelBackupsMaxBackupSize": "Maksimalus atsarginių kopijų dydis (GB) (0 - neribotai)",
"LabelBackupsMaxBackupSizeHelp": "Jei konfigūruotas dydis viršijamas, atsarginės kopijos nebus sukurtos, kad būtų išvengta klaidingų konfigūracijų.",
"LabelBackupsNumberToKeep": "Laikytinų atsarginių kopijų skaičius",
"LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.",
@@ -397,7 +419,7 @@
"LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai",
"LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.",
"LabelSettingsFindCovers": "Rasti viršelius",
- "LabelSettingsFindCoversHelp": "Jei jūsų audioknyga neturi įterpto viršelio arba viršelio paveikslėlio aplanko, skeneris bandys rasti viršelį. Pastaba: Tai padidins skenavimo trukmę.",
+ "LabelSettingsFindCoversHelp": "Jei jūsų audioknyga neturi įterpto viršelio arba viršelio paveikslėlio aplanke, bandyti rasti viršelį. Pastaba: Tai padidins skenavimo trukmę.",
"LabelSettingsHideSingleBookSeries": "Slėpti serijas, turinčias tik vieną knygą",
"LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.",
"LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą",
@@ -413,7 +435,7 @@
"LabelSettingsSquareBookCovers": "Naudoti kvadratinius knygos viršelius",
"LabelSettingsSquareBookCoversHelp": "Naudoti kvadratinius viršelius vietoj standartinių 1.6:1 knygų viršelių",
"LabelSettingsStoreCoversWithItem": "Saugoti viršelius su elementu",
- "LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas „cover“ pavadinimo failas.",
+ "LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas failas su \"cover\" pavadinimu.",
"LabelSettingsStoreMetadataWithItem": "Saugoti metaduomenis su elementu",
"LabelSettingsStoreMetadataWithItemHelp": "Pagal nutylėjimą metaduomenų failai saugomi /metadata/items aplanke, įjungus šią parinktį metaduomenų failai bus saugomi jūsų bibliotekos elemento aplanke",
"LabelSettingsTimeFormat": "Laiko formatas",
@@ -622,10 +644,8 @@
"PlaceholderNewPlaylist": "Naujas grojaraščio pavadinimas",
"PlaceholderSearch": "Ieškoti..",
"PlaceholderSearchEpisode": "Ieškoti epizodo..",
- "ToastAccountUpdateFailed": "Paskyros atnaujinimas nepavyko",
"ToastAccountUpdateSuccess": "Paskyra atnaujinta",
"ToastAuthorImageRemoveSuccess": "Autoriaus paveiksliukas pašalintas",
- "ToastAuthorUpdateFailed": "Nepavyko atnaujinti autoriaus",
"ToastAuthorUpdateMerged": "Autorius sujungtas",
"ToastAuthorUpdateSuccess": "Autorius atnaujintas",
"ToastAuthorUpdateSuccessNoImageFound": "Autorius atnaujintas (paveiksliukas nerastas)",
@@ -641,17 +661,20 @@
"ToastBookmarkCreateFailed": "Žymos sukurti nepavyko",
"ToastBookmarkCreateSuccess": "Žyma pridėta",
"ToastBookmarkRemoveSuccess": "Žyma pašalinta",
- "ToastBookmarkUpdateFailed": "Žymos atnaujinti nepavyko",
"ToastBookmarkUpdateSuccess": "Žyma atnaujinta",
"ToastChaptersHaveErrors": "Skyriai turi klaidų",
"ToastChaptersMustHaveTitles": "Skyriai turi turėti pavadinimus",
+ "ToastChaptersRemoved": "Skyriai pašalinti",
+ "ToastCollectionItemsAddFailed": "Nepavyko pridėti į kolekciją",
+ "ToastCollectionItemsAddSuccess": "Pridėta į kolekciją",
"ToastCollectionItemsRemoveSuccess": "Elementai pašalinti iš kolekcijos",
"ToastCollectionRemoveSuccess": "Kolekcija pašalinta",
- "ToastCollectionUpdateFailed": "Kolekcijos atnaujinti nepavyko",
"ToastCollectionUpdateSuccess": "Kolekcija atnaujinta",
- "ToastItemCoverUpdateFailed": "Elemento viršelio atnaujinti nepavyko",
+ "ToastCoverUpdateFailed": "Viršelio atnaujinimas nepavyko",
+ "ToastDeviceTestEmailSuccess": "Bandomasis el. laiškas išsiųstas",
"ToastItemCoverUpdateSuccess": "Elemento viršelis atnaujintas",
- "ToastItemDetailsUpdateFailed": "Elemento detalių atnaujinti nepavyko",
+ "ToastItemDeletedFailed": "Nepavyko ištrinti",
+ "ToastItemDeletedSuccess": "Ištrinta",
"ToastItemDetailsUpdateSuccess": "Elemento detalės atnaujintos",
"ToastItemMarkedAsFinishedFailed": "Pažymėti kaip Baigta nepavyko",
"ToastItemMarkedAsFinishedSuccess": "Elementas pažymėtas kaip Baigta",
@@ -663,12 +686,10 @@
"ToastLibraryDeleteSuccess": "Biblioteka ištrinta",
"ToastLibraryScanFailedToStart": "Nepavyko pradėti bibliotekos skenavimo",
"ToastLibraryScanStarted": "Bibliotekos skenavimas pradėtas",
- "ToastLibraryUpdateFailed": "Bibliotekos atnaujinti nepavyko",
"ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" atnaujinta",
"ToastPlaylistCreateFailed": "Grojaraščio sukurti nepavyko",
"ToastPlaylistCreateSuccess": "Grojaraštis sukurtas",
"ToastPlaylistRemoveSuccess": "Grojaraštis pašalintas",
- "ToastPlaylistUpdateFailed": "Grojaraščio atnaujinti nepavyko",
"ToastPlaylistUpdateSuccess": "Grojaraštis atnaujintas",
"ToastPodcastCreateFailed": "Tinklalaidės sukurti nepavyko",
"ToastPodcastCreateSuccess": "Tinklalaidė sėkmingai sukurta",
diff --git a/client/strings/nl.json b/client/strings/nl.json
index 41fd8ef687..bc5a40ca25 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Bestanden kiezen",
"ButtonClearFilter": "Filter verwijderen",
"ButtonCloseFeed": "Feed sluiten",
+ "ButtonCloseSession": "Sluit Sessie",
"ButtonCollections": "Collecties",
"ButtonConfigureScanner": "Configureer scanner",
"ButtonCreate": "Creëer",
@@ -28,9 +29,13 @@
"ButtonEdit": "Wijzig",
"ButtonEditChapters": "Hoofdstukken wijzigen",
"ButtonEditPodcast": "Podcast wijzigen",
+ "ButtonEnable": "Aanzetten",
+ "ButtonFireAndFail": "Fire and Fail",
+ "ButtonFireOnTest": "Fire onTest event",
"ButtonForceReScan": "Forceer nieuwe scan",
"ButtonFullPath": "Volledig pad",
"ButtonHide": "Verberg",
+ "ButtonHome": "Thuis",
"ButtonIssues": "Problemen",
"ButtonJumpBackward": "Spring achteruit",
"ButtonJumpForward": "Spring vooruit",
@@ -45,18 +50,24 @@
"ButtonNevermind": "Laat maar",
"ButtonNext": "Volgende",
"ButtonNextChapter": "Volgend hoofdstuk",
+ "ButtonNextItemInQueue": "Volgend Item in Wachtrij",
+ "ButtonOk": "Ok",
"ButtonOpenFeed": "Feed openen",
"ButtonOpenManager": "Manager openen",
"ButtonPause": "Pauze",
"ButtonPlay": "Afspelen",
+ "ButtonPlayAll": "Alles Afspelen",
"ButtonPlaying": "Speelt",
"ButtonPlaylists": "Afspeellijsten",
"ButtonPrevious": "Vorige",
"ButtonPreviousChapter": "Vorig hoofdstuk",
+ "ButtonProbeAudioFile": "Onderzoek Audio Bestand",
"ButtonPurgeAllCache": "Volledige cache legen",
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
"ButtonQueueAddItem": "In wachtrij zetten",
"ButtonQueueRemoveItem": "Uit wachtrij verwijderen",
+ "ButtonQuickEmbed": "Snel Embedden",
+ "ButtonQuickEmbedMetadata": "Snel Metadata Insluiten",
"ButtonQuickMatch": "Snelle match",
"ButtonReScan": "Nieuwe scan",
"ButtonRead": "Lees",
@@ -69,20 +80,28 @@
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
"ButtonRemoveFromContinueReading": "Verwijder van Verder luisteren",
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
+ "ButtonReset": "Opnieuw Instellen",
+ "ButtonResetToDefault": "Standaardwaarden Terugzetten",
"ButtonRestore": "Herstel",
"ButtonSave": "Opslaan",
"ButtonSaveAndClose": "Opslaan & sluiten",
"ButtonSaveTracklist": "Afspeellijst opslaan",
+ "ButtonScan": "Scannen",
"ButtonScanLibrary": "Scan bibliotheek",
"ButtonSearch": "Zoeken",
"ButtonSelectFolderPath": "Maplocatie selecteren",
+ "ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
"ButtonShare": "Deel",
"ButtonShiftTimes": "Tijden verschuiven",
"ButtonShow": "Toon",
"ButtonStartM4BEncode": "Start M4B-encoding",
"ButtonStartMetadataEmbed": "Start insluiten metadata",
+ "ButtonStats": "Statistieken",
"ButtonSubmit": "Indienen",
+ "ButtonTest": "Testen",
+ "ButtonUnlinkOpenId": "OpenID Ontkoppelen",
+ "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload back-up",
"ButtonUploadCover": "Upload cover",
"ButtonUploadOPMLFile": "Upload OPML-bestand",
@@ -93,10 +112,13 @@
"ErrorUploadFetchMetadataAPI": "Error metadata ophalen",
"ErrorUploadFetchMetadataNoResults": "Kan metadata niet ophalen - probeer de titel en/of auteur te updaten",
"ErrorUploadLacksTitle": "Moet een titel hebben",
+ "HeaderAccount": "Account",
+ "HeaderAddCustomMetadataProvider": "Aangepaste Metadataprovider Toevoegen",
"HeaderAdvanced": "Geavanceerd",
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
"HeaderAudioTracks": "Audiotracks",
"HeaderAudiobookTools": "Audioboekbestandbeheer tools",
+ "HeaderAuthentication": "Authenticatie",
"HeaderBackups": "Back-ups",
"HeaderChangePassword": "Wachtwoord wijzigen",
"HeaderChapters": "Hoofdstukken",
@@ -105,6 +127,9 @@
"HeaderCollectionItems": "Collectie-objecten",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Huidige downloads",
+ "HeaderCustomMessageOnLogin": "Aangepast Bericht bij Aanmelden",
+ "HeaderCustomMetadataProviders": "Aangepaste Metadata Providers",
+ "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download-wachtrij",
"HeaderEbookFiles": "Ebook bestanden",
"HeaderEmail": "E-mail",
@@ -124,16 +149,27 @@
"HeaderLibraryStats": "Bibliotheekstatistieken",
"HeaderListeningSessions": "Luistersessies",
"HeaderListeningStats": "Luisterstatistieken",
+ "HeaderLogin": "Aanmelden",
+ "HeaderLogs": "Logboek",
"HeaderManageGenres": "Genres beheren",
"HeaderManageTags": "Tags beheren",
+ "HeaderMapDetails": "Map details",
+ "HeaderMatch": "Vergelijken",
+ "HeaderMetadataOrderOfPrecedence": "Metadata volgorde",
"HeaderMetadataToEmbed": "In te sluiten metadata",
"HeaderNewAccount": "Nieuwe account",
"HeaderNewLibrary": "Nieuwe bibliotheek",
+ "HeaderNotificationCreate": "Notificatie Aanmaken",
+ "HeaderNotificationUpdate": "Update Notificatie",
"HeaderNotifications": "Notificaties",
+ "HeaderOpenIDConnectAuthentication": "OpenID Connect Authenticatie",
+ "HeaderOpenListeningSessions": "Open Luistersessies",
"HeaderOpenRSSFeed": "Open RSS-feed",
"HeaderOtherFiles": "Andere bestanden",
+ "HeaderPasswordAuthentication": "Wachtwoord Authenticatie",
"HeaderPermissions": "Toestemmingen",
"HeaderPlayerQueue": "Afspeelwachtrij",
+ "HeaderPlayerSettings": "Speler Instellingen",
"HeaderPlaylist": "Afspeellijst",
"HeaderPlaylistItems": "Onderdelen in afspeellijst",
"HeaderPodcastsToAdd": "Toe te voegen podcasts",
@@ -145,6 +181,7 @@
"HeaderRemoveEpisodes": "Verwijder {0} afleveringen",
"HeaderSavedMediaProgress": "Opgeslagen mediavoortgang",
"HeaderSchedule": "Schema",
+ "HeaderScheduleEpisodeDownloads": "Automatische afleveringsdownloads plannen",
"HeaderScheduleLibraryScans": "Schema automatische bibliotheekscans",
"HeaderSession": "Sessie",
"HeaderSetBackupSchedule": "Kies schema voor back-up",
@@ -152,6 +189,7 @@
"HeaderSettingsDisplay": "Toon",
"HeaderSettingsExperimental": "Experimentele functies",
"HeaderSettingsGeneral": "Algemeen",
+ "HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Slaaptimer",
"HeaderStatsLargestItems": "Grootste items",
"HeaderStatsLongestItems": "Langste items (uren)",
@@ -160,13 +198,18 @@
"HeaderStatsTop10Authors": "Top 10 auteurs",
"HeaderStatsTop5Genres": "Top 5 genres",
"HeaderTableOfContents": "Inhoudsopgave",
+ "HeaderTools": "Gereedschap",
"HeaderUpdateAccount": "Account bijwerken",
"HeaderUpdateAuthor": "Auteur bijwerken",
"HeaderUpdateDetails": "Details bijwerken",
"HeaderUpdateLibrary": "Bibliotheek bijwerken",
"HeaderUsers": "Gebruikers",
+ "HeaderYearReview": "Jaar {0} in Review",
"HeaderYourStats": "Je statistieken",
"LabelAbridged": "Verkort",
+ "LabelAbridgedChecked": "Verkort (gechecked)",
+ "LabelAbridgedUnchecked": "Onverkort (niet gechecked)",
+ "LabelAccessibleBy": "Toegankelijk door",
"LabelAccountType": "Accounttype",
"LabelAccountTypeAdmin": "Beheerder",
"LabelAccountTypeGuest": "Gast",
@@ -177,39 +220,63 @@
"LabelAddToPlaylist": "Toevoegen aan afspeellijst",
"LabelAddToPlaylistBatch": "{0} onderdelen toevoegen aan afspeellijst",
"LabelAddedAt": "Toegevoegd op",
+ "LabelAddedDate": "Toegevoegd {0}",
+ "LabelAdminUsersOnly": "Enkel Admin gebruikers",
"LabelAll": "Alle",
"LabelAllUsers": "Alle gebruikers",
+ "LabelAllUsersExcludingGuests": "Alle gebruikers exclusief gasten",
+ "LabelAllUsersIncludingGuests": "Alle gebruikers inclusief gasten",
"LabelAlreadyInYourLibrary": "Reeds in je bibliotheek",
+ "LabelApiToken": "API Token",
"LabelAppend": "Achteraan toevoegen",
+ "LabelAudioBitrate": "Audio Bitrate (b.v. 128k)",
+ "LabelAudioChannels": "Audio Kanalen (1 of 2)",
+ "LabelAudioCodec": "Audio Codec",
"LabelAuthor": "Auteur",
"LabelAuthorFirstLast": "Auteur (Voornaam Achternaam)",
"LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)",
"LabelAuthors": "Auteurs",
"LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden",
+ "LabelAutoFetchMetadata": "Automatisch Metadata Ophalen",
+ "LabelAutoFetchMetadataHelp": "Haalt metadata op voor titel, auteur en serie om het uploaden te stroomlijnen. Aanvullende metadata moet mogelijk worden gematcht na het uploaden.",
+ "LabelAutoLaunch": "Automatisch Openen",
+ "LabelAutoLaunchDescription": "Automatisch doorverwijzen naar de auth-provider bij het navigeren naar de inlogpagina (handmatig pad /login?autoLaunch=0
)",
+ "LabelAutoRegister": "Automatisch Registreren",
+ "LabelAutoRegisterDescription": "Automatisch nieuwe gebruikers aanmaken na inloggen",
"LabelBackToUser": "Terug naar gebruiker",
+ "LabelBackupAudioFiles": "Back-up audiobestanden",
"LabelBackupLocation": "Back-up locatie",
"LabelBackupsEnableAutomaticBackups": "Automatische back-ups inschakelen",
"LabelBackupsEnableAutomaticBackupsHelp": "Back-ups opgeslagen in /metadata/backups",
- "LabelBackupsMaxBackupSize": "Maximale back-up-grootte (in GB)",
+ "LabelBackupsMaxBackupSize": "Maximale back-up-grootte (in GB) (0 voor ongelimiteerd)",
"LabelBackupsMaxBackupSizeHelp": "Als een beveiliging tegen verkeerde instelling, zullen back-up mislukken als ze de ingestelde grootte overschrijden.",
"LabelBackupsNumberToKeep": "Aantal te bewaren back-ups",
"LabelBackupsNumberToKeepHelp": "Er wordt slechts 1 back-up per keer verwijderd, dus als je reeds meer back-ups dan dit hebt moet je ze handmatig verwijderen.",
+ "LabelBitrate": "Bitrate",
+ "LabelBonus": "Bonus",
"LabelBooks": "Boeken",
+ "LabelButtonText": "Knop Tekst",
+ "LabelByAuthor": "Door {0}",
"LabelChangePassword": "Wachtwoord wijzigen",
"LabelChannels": "Kanalen",
+ "LabelChapterCount": "{0} Hoofdstukken",
"LabelChapterTitle": "Hoofdstuktitel",
"LabelChapters": "Hoofdstukken",
"LabelChaptersFound": "Hoofdstukken gevonden",
"LabelClickForMoreInfo": "Klik voor meer informatie",
+ "LabelClickToUseCurrentValue": "Klik om huidige waarde te gebruiken",
"LabelClosePlayer": "Sluit speler",
+ "LabelCodec": "Codec",
"LabelCollapseSeries": "Series inklappen",
+ "LabelCollapseSubSeries": "Subserie samenvouwen",
"LabelCollection": "Collectie",
"LabelCollections": "Collecties",
"LabelComplete": "Compleet",
"LabelConfirmPassword": "Bevestig wachtwoord",
- "LabelContinueListening": "Verder luisteren",
- "LabelContinueReading": "Verder luisteren",
- "LabelContinueSeries": "Ga verder met serie",
+ "LabelContinueListening": "Verder Luisteren",
+ "LabelContinueReading": "Verder lezen",
+ "LabelContinueSeries": "Doorgaan met Serie",
+ "LabelCover": "Omslag",
"LabelCoverImageURL": "Coverafbeelding URL",
"LabelCreatedAt": "Gecreëerd op",
"LabelCronExpression": "Cron-uitdrukking",
@@ -218,33 +285,68 @@
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
"LabelDatetime": "Datum-tijd",
"LabelDays": "Dagen",
+ "LabelDeleteFromFileSystemCheckbox": "Verwijderen uit bestandssysteem (uncheck om alleen uit database te verwijderen)",
"LabelDescription": "Beschrijving",
"LabelDeselectAll": "Deselecteer alle",
"LabelDevice": "Apparaat",
"LabelDeviceInfo": "Apparaat info",
+ "LabelDeviceIsAvailableTo": "Apparaat is beschikbaar voor...",
"LabelDirectory": "Map",
"LabelDiscFromFilename": "Schijf uit bestandsnaam",
"LabelDiscFromMetadata": "Schijf uit metadata",
- "LabelDiscover": "Ontdek",
+ "LabelDiscover": "Ontdekken",
+ "LabelDownload": "Download",
+ "LabelDownloadNEpisodes": "Download {0} afleveringen",
"LabelDuration": "Duur",
+ "LabelDurationComparisonExactMatch": "(exacte overeenkomst)",
+ "LabelDurationComparisonLonger": "({0} langer)",
+ "LabelDurationComparisonShorter": "({0} korter)",
"LabelDurationFound": "Gevonden duur:",
+ "LabelEbook": "Ebook",
+ "LabelEbooks": "Eboeken",
"LabelEdit": "Wijzig",
+ "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Van-adres",
+ "LabelEmailSettingsRejectUnauthorized": "Ongeautoriseerde certificaten afwijzen",
+ "LabelEmailSettingsRejectUnauthorizedHelp": "Het uitschakelen van SSL-certificaatvalidatie kan uw verbinding blootstellen aan beveiligingsrisico's, zoals man-in-the-middle-aanvallen. Schakel deze optie alleen uit als u de implicaties begrijpt en de mailserver waarmee u verbinding maakt vertrouwt.",
"LabelEmailSettingsSecure": "Veilig",
"LabelEmailSettingsSecureHelp": "Als 'waar', dan gebruikt de verbinding TLS om met de server te verbinden. Als 'onwaar', dan wordt TLS gebruikt als de server de STARTTLS-extensie ondersteunt. In de meeste gevallen kies je voor 'waar' verbindt met poort 465. Voo poort 587 of 25, laat op 'onwaar'. (van nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test-adres",
"LabelEmbeddedCover": "Ingesloten cover",
"LabelEnable": "Inschakelen",
+ "LabelEncodingBackupLocation": "Er wordt een back-up van uw originele audiobestanden opgeslagen in:",
+ "LabelEncodingChaptersNotEmbedded": "Hoofdstukken zijn niet ingesloten in audioboeken met meerdere sporen.",
+ "LabelEncodingClearItemCache": "Zorg ervoor dat u de cache van items regelmatig wist.",
+ "LabelEncodingFinishedM4B": "Een voltooide M4B wordt in uw audioboekfolder geplaatst in:",
+ "LabelEncodingInfoEmbedded": "Metagegevens worden ingesloten in de audiotracks in uw audioboekmap.",
+ "LabelEncodingStartedNavigation": "Eenmaal de taak is gestart kan u weg navigeren van deze pagina.",
+ "LabelEncodingTimeWarning": "Encoding kan tot 30 minuten duren.",
+ "LabelEncodingWarningAdvancedSettings": "Waarschuwing: update deze instellingen niet tenzij u bekend bent met de coderingsopties van ffmpeg.",
+ "LabelEncodingWatcherDisabled": "Als u de watcher hebt uitgeschakeld, moet u het audioboek daarna opnieuw scannen.",
"LabelEnd": "Einde",
+ "LabelEndOfChapter": "Einde van het Hoofdstuk",
"LabelEpisode": "Aflevering",
+ "LabelEpisodeNotLinkedToRssFeed": "Aflevering niet gelinkt aan RSS feed",
+ "LabelEpisodeNumber": "Aflevering #{0}",
"LabelEpisodeTitle": "Afleveringtitel",
"LabelEpisodeType": "Afleveringtype",
+ "LabelEpisodeUrlFromRssFeed": "Aflevering URL van RSS feed",
+ "LabelEpisodes": "Afleveringen",
+ "LabelEpisodic": "Episodisch",
"LabelExample": "Voorbeeld",
+ "LabelExpandSeries": "Serie Uitvouwen",
+ "LabelExpandSubSeries": "Subserie Uitvouwen",
"LabelExplicit": "Expliciet",
+ "LabelExplicitChecked": "Expliciet (gechecked)",
+ "LabelExplicitUnchecked": "Niet Expliciet (niet gechecked)",
+ "LabelExportOPML": "OPML exporteren",
+ "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Metadata ophalen",
"LabelFile": "Bestand",
"LabelFileBirthtime": "Aanmaaktijd bestand",
+ "LabelFileBornDate": "Geboren {0}",
"LabelFileModified": "Bestand gewijzigd",
+ "LabelFileModifiedDate": "Gewijzigd {0}",
"LabelFilename": "Bestandsnaam",
"LabelFilterByUser": "Filter op gebruiker",
"LabelFindEpisodes": "Zoek afleveringen",
@@ -252,18 +354,29 @@
"LabelFolder": "Map",
"LabelFolders": "Mappen",
"LabelFontBold": "Vetgedrukt",
+ "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Lettertypefamilie",
+ "LabelFontItalic": "Cursief",
"LabelFontScale": "Lettertype schaal",
+ "LabelFontStrikethrough": "Doorgestreept",
"LabelFormat": "Formaat",
+ "LabelFull": "Vol",
+ "LabelGenre": "Genre",
+ "LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard-delete bestand",
- "LabelHasEbook": "Heeft ebook",
- "LabelHasSupplementaryEbook": "Heeft supplementair ebook",
+ "LabelHasEbook": "Heeft Ebook",
+ "LabelHasSupplementaryEbook": "Heeft aanvullend Ebook",
+ "LabelHideSubtitles": "Ondertitels Verstoppen",
+ "LabelHighestPriority": "Hoogste Prioriteit",
+ "LabelHost": "Host",
"LabelHour": "Uur",
"LabelHours": "Uren",
"LabelIcon": "Icoon",
+ "LabelImageURLFromTheWeb": "Afbeelding URL van web",
"LabelInProgress": "Bezig",
"LabelIncludeInTracklist": "Includeer in tracklijst",
"LabelIncomplete": "Incompleet",
+ "LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Aangepast dagelijks/wekelijks",
"LabelIntervalEvery12Hours": "Iedere 12 uur",
"LabelIntervalEvery15Minutes": "Iedere 15 minuten",
@@ -274,32 +387,52 @@
"LabelIntervalEveryHour": "Ieder uur",
"LabelInvert": "Omdraaien",
"LabelItem": "Onderdeel",
+ "LabelJumpBackwardAmount": "Terugspoelen hoeveelheid",
+ "LabelJumpForwardAmount": "Vooruitspoelen hoeveelheid",
"LabelLanguage": "Taal",
"LabelLanguageDefaultServer": "Standaard servertaal",
+ "LabelLanguages": "Talen",
"LabelLastBookAdded": "Laatst toegevoegde boek",
"LabelLastBookUpdated": "Laatst bijgewerkte boek",
"LabelLastSeen": "Laatst gezien",
"LabelLastTime": "Laatste keer",
"LabelLastUpdate": "Laatste update",
+ "LabelLayout": "Layout",
"LabelLayoutSinglePage": "Enkele pagina",
"LabelLayoutSplitPage": "Gesplitste pagina",
"LabelLess": "Minder",
"LabelLibrariesAccessibleToUser": "Voor gebruiker toegankelijke bibliotheken",
"LabelLibrary": "Bibliotheek",
+ "LabelLibraryFilterSublistEmpty": "Nee {0}",
"LabelLibraryItem": "Bibliotheekonderdeel",
"LabelLibraryName": "Bibliotheeknaam",
"LabelLimit": "Limiet",
"LabelLineSpacing": "Regelruimte",
- "LabelListenAgain": "Luister opnieuw",
+ "LabelListenAgain": "Opnieuw Beluisteren",
+ "LabelLogLevelDebug": "Debug",
+ "LabelLogLevelInfo": "Informatie",
"LabelLogLevelWarn": "Waarschuwing",
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
+ "LabelLowestPriority": "Laagste Prioriteit",
+ "LabelMatchExistingUsersBy": "Bestaande gebruikers matchen op",
+ "LabelMatchExistingUsersByDescription": "Wordt gebruikt om bestaande gebruikers te verbinden. Zodra ze verbonden zijn, worden gebruikers gekoppeld aan een unieke id van uw SSO-provider.",
+ "LabelMaxEpisodesToDownload": "Maximale # afleveringen om te downloaden. Gebruik 0 voor ongelimiteerd.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Maximale # nieuwe afleveringen om te downloaden per check",
+ "LabelMaxEpisodesToKeep": "Maximale # afleveringen om te houden",
+ "LabelMaxEpisodesToKeepHelp": "Waarde van 0 stelt geen maximumlimiet in. Nadat een nieuwe aflevering automatisch is gedownload, wordt de oudste aflevering verwijderd als u meer dan X afleveringen hebt. Hiermee wordt slechts 1 aflevering per nieuwe download verwijderd.",
"LabelMediaPlayer": "Mediaspeler",
"LabelMediaType": "Mediatype",
"LabelMetaTag": "Meta-tag",
"LabelMetaTags": "Meta-tags",
+ "LabelMetadataOrderOfPrecedenceDescription": "Metadatabronnen met een hogere prioriteit zullen metadatabronnen met een lagere prioriteit overschrijven",
"LabelMetadataProvider": "Metadatabron",
"LabelMinute": "Minuut",
+ "LabelMinutes": "Minuten",
"LabelMissing": "Ontbrekend",
+ "LabelMissingEbook": "Heeft geen ebook",
+ "LabelMissingSupplementaryEbook": "Heeft geen supplementair ebook",
+ "LabelMobileRedirectURIs": "Toegestane mobiele omleidings-URL's",
+ "LabelMobileRedirectURIsDescription": "Dit is een whitelist met geldige redirect-URI's voor mobiele apps. De standaard is audiobookshelf://oauth
, die u kunt verwijderen of aanvullen met extra URI's voor integratie met apps van derden. Als u een asterisk (*
) als enige invoer gebruikt, is elke URI toegestaan.",
"LabelMore": "Meer",
"LabelMoreInfo": "Meer info",
"LabelName": "Naam",
@@ -307,14 +440,16 @@
"LabelNarrators": "Vertellers",
"LabelNew": "Nieuw",
"LabelNewPassword": "Nieuw wachtwoord",
- "LabelNewestAuthors": "Nieuwste auteurs",
- "LabelNewestEpisodes": "Nieuwste afleveringen",
+ "LabelNewestAuthors": "Nieuwste Auteurs",
+ "LabelNewestEpisodes": "Nieuwste Afleveringen",
"LabelNextBackupDate": "Volgende back-up datum",
"LabelNextScheduledRun": "Volgende geplande run",
+ "LabelNoCustomMetadataProviders": "Geen custom metadata bronnen",
"LabelNoEpisodesSelected": "Geen afleveringen geselecteerd",
"LabelNotFinished": "Niet Voltooid",
"LabelNotStarted": "Niet Gestart",
"LabelNotes": "Notities",
+ "LabelNotificationAppriseURL": "URL(s) van kennisgeving",
"LabelNotificationAvailableVariables": "Beschikbare variabelen",
"LabelNotificationBodyTemplate": "Body-template",
"LabelNotificationEvent": "Notificatie gebeurtenis",
@@ -325,10 +460,15 @@
"LabelNotificationsMaxQueueSizeHelp": "Gebeurtenissen zijn beperkt tot 1 aftrap per seconde. Gebeurtenissen zullen genegeerd worden als de rij aan de maximale grootte zit. Dit voorkomt notificatie-spamming.",
"LabelNumberOfBooks": "Aantal Boeken",
"LabelNumberOfEpisodes": "# afleveringen",
+ "LabelOpenIDAdvancedPermsClaimDescription": "Naam van de OpenID-claim die geavanceerde machtigingen bevat voor gebruikersacties binnen de applicatie die van toepassing zijn op niet-beheerdersrollen (indien geconfigureerd ). Als de claim ontbreekt in het antwoord, wordt toegang tot ABS geweigerd. Als er één optie ontbreekt, wordt deze behandeld als false
. Zorg ervoor dat de claim van de identiteitsprovider overeenkomt met de verwachte structuur:",
+ "LabelOpenIDClaims": "Laat de volgende opties leeg om geavanceerde groeps- en machtigingstoewijzing uit te schakelen en de groep 'Gebruiker' automatisch toe te wijzen.",
+ "LabelOpenIDGroupClaimDescription": "Naam van de OpenID-claim die een lijst met de groepen van de gebruiker bevat. Vaak aangeduid als groepen
. Indien geconfigureerd , zal de applicatie automatisch rollen toewijzen op basis van de groepslidmaatschappen van de gebruiker, op voorwaarde dat deze groepen hoofdlettergevoelig 'admin', 'gebruiker' of 'gast' worden genoemd in de claim. De claim moet een lijst bevatten en als een gebruiker tot meerdere groepen behoort, zal de applicatie de rol toewijzen die overeenkomt met het hoogste toegangsniveau. Als er geen groep overeenkomt, wordt de toegang geweigerd.",
"LabelOpenRSSFeed": "Open RSS-feed",
"LabelOverwrite": "Overschrijf",
+ "LabelPaginationPageXOfY": "Pagina {0} van {1}",
"LabelPassword": "Wachtwoord",
"LabelPath": "Pad",
+ "LabelPermanent": "Permanent",
"LabelPermissionsAccessAllLibraries": "Heeft toegang tot all bibliotheken",
"LabelPermissionsAccessAllTags": "Heeft toegang tot alle tags",
"LabelPermissionsAccessExplicitContent": "Heeft toegang tot expliciete inhoud",
@@ -336,51 +476,75 @@
"LabelPermissionsDownload": "Kan downloaden",
"LabelPermissionsUpdate": "Kan bijwerken",
"LabelPermissionsUpload": "Kan uploaden",
+ "LabelPersonalYearReview": "Jouw jaar in review ({0})",
"LabelPhotoPathURL": "Foto pad/URL",
"LabelPlayMethod": "Afspeelwijze",
+ "LabelPlayerChapterNumberMarker": "{0} van {1}",
"LabelPlaylists": "Afspeellijsten",
+ "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcast zoekregio",
"LabelPodcastType": "Podcasttype",
+ "LabelPodcasts": "Podcasts",
"LabelPort": "Poort",
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
"LabelPreventIndexing": "Voorkom indexering van je feed door iTunes- en Google podcastmappen",
"LabelPrimaryEbook": "Primair ebook",
"LabelProgress": "Voortgang",
"LabelProvider": "Bron",
+ "LabelProviderAuthorizationValue": "Autorisatie Header Waarde",
"LabelPubDate": "Publicatiedatum",
"LabelPublishYear": "Jaar van uitgave",
+ "LabelPublishedDate": "Gepubliceerd {0}",
+ "LabelPublishedDecade": "Gepubliceerd Decennium",
+ "LabelPublishedDecades": "Gepubliceerd Decennia",
"LabelPublisher": "Uitgever",
+ "LabelPublishers": "Uitgevers",
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
"LabelRSSFeedOpen": "RSS-feed open",
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
"LabelRSSFeedSlug": "RSS-feed slug",
"LabelRSSFeedURL": "RSS-feed URL",
+ "LabelRandomly": "Willekeurig",
+ "LabelReAddSeriesToContinueListening": "Serie opnieuw toevoegen aan verder luisteren",
"LabelRead": "Lees",
- "LabelReadAgain": "Lees opnieuw",
+ "LabelReadAgain": "Opnieuw Lezen",
"LabelReadEbookWithoutProgress": "Lees ebook zonder voortgang bij te houden",
- "LabelRecentSeries": "Recente series",
- "LabelRecentlyAdded": "Recent toegevoegd",
+ "LabelRecentSeries": "Recente Serie",
+ "LabelRecentlyAdded": "Recent Toegevoegd",
"LabelRecommended": "Aangeraden",
+ "LabelRedo": "Opnieuw",
"LabelRegion": "Regio",
"LabelReleaseDate": "Verschijningsdatum",
+ "LabelRemoveAllMetadataAbs": "Verwijder alle metadata.abs bestanden",
+ "LabelRemoveAllMetadataJson": "Verwijder alle metadata.json bestanden",
"LabelRemoveCover": "Verwijder cover",
+ "LabelRemoveMetadataFile": "Verwijder metadata bestanden in bibliotheek item folders",
+ "LabelRemoveMetadataFileHelp": "Verwijder alle metadata.json en metadata.abs bestanden in uw {0} folders.",
+ "LabelRowsPerPage": "Rijen per pagina",
"LabelSearchTerm": "Zoekterm",
"LabelSearchTitle": "Zoek titel",
"LabelSearchTitleOrASIN": "Zoek titel of ASIN",
"LabelSeason": "Seizoen",
+ "LabelSeasonNumber": "Seizoen #{0}",
+ "LabelSelectAll": "Alles selecteren",
"LabelSelectAllEpisodes": "Selecteer alle afleveringen",
"LabelSelectEpisodesShowing": "Selecteer {0} afleveringen laten zien",
+ "LabelSelectUsers": "Selecteer gebruikers",
"LabelSendEbookToDevice": "Stuur ebook naar...",
"LabelSequence": "Sequentie",
+ "LabelSerial": "Serie",
"LabelSeries": "Serie",
"LabelSeriesName": "Naam serie",
"LabelSeriesProgress": "Voortgang serie",
+ "LabelServerLogLevel": "Server Log Niveau",
+ "LabelServerYearReview": "Server Jaar in Review ({0})",
"LabelSetEbookAsPrimary": "Stel in als primair",
"LabelSetEbookAsSupplementary": "Stel in als supplementair",
"LabelSettingsAudiobooksOnly": "Alleen audiobooks",
"LabelSettingsAudiobooksOnlyHelp": "Deze instelling inschakelen zorgt ervoor dat ebook-bestanden genegeerd worden tenzij ze in een audiobook-map staan, in welk geval ze worden ingesteld als supplementaire ebooks",
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
+ "LabelSettingsChromecastSupport": "Chromecast ondersteuning",
"LabelSettingsDateFormat": "Datum format",
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
@@ -388,6 +552,8 @@
"LabelSettingsEnableWatcher": "Watcher inschakelen",
"LabelSettingsEnableWatcherForLibrary": "Map-watcher voor bibliotheek inschakelen",
"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.",
"LabelSettingsExperimentalFeatures": "Experimentele functies",
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
"LabelSettingsFindCovers": "Zoek covers",
@@ -396,6 +562,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series die slechts een enkel boek bevatten worden verborgen op de seriespagina en de homepagina-planken.",
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
"LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek",
+ "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Sla eedere boeken in Serie Verderzetten over",
+ "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "De Continue Series home page shelf toont het eerste boek dat nog niet is begonnen in series waarvan er minstens één is voltooid en er geen boeken in uitvoering zijn. Als u deze instelling inschakelt, wordt de serie voortgezet vanaf het boek dat het verst is voltooid in plaats van het eerste boek dat nog niet is begonnen.",
"LabelSettingsParseSubtitles": "Parseer subtitel",
"LabelSettingsParseSubtitlesHelp": "Haal subtitels uit mapnaam van audioboek. Subtitel moet gescheiden zijn met \" - \" b.v. \"Boektitel - Een Subtitel Hier\" heeft als subtitel \"Een Subtitel Hier\"",
"LabelSettingsPreferMatchedMetadata": "Prefereer gematchte metadata",
@@ -411,9 +579,16 @@
"LabelSettingsStoreMetadataWithItem": "Bewaar metadata bij onderdeel",
"LabelSettingsStoreMetadataWithItemHelp": "Standaard worden metadata-bestanden bewaard in /metadata/items, door deze instelling in te schakelen zullen metadata bestanden in de map van je bibliotheekonderdeel bewaard worden",
"LabelSettingsTimeFormat": "Tijdformat",
+ "LabelShare": "Delen",
+ "LabelShareOpen": "Delen Open",
+ "LabelShareURL": "URL Delen",
"LabelShowAll": "Toon alle",
+ "LabelShowSeconds": "Laat seconden zien",
+ "LabelShowSubtitles": "Laat Ondertitels zien",
"LabelSize": "Grootte",
"LabelSleepTimer": "Slaaptimer",
+ "LabelSlug": "Slak",
+ "LabelStart": "Start",
"LabelStartTime": "Starttijd",
"LabelStarted": "Gestart",
"LabelStartedAt": "Gestart op",
@@ -434,13 +609,24 @@
"LabelStatsWeekListening": "Week luisterend",
"LabelSubtitle": "Subtitel",
"LabelSupportedFileTypes": "Ondersteunde bestandstypes",
+ "LabelTag": "Tag",
+ "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker",
"LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker",
"LabelTasks": "Lopende taken",
+ "LabelTextEditorBulletedList": "Opgesomde lijst",
+ "LabelTextEditorLink": "Link",
+ "LabelTextEditorNumberedList": "Genummerde lijst",
+ "LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Thema",
"LabelThemeDark": "Donker",
"LabelThemeLight": "Licht",
"LabelTimeBase": "Tijdsbasis",
+ "LabelTimeDurationXHours": "{0} Uren",
+ "LabelTimeDurationXMinutes": "{0} minuten",
+ "LabelTimeDurationXSeconds": "{0} seconden",
+ "LabelTimeInMinutes": "Tijd in minuten",
+ "LabelTimeLeft": "{0} over",
"LabelTimeListened": "Tijd geluisterd",
"LabelTimeListenedToday": "Tijd geluisterd vandaag",
"LabelTimeRemaining": "{0} te gaan",
@@ -448,6 +634,7 @@
"LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Metadata insluiten",
"LabelToolsEmbedMetadataDescription": "Metadata insluiten in audiobestanden, inclusief coverafbeelding en hoofdstukken.",
+ "LabelToolsM4bEncoder": "M4B Encoder",
"LabelToolsMakeM4b": "Maak M4B-audioboekbestand",
"LabelToolsMakeM4bDescription": "Genereer een .M4B-audioboekbestand met ingesloten metadata, coverafbeelding en hoofdstukken.",
"LabelToolsSplitM4b": "Splitst M4B in MP3's",
@@ -456,11 +643,16 @@
"LabelTotalTimeListened": "Totale tijd geluisterd",
"LabelTrackFromFilename": "Track vanuit bestandsnaam",
"LabelTrackFromMetadata": "Track vanuit metadata",
+ "LabelTracks": "Audiosporen",
+ "LabelTracksMultiTrack": "Multi-spoor",
"LabelTracksNone": "Geen tracks",
"LabelTracksSingleTrack": "Enkele track",
+ "LabelTrailer": "Trailer",
+ "LabelType": "Type",
"LabelUnabridged": "Onverkort",
"LabelUndo": "Ongedaan maken",
"LabelUnknown": "Onbekend",
+ "LabelUnknownPublishDate": "Onbekende uitgeefdatum",
"LabelUpdateCover": "Cover bijwerken",
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
"LabelUpdateDetails": "Details bijwerken",
@@ -468,16 +660,25 @@
"LabelUpdatedAt": "Bijgewerkt op",
"LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen",
"LabelUploaderDropFiles": "Bestanden neerzetten",
+ "LabelUploaderItemFetchMetadataHelp": "Automatisch titel, auteur en serie ophalen",
+ "LabelUseAdvancedOptions": "Gebruik Geavanceerde Instellingen",
"LabelUseChapterTrack": "Gebruik hoofdstuktrack",
"LabelUseFullTrack": "Gebruik volledige track",
+ "LabelUseZeroForUnlimited": "Gebruik 0 voor ongelimiteerd",
"LabelUser": "Gebruiker",
"LabelUsername": "Gebruikersnaam",
"LabelValue": "Waarde",
"LabelVersion": "Versie",
"LabelViewBookmarks": "Bekijk boekwijzers",
"LabelViewChapters": "Bekijk hoofdstukken",
+ "LabelViewPlayerSettings": "Laat spelerinstellingen zien",
"LabelViewQueue": "Bekijk afspeelwachtrij",
+ "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdagen om te draaien",
+ "LabelXBooks": "{0} boeken",
+ "LabelXItems": "{0} items",
+ "LabelYearReviewHide": "Verberg Jaar in Review",
+ "LabelYearReviewShow": "Laat Jaar in Review zien",
"LabelYourAudiobookDuration": "Je audioboekduur",
"LabelYourBookmarks": "Je boekwijzers",
"LabelYourPlaylists": "Je afspeellijsten",
@@ -485,10 +686,14 @@
"MessageAddToPlayerQueue": "Toevoegen aan wachtrij",
"MessageAppriseDescription": "Om deze functie te gebruiken heb je een draaiende instantie van Apprise API nodig of een api die dezelfde requests afhandelt. De Apprise API Url moet het volledige URL-pad zijn om de notificatie te verzenden, b.v., als je API-instantie draait op http://192.168.1.1:8337
dan zou je http://192.168.1.1:8337/notify
gebruiken.",
"MessageBackupsDescription": "Back-ups omvatten gebruikers, gebruikers' voortgang, bibliotheekonderdeeldetails, serverinstellingen en afbeeldingen bewaard in /metadata/items
& /metadata/authors
. Back-ups bevatten niet de bestanden bewaard in je bibliotheekmappen.",
+ "MessageBackupsLocationEditNote": "Let op: het bijwerken van de back-uplocatie zal bestaande back-ups niet verplaatsen of wijzigen",
+ "MessageBackupsLocationNoEditNote": "Let op: De back-uplocatie wordt ingesteld via een omgevingsvariabele en kan hier niet worden gewijzigd.",
+ "MessageBackupsLocationPathEmpty": "Backup locatie pad kan niet leeg zijn",
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
+ "MessageBookshelfNoResultsForQuery": "Geen resultaten voor query",
"MessageBookshelfNoSeries": "Je hebt geen series",
"MessageChapterEndIsAfter": "Hoofdstukeinde is na het einde van je audioboek",
"MessageChapterErrorFirstNotZero": "Eerste hoofdstuk moet starten op 0",
@@ -496,21 +701,37 @@
"MessageChapterErrorStartLtPrev": "Ongeldig: starttijd moet be groter zijn dan of equal aan starttijd van vorig hoofdstuk",
"MessageChapterStartIsAfter": "Start van hoofdstuk is na het einde van je audioboek",
"MessageCheckingCron": "Cron aan het checken...",
+ "MessageConfirmCloseFeed": "Ben je zeker dat je deze feed wil sluiten?",
"MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?",
+ "MessageConfirmDeleteDevice": "Ben je zeker dat je e-reader apparaat \"{0}\" wil verwijderen?",
"MessageConfirmDeleteFile": "Dit verwijdert het bestand uit het bestandssysteem. Weet je het zeker?",
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
+ "MessageConfirmDeleteLibraryItem": "Hiermee wordt het bibliotheekitem uit de database en uw bestandssysteem verwijderd. Bent u zeker?",
+ "MessageConfirmDeleteLibraryItems": "Hiermee worden {0} bibliotheekitems uit de database en uw bestandssysteem verwijderd. Bent u zeker?",
+ "MessageConfirmDeleteMetadataProvider": "Weet u zeker dat u de aangepaste metadataprovider \"{0}\" wilt verwijderen?",
+ "MessageConfirmDeleteNotification": "Weet u zeker dat u deze melding wil verwijderen?",
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "Weet u zeker dat u metagegevens wilt insluiten in {0} audiobestanden?",
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
"MessageConfirmMarkAllEpisodesFinished": "Weet je zeker dat je alle afleveringen als voltooid wil markeren?",
"MessageConfirmMarkAllEpisodesNotFinished": "Weet je zeker dat je alle afleveringen als niet-voltooid wil markeren?",
+ "MessageConfirmMarkItemFinished": "Weet u zeker dat u \"{0}\" als voltooid wilt markeren?",
+ "MessageConfirmMarkItemNotFinished": "Weet u zeker dat u \"{0}\" als niet voltooid wilt markeren?",
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
+ "MessageConfirmNotificationTestTrigger": "Trigger deze melding met test data?",
+ "MessageConfirmPurgeCache": "Met Purge cache wordt de gehele directory op /metadata/cache
verwijderd. Weet u zeker dat u de cachedirectory wilt verwijderen?",
+ "MessageConfirmPurgeItemsCache": "Met Purge items cache wordt de gehele directory op /metadata/cache/items
verwijderd. Weet u het zeker?",
+ "MessageConfirmQuickEmbed": "Waarschuwing! Quick embed maakt geen back-up van uw audiobestanden. Zorg ervoor dat u een back-up van uw audiobestanden hebt. Wilt u doorgaan?",
+ "MessageConfirmQuickMatchEpisodes": "Snel matchende afleveringen overschrijven details als er een match is gevonden. Alleen niet-matchende afleveringen worden bijgewerkt. Weet u het zeker?",
+ "MessageConfirmReScanLibraryItems": "Bent u zeker dat u {0} items opnieuw wil scannen?",
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
"MessageConfirmRemoveAuthor": "Weet je zeker dat je auteur \"{0}\" wil verwijderen?",
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
"MessageConfirmRemoveListeningSessions": "Weet je zeker dat je {0} luistersessies wilt verwijderen?",
+ "MessageConfirmRemoveMetadataFiles": "Bent u zeker dat u alle metadata wil verwijderen. {0} bestanden in uw bibliotheel item folders?",
"MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?",
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
@@ -519,11 +740,16 @@
"MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?",
"MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.",
"MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".",
+ "MessageConfirmResetProgress": "Bet u zeker dat u uw voortgang wil resetten?",
"MessageConfirmSendEbookToDevice": "Weet je zeker dat je {0} ebook \"{1}\" naar apparaat \"{2}\" wil sturen?",
+ "MessageConfirmUnlinkOpenId": "Bent u zeker dat u deze gebruiker wil ontkoppelen van OpenID?",
"MessageDownloadingEpisode": "Aflevering aan het dowloaden",
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
+ "MessageEmbedFailed": "Insluiten Mislukt!",
"MessageEmbedFinished": "Insluiting voltooid!",
+ "MessageEmbedQueue": "In de wachtrij voor metadata-embed ({0} in wachtrij)",
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
+ "MessageEreaderDevices": "Om de levering van e-books te garanderen, moet u mogelijk bovenstaand e-mailadres opgeven als geldige afzender voor elk hieronder vermeld apparaat.",
"MessageFeedURLWillBe": "Feed URL zal {0} zijn",
"MessageFetching": "Aan het ophalen...",
"MessageForceReScanDescription": "zal alle bestanden opnieuw scannen als een verse scan. Audiobestanden ID3-tags, OPF-bestanden en textbestanden zullen als nieuw worden gescand.",
@@ -535,6 +761,7 @@
"MessageListeningSessionsInTheLastYear": "{0} luistersessies in het laatste jaar",
"MessageLoading": "Aan het laden...",
"MessageLoadingFolders": "Mappen aan het laden...",
+ "MessageLogsDescription": "Logs worden opgeslagen in /metadata/logs
als JSON-bestanden. Crashlogs worden opgeslagen in /metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B mislukt!",
"MessageM4BFinished": "M4B voltooid!",
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
@@ -551,6 +778,7 @@
"MessageNoCollections": "Geen collecties",
"MessageNoCoversFound": "Geen covers gevonden",
"MessageNoDescription": "Geen beschrijving",
+ "MessageNoDevices": "Geen Apparaten",
"MessageNoDownloadsInProgress": "Geen downloads bezig op dit moment",
"MessageNoDownloadsQueued": "Geen downloads in de wachtrij",
"MessageNoEpisodeMatchesFound": "Geen afleveringsmatches gevonden",
@@ -564,6 +792,7 @@
"MessageNoLogs": "Geen logs",
"MessageNoMediaProgress": "Geen mediavoortgang",
"MessageNoNotifications": "Geen notificaties",
+ "MessageNoPodcastFeed": "Ongeldige podcast: Geen Feed",
"MessageNoPodcastsFound": "Geen podcasts gevonden",
"MessageNoResults": "Geen resultaten",
"MessageNoSearchResultsFor": "Geen zoekresultaten voor \"{0}\"",
@@ -573,11 +802,17 @@
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
"MessageNotYetImplemented": "Nog niet geimplementeerd",
+ "MessageOpmlPreviewNote": "Let op: Dit is een preview van het geparseerde OPML-bestand. De werkelijke podcasttitel wordt overgenomen uit de RSS-feed.",
"MessageOr": "of",
"MessagePauseChapter": "Pauzeer afspelen hoofdstuk",
"MessagePlayChapter": "Luister naar begin van hoofdstuk",
"MessagePlaylistCreateFromCollection": "Afspeellijst aanmaken vanuit collectie",
+ "MessagePleaseWait": "Even geduld...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast heeft geen RSS-feed URL om te gebruiken voor matching",
+ "MessagePodcastSearchField": "Voer zoekterm of RSS-feed-URL in",
+ "MessageQuickEmbedInProgress": "Snelle inbedding in uitvoering",
+ "MessageQuickEmbedQueue": "In de wachtrij voor snelle insluiting ({0} in wachtrij)",
+ "MessageQuickMatchAllEpisodes": "Alle Afleveringen Snel Matchen",
"MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.",
"MessageRemoveChapter": "Verwijder hoofdstuk",
"MessageRemoveEpisodes": "Verwijder {0} aflevering(en)",
@@ -588,10 +823,48 @@
"MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op",
"MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven. Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven. Alle clients die van je server gebruik maken zullen automatisch worden ververst.",
"MessageSearchResultsFor": "Zoekresultaten voor",
+ "MessageSelected": "{0} geselecteerd",
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
+ "MessageShareExpirationWillBe": "Vervaldatum is {0} ",
"MessageShareExpiresIn": "Vervalt in {0}",
+ "MessageShareURLWillBe": "De gedeelde URL wordt {0} ",
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",
+ "MessageTaskAudioFileNotWritable": "Audiobestand \"{0}\" is niet beschrijfbaar",
+ "MessageTaskCanceledByUser": "Taak geannuleerd door gebruiker",
+ "MessageTaskDownloadingEpisodeDescription": "Aflevering \"{0}\" downloaden",
+ "MessageTaskEmbeddingMetadata": "Metadata insluiten",
+ "MessageTaskEmbeddingMetadataDescription": "Metadata insluiten in audioboek \"{0}\"",
+ "MessageTaskEncodingM4b": "M4B Encoden",
+ "MessageTaskEncodingM4bDescription": "Audioboek \"{0}\" coderen in één m4b-bestand",
+ "MessageTaskFailed": "Mislukt",
+ "MessageTaskFailedToBackupAudioFile": "Het is niet gelukt om een back-up te maken van audiobestand \"{0}\"",
+ "MessageTaskFailedToCreateCacheDirectory": "Het is niet gelukt om een cachemap te maken",
+ "MessageTaskFailedToEmbedMetadataInFile": "Het is niet gelukt om metagegevens in bestand \"{0}\" in te sluiten",
+ "MessageTaskFailedToMergeAudioFiles": "Audiobestanden samenvoegen mislukt",
+ "MessageTaskFailedToMoveM4bFile": "m4b bestand verplaatsen mislukt",
+ "MessageTaskFailedToWriteMetadataFile": "Metadata bestand schrijven mislukt",
+ "MessageTaskMatchingBooksInLibrary": "Overeenkomende boeken in bibliotheek \"{0}\"",
+ "MessageTaskNoFilesToScan": "Geen bestanden om te scannen",
+ "MessageTaskOpmlImport": "OPML importeren",
+ "MessageTaskOpmlImportDescription": "Podcasts maken van {0} RSS feeds",
+ "MessageTaskOpmlImportFeed": "OPML feed importeren",
+ "MessageTaskOpmlImportFeedDescription": "RSS feed \"{0}\" importeren",
+ "MessageTaskOpmlImportFeedFailed": "Podcastfeed kon niet worden opgehaald",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Podcast \"{0}\" maken",
+ "MessageTaskOpmlImportFeedPodcastExists": "Podcast bestaat al in pad",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Mislukt om podcast aan te maken",
+ "MessageTaskOpmlImportFinished": "{0} podcasts toegevoegd",
+ "MessageTaskOpmlParseFailed": "Het is niet gelukt om het OPML-bestand te parseren",
+ "MessageTaskOpmlParseFastFail": "Ongeldig OPML-bestand tag niet gevonden OF een tag is niet gevonden",
+ "MessageTaskOpmlParseNoneFound": "Geen feeds gevonden in OPML bestand",
+ "MessageTaskScanItemsAdded": "{0} toegevoegd",
+ "MessageTaskScanItemsMissing": "{0} missend",
+ "MessageTaskScanItemsUpdated": "{0} bijgewerkt",
+ "MessageTaskScanNoChangesNeeded": "Geen aanpassingen nodig",
+ "MessageTaskScanningFileChanges": "Scannen van bestandswijzigingen in \"{0}\"",
+ "MessageTaskScanningLibrary": "Scannen van bibliotheek \"{0}\"",
+ "MessageTaskTargetDirectoryNotWritable": "Doelmap is niet beschrijfbaar",
"MessageThinking": "Aan het denken...",
"MessageUploaderItemFailed": "Uploaden mislukt",
"MessageUploaderItemSuccess": "Uploaden gelukt!",
@@ -609,74 +882,188 @@
"NoteUploaderFoldersWithMediaFiles": "Mappen met mediabestanden zullen worden behandeld als aparte bibliotheekonderdelen.",
"NoteUploaderOnlyAudioFiles": "Bij uploaden van uitsluitend audiobestanden wordt ieder audiobestand als apart audiobook worden behandeld.",
"NoteUploaderUnsupportedFiles": "Niet-ondersteunde bestanden worden genegeerd. Bij het kiezen of neerzetten van een map worden andere bestanden die niet in de map staan genegeerd.",
+ "NotificationOnBackupCompletedDescription": "Wordt geactiveerd wanneer een back-up is voltooid",
+ "NotificationOnBackupFailedDescription": "Wordt geactiveerd wanneer een back-up mislukt",
+ "NotificationOnEpisodeDownloadedDescription": "Wordt geactiveerd wanneer een podcastaflevering automatisch wordt gedownload",
+ "NotificationOnTestDescription": "Event voor het testen van het notificatiesysteem",
"PlaceholderNewCollection": "Nieuwe naam collectie",
"PlaceholderNewFolderPath": "Nieuwe locatie map",
"PlaceholderNewPlaylist": "Nieuwe naam afspeellijst",
"PlaceholderSearch": "Zoeken..",
"PlaceholderSearchEpisode": "Aflevering zoeken..",
- "ToastAccountUpdateFailed": "Bijwerken account mislukt",
+ "StatsAuthorsAdded": "auteurs toegevoegd",
+ "StatsBooksAdded": "boeken toegevoegd",
+ "StatsBooksAdditional": "Enkele toevoegingen zijn…",
+ "StatsBooksFinished": "boeken voltooid",
+ "StatsBooksFinishedThisYear": "Enkele boeken voltooid dit jaar…",
+ "StatsBooksListenedTo": "geluisterde boeken",
+ "StatsCollectionGrewTo": "Je boeken collectie groeide tot…",
+ "StatsSessions": "sessies",
+ "StatsSpentListening": "tijd geluisterd",
+ "StatsTopAuthor": "TOP AUTEUR",
+ "StatsTopAuthors": "TOP AUTEURS",
+ "StatsTopGenre": "TOP GENRE",
+ "StatsTopGenres": "TOP GENRES",
+ "StatsTopMonth": "TOP MAAND",
+ "StatsTopNarrator": "TOP VERTELLER",
+ "StatsTopNarrators": "TOP VERTELLERS",
+ "StatsTotalDuration": "Met een totale tijd van…",
+ "StatsYearInReview": "JAAR IN REVIEW",
"ToastAccountUpdateSuccess": "Account bijgewerkt",
+ "ToastAppriseUrlRequired": "Moet een Apprise URL invoeren",
+ "ToastAsinRequired": "ASIN is vereist",
"ToastAuthorImageRemoveSuccess": "Afbeelding auteur verwijderd",
- "ToastAuthorUpdateFailed": "Bijwerken auteur mislukt",
+ "ToastAuthorNotFound": "Auteur \"{0}\" niet gevonden",
+ "ToastAuthorRemoveSuccess": "Auteur verwijderd",
+ "ToastAuthorSearchNotFound": "Auteur niet gevonden",
"ToastAuthorUpdateMerged": "Auteur samengevoegd",
"ToastAuthorUpdateSuccess": "Auteur bijgewerkt",
"ToastAuthorUpdateSuccessNoImageFound": "Auteur bijgewerkt (geen afbeelding gevonden)",
+ "ToastBackupAppliedSuccess": "Backup toegepast",
"ToastBackupCreateFailed": "Back-up maken mislukt",
"ToastBackupCreateSuccess": "Back-up gemaakt",
"ToastBackupDeleteFailed": "Verwijderen back-up mislukt",
"ToastBackupDeleteSuccess": "Back-up verwijderd",
+ "ToastBackupInvalidMaxKeep": "Ongeldig aantal backups om bij te houden",
+ "ToastBackupInvalidMaxSize": "Ongeldige maximum backupgrootte",
"ToastBackupRestoreFailed": "Herstellen back-up mislukt",
"ToastBackupUploadFailed": "Uploaden back-up mislukt",
"ToastBackupUploadSuccess": "Back-up geüpload",
+ "ToastBatchDeleteFailed": "Batch verwijderen mislukt",
+ "ToastBatchDeleteSuccess": "Batch verwijderen gelukt",
+ "ToastBatchQuickMatchFailed": "Batch Snel Vergelijken mislukt!",
+ "ToastBatchQuickMatchStarted": "Bulk Snel Vergelijken van {0} boeken gestart!",
"ToastBatchUpdateFailed": "Bulk-bijwerking mislukt",
"ToastBatchUpdateSuccess": "Bulk-bijwerking gelukt",
"ToastBookmarkCreateFailed": "Aanmaken boekwijzer mislukt",
"ToastBookmarkCreateSuccess": "boekwijzer toegevoegd",
"ToastBookmarkRemoveSuccess": "Boekwijzer verwijderd",
- "ToastBookmarkUpdateFailed": "Bijwerken boekwijzer mislukt",
"ToastBookmarkUpdateSuccess": "Boekwijzer bijgewerkt",
+ "ToastCachePurgeFailed": "Cache wissen is mislukt",
+ "ToastCachePurgeSuccess": "Cache succesvol verwijderd",
"ToastChaptersHaveErrors": "Hoofdstukken bevatten fouten",
"ToastChaptersMustHaveTitles": "Hoofdstukken moeten titels hebben",
+ "ToastChaptersRemoved": "Hoofdstukken verwijderd",
+ "ToastChaptersUpdated": "Hoofdstukken bijgewerkt",
+ "ToastCollectionItemsAddFailed": "Item(s) toegevoegd aan collectie mislukt",
+ "ToastCollectionItemsAddSuccess": "Item(s) toegevoegd aan collectie gelukt",
"ToastCollectionItemsRemoveSuccess": "Onderdeel (of onderdelen) verwijderd uit collectie",
"ToastCollectionRemoveSuccess": "Collectie verwijderd",
- "ToastCollectionUpdateFailed": "Bijwerken collectie mislukt",
"ToastCollectionUpdateSuccess": "Collectie bijgewerkt",
- "ToastItemCoverUpdateFailed": "Bijwerken cover onderdeel mislukt",
+ "ToastCoverUpdateFailed": "Cover update mislukt",
+ "ToastDeleteFileFailed": "Bestand verwijderen mislukt",
+ "ToastDeleteFileSuccess": "Bestand verwijderd",
+ "ToastDeviceAddFailed": "Apparaat toevoegen mislukt",
+ "ToastDeviceNameAlreadyExists": "Er bestaat al een e-reader met die naam",
+ "ToastDeviceTestEmailFailed": "Het is niet gelukt om een test-e-mail te verzenden",
+ "ToastDeviceTestEmailSuccess": "Test e-mail verzonden",
+ "ToastEmailSettingsUpdateSuccess": "Emaill intellingen bijgewerkt",
+ "ToastEncodeCancelFailed": "Het is niet gelukt om het coderen te annuleren",
+ "ToastEncodeCancelSucces": "Encode geannuleerd",
+ "ToastEpisodeDownloadQueueClearFailed": "Wachtrij legen mislukt",
+ "ToastEpisodeDownloadQueueClearSuccess": "Aflevering download-wachtrij geleegt",
+ "ToastEpisodeUpdateSuccess": "{0} afleveringen bijgewerkt",
+ "ToastErrorCannotShare": "Kan niet native delen op dit apparaat",
+ "ToastFailedToLoadData": "Data laden mislukt",
+ "ToastFailedToMatch": "Match mislukt",
+ "ToastFailedToShare": "Delen mislukt",
+ "ToastFailedToUpdate": "Update mislukt",
+ "ToastInvalidImageUrl": "Ongeldige afbeeldings-URL",
+ "ToastInvalidMaxEpisodesToDownload": "Ongeldig maximum aantal afleveringen om te downloaden",
+ "ToastInvalidUrl": "Ongeldige URL",
"ToastItemCoverUpdateSuccess": "Cover onderdeel bijgewerkt",
- "ToastItemDetailsUpdateFailed": "Bijwerken details onderdeel mislukt",
+ "ToastItemDeletedFailed": "Item verwijderen mislukt",
+ "ToastItemDeletedSuccess": "Verwijderd item",
"ToastItemDetailsUpdateSuccess": "Details onderdeel bijgewerkt",
"ToastItemMarkedAsFinishedFailed": "Markeren als Voltooid mislukt",
"ToastItemMarkedAsFinishedSuccess": "Onderdeel gemarkeerd als Voltooid",
"ToastItemMarkedAsNotFinishedFailed": "Markeren als Niet Voltooid mislukt",
"ToastItemMarkedAsNotFinishedSuccess": "Onderdeel gemarkeerd als Niet Voltooid",
+ "ToastItemUpdateSuccess": "Item bijgewerkt",
"ToastLibraryCreateFailed": "Bibliotheek aanmaken mislukt",
"ToastLibraryCreateSuccess": "Bibliotheek \"{0}\" aangemaakt",
"ToastLibraryDeleteFailed": "Bibliotheek verwijderen mislukt",
"ToastLibraryDeleteSuccess": "Bibliotheek verwijderd",
"ToastLibraryScanFailedToStart": "Starten scan mislukt",
"ToastLibraryScanStarted": "Scannen bibliotheek gestart",
- "ToastLibraryUpdateFailed": "Bijwerken bibliotheek mislukt",
"ToastLibraryUpdateSuccess": "Bibliotheek \"{0}\" bijgewerkt",
+ "ToastMatchAllAuthorsFailed": "Alle auteurs matchen mislukt",
+ "ToastMetadataFilesRemovedError": "Fout bij verwijderen van metadata. {0} bestanden",
+ "ToastMetadataFilesRemovedNoneFound": "Geen metadata. {0} bestanden gevonden in bibliotheek",
+ "ToastMetadataFilesRemovedNoneRemoved": "Geen metadata. {0} bestanden verwijderd",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadata. {1} bestanden verwijderd",
+ "ToastMustHaveAtLeastOnePath": "Moet ten minste een pad hebben",
+ "ToastNameEmailRequired": "Naam en email zijn vereist",
+ "ToastNameRequired": "Naam is vereist",
+ "ToastNewEpisodesFound": "{0} nieuwe afleveringen gevonden",
+ "ToastNewUserCreatedFailed": "Account: \"{0}\" aanmaken mislukt",
+ "ToastNewUserCreatedSuccess": "Nieuw account aangemaakt",
+ "ToastNewUserLibraryError": "Moet ten minste een bibliotheek selecteren",
+ "ToastNewUserPasswordError": "Moet een wachtwoord hebben, enkel root gebruiker kan een leeg wachtwoord gebruiken",
+ "ToastNewUserTagError": "Moet ten minste een tag selecteren",
+ "ToastNewUserUsernameError": "Voer een gebruikersnaam in",
+ "ToastNoNewEpisodesFound": "Geen nieuwe afleveringen gevonden",
+ "ToastNoUpdatesNecessary": "Geen updates nodig",
+ "ToastNotificationCreateFailed": "Nieuwe melding aanmaken mislukt",
+ "ToastNotificationDeleteFailed": "Melding verwijderen mislukt",
+ "ToastNotificationFailedMaximum": "Maximum aantal pogingen moet >=0",
+ "ToastNotificationQueueMaximum": "Maximale meldingen wachtrij moet >=0",
+ "ToastNotificationSettingsUpdateSuccess": "Meldingsinstellingen bijgewerkt",
+ "ToastNotificationTestTriggerFailed": "Het is niet gelukt om een testmelding te activeren",
+ "ToastNotificationTestTriggerSuccess": "Geactiveerde testmelding",
+ "ToastNotificationUpdateSuccess": "Melding bijgewerkt",
"ToastPlaylistCreateFailed": "Aanmaken afspeellijst mislukt",
"ToastPlaylistCreateSuccess": "Afspeellijst aangemaakt",
"ToastPlaylistRemoveSuccess": "Afspeellijst verwijderd",
- "ToastPlaylistUpdateFailed": "Afspeellijst bijwerken mislukt",
"ToastPlaylistUpdateSuccess": "Afspeellijst bijgewerkt",
"ToastPodcastCreateFailed": "Podcast aanmaken mislukt",
"ToastPodcastCreateSuccess": "Podcast aangemaakt",
+ "ToastPodcastGetFeedFailed": "Podcast feed ophalen mislukt",
+ "ToastPodcastNoEpisodesInFeed": "Geen afleveringen gevonden in RSS feed",
+ "ToastPodcastNoRssFeed": "Podcast heeft geen RSS feed",
+ "ToastProgressIsNotBeingSynced": "De voortgang wordt niet gesynchroniseerd, start het afspelen opnieuw",
+ "ToastProviderCreatedFailed": "Provider toevoegen mislukt",
+ "ToastProviderCreatedSuccess": "Nieuwe provider toegevoegd",
+ "ToastProviderNameAndUrlRequired": "Naam en URL vereist",
+ "ToastProviderRemoveSuccess": "Provider verwijderd",
"ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt",
"ToastRSSFeedCloseSuccess": "RSS-feed gesloten",
+ "ToastRemoveFailed": "Verwijderen mislukt",
"ToastRemoveItemFromCollectionFailed": "Onderdeel verwijderen uit collectie mislukt",
"ToastRemoveItemFromCollectionSuccess": "Onderdeel verwijderd uit collectie",
+ "ToastRemoveItemsWithIssuesFailed": "Verwijderen van bibliotheekitems met problemen mislukt",
+ "ToastRemoveItemsWithIssuesSuccess": "Bibliotheekitems met problemen verwijderd",
+ "ToastRenameFailed": "Hernoemen mislukt",
+ "ToastRescanFailed": "Opnieuw scannen mislukt voor {0}",
+ "ToastRescanRemoved": "Opnieuw scannen voltooid, item is verwijderd",
+ "ToastRescanUpToDate": "Rescan voltooid, item is up to date",
+ "ToastRescanUpdated": "Rescan voltooid, item is geupdated",
+ "ToastScanFailed": "Bibliotheek item scannen mislukt",
+ "ToastSelectAtLeastOneUser": "Selecteer ten minste een gebruiker",
"ToastSendEbookToDeviceFailed": "Ebook naar apparaat sturen mislukt",
"ToastSendEbookToDeviceSuccess": "Ebook verstuurd naar apparaat \"{0}\"",
"ToastSeriesUpdateFailed": "Bijwerken serie mislukt",
"ToastSeriesUpdateSuccess": "Bijwerken serie gelukt",
+ "ToastServerSettingsUpdateSuccess": "Server instellingen bijgewerkt",
+ "ToastSessionCloseFailed": "Sessie sluiten mislukt",
"ToastSessionDeleteFailed": "Verwijderen sessie mislukt",
"ToastSessionDeleteSuccess": "Sessie verwijderd",
+ "ToastSleepTimerDone": "Slaap timer voltooid... zZzzZz",
+ "ToastSlugMustChange": "Slug bevat ongeldige symbolen",
+ "ToastSlugRequired": "Slug is vereist",
"ToastSocketConnected": "Socket verbonden",
"ToastSocketDisconnected": "Socket niet verbonden",
"ToastSocketFailedToConnect": "Verbinding Socket mislukt",
+ "ToastSortingPrefixesEmptyError": "Moet ten minste 1 sorteer-prefix bevatten",
+ "ToastSortingPrefixesUpdateSuccess": "Sorteer prefixes geupdated ({0} items)",
+ "ToastTitleRequired": "Titel is vereist",
+ "ToastUnknownError": "Onbekende fout",
+ "ToastUnlinkOpenIdFailed": "Gebruiker ontkoppelen van OpenID mislukt",
+ "ToastUnlinkOpenIdSuccess": "Gebruiker ontkoppeld van OpenID",
"ToastUserDeleteFailed": "Verwijderen gebruiker mislukt",
- "ToastUserDeleteSuccess": "Gebruiker verwijderd"
+ "ToastUserDeleteSuccess": "Gebruiker verwijderd",
+ "ToastUserPasswordChangeSuccess": "Wachtwoord succesvol gewijzigd",
+ "ToastUserPasswordMismatch": "Wachtwoorden komen niet overeen",
+ "ToastUserPasswordMustChange": "Het nieuwe wachtwoord kan niet overeenkomen met het oude wachtwoord",
+ "ToastUserRootRequireName": "U moet een root-gebruikersnaam invoeren"
}
diff --git a/client/strings/no.json b/client/strings/no.json
index ea2b8f0c53..01be88ef4a 100644
--- a/client/strings/no.json
+++ b/client/strings/no.json
@@ -641,10 +641,8 @@
"PlaceholderNewPlaylist": "Ny spillelistenavn",
"PlaceholderSearch": "Søk..",
"PlaceholderSearchEpisode": "Søk episode..",
- "ToastAccountUpdateFailed": "Mislykkes å oppdatere konto",
"ToastAccountUpdateSuccess": "Konto oppdatert",
"ToastAuthorImageRemoveSuccess": "Forfatter bilde fjernet",
- "ToastAuthorUpdateFailed": "Mislykkes å oppdatere forfatter",
"ToastAuthorUpdateMerged": "Forfatter slått sammen",
"ToastAuthorUpdateSuccess": "Forfatter oppdatert",
"ToastAuthorUpdateSuccessNoImageFound": "Forfatter oppdater (ingen bilde funnet)",
@@ -660,17 +658,13 @@
"ToastBookmarkCreateFailed": "Misslykkes å opprette bokmerke",
"ToastBookmarkCreateSuccess": "Bokmerke lagt til",
"ToastBookmarkRemoveSuccess": "Bokmerke fjernet",
- "ToastBookmarkUpdateFailed": "Misslykkes å oppdatere bokmerke",
"ToastBookmarkUpdateSuccess": "Bokmerke oppdatert",
"ToastChaptersHaveErrors": "Kapittel har feil",
"ToastChaptersMustHaveTitles": "Kapittel må ha titler",
"ToastCollectionItemsRemoveSuccess": "Gjenstand(er) fjernet fra samling",
"ToastCollectionRemoveSuccess": "Samling fjernet",
- "ToastCollectionUpdateFailed": "Misslykkes å oppdatere samling",
"ToastCollectionUpdateSuccess": "samlingupdated",
- "ToastItemCoverUpdateFailed": "Misslykkes å oppdatere omslag",
"ToastItemCoverUpdateSuccess": "Omslag oppdatert",
- "ToastItemDetailsUpdateFailed": "Misslykkes å oppdatere detaljer",
"ToastItemDetailsUpdateSuccess": "Detaljer oppdatert",
"ToastItemMarkedAsFinishedFailed": "Misslykkes å markere som Fullført",
"ToastItemMarkedAsFinishedSuccess": "Gjenstand marker som Fullført",
@@ -682,12 +676,10 @@
"ToastLibraryDeleteSuccess": "Bibliotek slettet",
"ToastLibraryScanFailedToStart": "Misslykkes å starte skann",
"ToastLibraryScanStarted": "Bibliotek skann startet",
- "ToastLibraryUpdateFailed": "Misslykkes å oppdatere bibiliotek",
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" oppdatert",
"ToastPlaylistCreateFailed": "Misslykkes å opprette spilleliste",
"ToastPlaylistCreateSuccess": "Spilleliste opprettet",
"ToastPlaylistRemoveSuccess": "Spilleliste fjernet",
- "ToastPlaylistUpdateFailed": "Misslykkes å oppdatere spilleliste",
"ToastPlaylistUpdateSuccess": "Spilleliste oppdatert",
"ToastPodcastCreateFailed": "Misslykkes å opprette podcast",
"ToastPodcastCreateSuccess": "Podcast opprettet",
diff --git a/client/strings/pl.json b/client/strings/pl.json
index c786a1188f..85c7b769b1 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -54,14 +54,17 @@
"ButtonOpenManager": "Otwórz menadżera",
"ButtonPause": "Wstrzymaj",
"ButtonPlay": "Odtwarzaj",
+ "ButtonPlayAll": "Odtwórz wszystko",
"ButtonPlaying": "Odtwarzane",
"ButtonPlaylists": "Listy odtwarzania",
"ButtonPrevious": "Poprzedni",
"ButtonPreviousChapter": "Poprzedni rozdział",
+ "ButtonProbeAudioFile": "Próbka audio",
"ButtonPurgeAllCache": "Wyczyść dane tymczasowe",
"ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji",
"ButtonQueueAddItem": "Dodaj do kolejki",
"ButtonQueueRemoveItem": "Usuń z kolejki",
+ "ButtonQuickEmbed": "Szybkie wstawienie",
"ButtonQuickEmbedMetadata": "Szybkie wstawianie metadanych",
"ButtonQuickMatch": "Szybkie dopasowanie",
"ButtonReScan": "Ponowne skanowanie",
@@ -93,8 +96,9 @@
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
"ButtonStartMetadataEmbed": "Osadź metadane",
"ButtonStats": "Statystyki",
- "ButtonSubmit": "Zaloguj",
+ "ButtonSubmit": "Zapisz",
"ButtonTest": "Test",
+ "ButtonUnlinkOpenId": "Odłącz OpenID",
"ButtonUpload": "Wgraj",
"ButtonUploadBackup": "Wgraj kopię zapasową",
"ButtonUploadCover": "Wgraj okładkę",
@@ -135,6 +139,7 @@
"HeaderFindChapters": "Wyszukaj rozdziały",
"HeaderIgnoredFiles": "Zignoruj pliki",
"HeaderItemFiles": "Pliki",
+ "HeaderItemMetadataUtils": "Narzędzia dla metadanych",
"HeaderLastListeningSession": "Ostatnia sesja słuchania",
"HeaderLatestEpisodes": "Najnowsze odcinki",
"HeaderLibraries": "Biblioteki",
@@ -173,6 +178,7 @@
"HeaderRemoveEpisodes": "Usuń {0} odcinków",
"HeaderSavedMediaProgress": "Zapisany postęp",
"HeaderSchedule": "Harmonogram",
+ "HeaderScheduleEpisodeDownloads": "Planowanie automatycznego ściągania odcinków",
"HeaderScheduleLibraryScans": "Zaplanuj automatyczne skanowanie biblioteki",
"HeaderSession": "Sesja",
"HeaderSetBackupSchedule": "Ustaw harmonogram tworzenia kopii zapasowej",
@@ -218,7 +224,11 @@
"LabelAllUsersExcludingGuests": "Wszyscy użytkownicy z wyłączeniem gości",
"LabelAllUsersIncludingGuests": "Wszyscy użytkownicy, łącznie z gośćmi",
"LabelAlreadyInYourLibrary": "Już istnieje w twojej bibliotece",
+ "LabelApiToken": "API Token",
"LabelAppend": "Dołącz",
+ "LabelAudioBitrate": "Audio Bitrate (np. 128k)",
+ "LabelAudioChannels": "Kanały dźwięku (1 lub 2)",
+ "LabelAudioCodec": "Kodek audio",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Rosnąco)",
"LabelAuthorLastFirst": "Author (Malejąco)",
@@ -230,6 +240,7 @@
"LabelAutoRegister": "Automatyczna rejestracja",
"LabelAutoRegisterDescription": "Automatycznie utwórz nowych użytkowników po zalogowaniu",
"LabelBackToUser": "Powrót",
+ "LabelBackupAudioFiles": "Kopia zapasowa plików audio",
"LabelBackupLocation": "Lokalizacja kopii zapasowej",
"LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe",
"LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups",
@@ -238,15 +249,18 @@
"LabelBackupsNumberToKeep": "Liczba kopii zapasowych do przechowywania",
"LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.",
"LabelBitrate": "Bitrate",
+ "LabelBonus": "Bonus",
"LabelBooks": "Książki",
"LabelButtonText": "Tekst przycisku",
"LabelByAuthor": "autorstwa {0}",
"LabelChangePassword": "Zmień hasło",
"LabelChannels": "Kanały",
+ "LabelChapterCount": "{0} rozdziałów",
"LabelChapterTitle": "Tytuł rozdziału",
"LabelChapters": "Rozdziały",
"LabelChaptersFound": "Znalezione rozdziały",
"LabelClickForMoreInfo": "Kliknij po więcej szczegółów",
+ "LabelClickToUseCurrentValue": "Kliknij by zastosować aktualną wartość",
"LabelClosePlayer": "Zamknij odtwarzacz",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Podsumuj serię",
@@ -296,6 +310,7 @@
"LabelEmailSettingsTestAddress": "Adres testowy",
"LabelEmbeddedCover": "Wbudowana okładka",
"LabelEnable": "Włącz",
+ "LabelEncodingBackupLocation": "Kopia zapasowa twoich oryginalnych plików audio będzie się znajdować w:",
"LabelEnd": "Zakończ",
"LabelEndOfChapter": "Koniec rozdziału",
"LabelEpisode": "Odcinek",
@@ -355,7 +370,8 @@
"LabelIntervalEveryHour": "Każdej godziny",
"LabelInvert": "Inversja",
"LabelItem": "Pozycja",
- "LabelJumpBackwardAmount": "Rozmiar skoku do przodu",
+ "LabelJumpBackwardAmount": "Przeskocz do tyłu o:",
+ "LabelJumpForwardAmount": "Przeskocz do przodu o:",
"LabelLanguage": "Język",
"LabelLanguageDefaultServer": "Domyślny język serwera",
"LabelLanguages": "Języki",
@@ -458,7 +474,7 @@
"LabelReadAgain": "Czytaj ponownie",
"LabelReadEbookWithoutProgress": "Czytaj książkę bez zapamiętywania postępu",
"LabelRecentSeries": "Ostatnie serie",
- "LabelRecentlyAdded": "Niedawno dodany",
+ "LabelRecentlyAdded": "Niedawno dodane",
"LabelRecommended": "Polecane",
"LabelRedo": "Wycofaj",
"LabelReleaseDate": "Data wydania",
@@ -738,10 +754,8 @@
"StatsTopNarrator": "TOPOWY NARRATOR",
"StatsTopNarrators": "TOPOWI NARRATORZY",
"StatsYearInReview": "PRZEGLĄD ROKU",
- "ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
"ToastAccountUpdateSuccess": "Zaktualizowano konto",
"ToastAuthorImageRemoveSuccess": "Zdjęcie autora usunięte",
- "ToastAuthorUpdateFailed": "nie udało się zaktualizować autora",
"ToastAuthorUpdateMerged": "Autor scalony",
"ToastAuthorUpdateSuccess": "Autor zaktualizowany",
"ToastAuthorUpdateSuccessNoImageFound": "Autor zaktualizowany (nie znaleziono obrazu)",
@@ -757,15 +771,11 @@
"ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki",
"ToastBookmarkCreateSuccess": "Dodano zakładkę",
"ToastBookmarkRemoveSuccess": "Zakładka została usunięta",
- "ToastBookmarkUpdateFailed": "Nie udało się zaktualizować zakładki",
"ToastBookmarkUpdateSuccess": "Zaktualizowano zakładkę",
"ToastCollectionItemsRemoveSuccess": "Przedmiot(y) zostały usunięte z kolekcji",
"ToastCollectionRemoveSuccess": "Kolekcja usunięta",
- "ToastCollectionUpdateFailed": "Nie udało się zaktualizować kolekcji",
"ToastCollectionUpdateSuccess": "Zaktualizowano kolekcję",
- "ToastItemCoverUpdateFailed": "Nie udało się zaktualizować okładki",
"ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę",
- "ToastItemDetailsUpdateFailed": "Nie udało się zaktualizować szczegółów",
"ToastItemDetailsUpdateSuccess": "Zaktualizowano szczegóły",
"ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako ukończone",
"ToastItemMarkedAsFinishedSuccess": "Pozycja oznaczona jako ukończona",
@@ -777,12 +787,10 @@
"ToastLibraryDeleteSuccess": "Biblioteka usunięta",
"ToastLibraryScanFailedToStart": "Nie udało się rozpocząć skanowania",
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
- "ToastLibraryUpdateFailed": "Nie udało się zaktualizować biblioteki",
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
"ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
"ToastPlaylistCreateSuccess": "Playlista utworzona",
"ToastPlaylistRemoveSuccess": "Playlista usunięta",
- "ToastPlaylistUpdateFailed": "Nie udało się zaktualizować playlisty",
"ToastPlaylistUpdateSuccess": "Playlista zaktualizowana",
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json
index dba497d44e..7df7c47dec 100644
--- a/client/strings/pt-br.json
+++ b/client/strings/pt-br.json
@@ -258,12 +258,15 @@
"LabelDiscFromFilename": "Disco a partir do nome do arquivo",
"LabelDiscFromMetadata": "Disco a partir dos metadados",
"LabelDiscover": "Descobrir",
+ "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download de {0} Episódios",
"LabelDuration": "Duração",
"LabelDurationComparisonExactMatch": "(exato)",
"LabelDurationComparisonLonger": "({0} maior)",
"LabelDurationComparisonShorter": "({0} menor)",
"LabelDurationFound": "Duração comprovada:",
+ "LabelEbook": "Ebook",
+ "LabelEbooks": "Ebooks",
"LabelEdit": "Editar",
"LabelEmailSettingsFromAddress": "Remetente",
"LabelEmailSettingsRejectUnauthorized": "Rejeitar certificados não autorizados",
@@ -710,10 +713,8 @@
"PlaceholderNewPlaylist": "Novo nome da lista de reprodução",
"PlaceholderSearch": "Buscar..",
"PlaceholderSearchEpisode": "Buscar Episódio..",
- "ToastAccountUpdateFailed": "Falha ao atualizar a conta",
"ToastAccountUpdateSuccess": "Conta atualizada",
"ToastAuthorImageRemoveSuccess": "Imagem do autor removida",
- "ToastAuthorUpdateFailed": "Falha ao atualizar o autor",
"ToastAuthorUpdateMerged": "Autor combinado",
"ToastAuthorUpdateSuccess": "Autor atualizado",
"ToastAuthorUpdateSuccessNoImageFound": "Autor atualizado (nenhuma imagem encontrada)",
@@ -729,7 +730,6 @@
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
"ToastBookmarkCreateSuccess": "Marcador adicionado",
"ToastBookmarkRemoveSuccess": "Marcador removido",
- "ToastBookmarkUpdateFailed": "Falha ao atualizar o marcador",
"ToastBookmarkUpdateSuccess": "Marcador atualizado",
"ToastCachePurgeFailed": "Falha ao apagar o cache",
"ToastCachePurgeSuccess": "Cache apagado com sucesso",
@@ -737,14 +737,11 @@
"ToastChaptersMustHaveTitles": "Capítulos precisam ter títulos",
"ToastCollectionItemsRemoveSuccess": "Item(ns) removidos da coleção",
"ToastCollectionRemoveSuccess": "Coleção removida",
- "ToastCollectionUpdateFailed": "Falha ao atualizar coleção",
"ToastCollectionUpdateSuccess": "Coleção atualizada",
"ToastDeleteFileFailed": "Falha ao apagar arquivo",
"ToastDeleteFileSuccess": "Arquivo apagado",
"ToastFailedToLoadData": "Falha ao carregar dados",
- "ToastItemCoverUpdateFailed": "Falha ao atualizar capa do item",
"ToastItemCoverUpdateSuccess": "Capa do item atualizada",
- "ToastItemDetailsUpdateFailed": "Falha ao atualizar detalhes do item",
"ToastItemDetailsUpdateSuccess": "Detalhes do item atualizados",
"ToastItemMarkedAsFinishedFailed": "Falha ao marcar como Concluído",
"ToastItemMarkedAsFinishedSuccess": "Item marcado como Concluído",
@@ -756,12 +753,10 @@
"ToastLibraryDeleteSuccess": "Biblioteca apagada",
"ToastLibraryScanFailedToStart": "Falha ao iniciar verificação",
"ToastLibraryScanStarted": "Verificação da biblioteca iniciada",
- "ToastLibraryUpdateFailed": "Falha ao atualizar a biblioteca",
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada",
"ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução",
"ToastPlaylistCreateSuccess": "Lista de reprodução criada",
"ToastPlaylistRemoveSuccess": "Lista de reprodução removida",
- "ToastPlaylistUpdateFailed": "Falha ao atualizar lista de reprodução",
"ToastPlaylistUpdateSuccess": "Lista de reprodução atualizada",
"ToastPodcastCreateFailed": "Falha ao criar podcast",
"ToastPodcastCreateSuccess": "Podcast criado",
@@ -773,7 +768,6 @@
"ToastSendEbookToDeviceSuccess": "Ebook enviado para o dispositivo \"{0}\"",
"ToastSeriesUpdateFailed": "Falha ao atualizar série",
"ToastSeriesUpdateSuccess": "Série atualizada",
- "ToastServerSettingsUpdateFailed": "Falha ao atualizar configurações do servidor",
"ToastServerSettingsUpdateSuccess": "Configurações do servidor atualizadas",
"ToastSessionDeleteFailed": "Falha ao apagar sessão",
"ToastSessionDeleteSuccess": "Sessão apagada",
@@ -781,7 +775,6 @@
"ToastSocketDisconnected": "Socket desconectado",
"ToastSocketFailedToConnect": "Falha na conexão do socket",
"ToastSortingPrefixesEmptyError": "É preciso ter pelo menos um prefixo de ordenação",
- "ToastSortingPrefixesUpdateFailed": "Falha ao atualizar prefixos de ordenação",
"ToastSortingPrefixesUpdateSuccess": "Prefixos de ordenação atualizados ({0} item(ns))",
"ToastUserDeleteFailed": "Falha ao apagar usuário",
"ToastUserDeleteSuccess": "Usuário apagado"
diff --git a/client/strings/ru.json b/client/strings/ru.json
index bfefb5bd87..b0743af630 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -56,6 +56,7 @@
"ButtonOpenManager": "Открыть менеджер",
"ButtonPause": "Пауза",
"ButtonPlay": "Слушать",
+ "ButtonPlayAll": "Играть все",
"ButtonPlaying": "Проигрывается",
"ButtonPlaylists": "Плейлисты",
"ButtonPrevious": "Предыдущий",
@@ -65,6 +66,7 @@
"ButtonPurgeItemsCache": "Очистить кэш элементов",
"ButtonQueueAddItem": "Добавить в очередь",
"ButtonQueueRemoveItem": "Удалить из очереди",
+ "ButtonQuickEmbed": "Быстрое внедрение",
"ButtonQuickEmbedMetadata": "Быстрое встраивание метаданных",
"ButtonQuickMatch": "Быстрый поиск",
"ButtonReScan": "Пересканировать",
@@ -161,6 +163,7 @@
"HeaderNotificationUpdate": "Уведомление об обновлении",
"HeaderNotifications": "Уведомления",
"HeaderOpenIDConnectAuthentication": "Аутентификация OpenID Connect",
+ "HeaderOpenListeningSessions": "Открытые сеансы прослушивания",
"HeaderOpenRSSFeed": "Открыть RSS-канал",
"HeaderOtherFiles": "Другие файлы",
"HeaderPasswordAuthentication": "Аутентификация по паролю",
@@ -178,6 +181,7 @@
"HeaderRemoveEpisodes": "Удалить {0} эпизодов",
"HeaderSavedMediaProgress": "Прогресс медиа сохранен",
"HeaderSchedule": "Планировщик",
+ "HeaderScheduleEpisodeDownloads": "Запланируйте автоматическую загрузку эпизодов",
"HeaderScheduleLibraryScans": "Планировщик автоматического сканирования библиотеки",
"HeaderSession": "Сеансы",
"HeaderSetBackupSchedule": "Установить планировщик бэкапов",
@@ -223,7 +227,11 @@
"LabelAllUsersExcludingGuests": "Все пользователи, кроме гостей",
"LabelAllUsersIncludingGuests": "Все пользователи, включая гостей",
"LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке",
+ "LabelApiToken": "Токен API",
"LabelAppend": "Добавить",
+ "LabelAudioBitrate": "Битрейт (напр. 128k)",
+ "LabelAudioChannels": "Аудиоканалы (1 или 2)",
+ "LabelAudioCodec": "Аудиокодек",
"LabelAuthor": "Автор",
"LabelAuthorFirstLast": "Автор (Имя Фамилия)",
"LabelAuthorLastFirst": "Автор (Фамилия, Имя)",
@@ -236,6 +244,7 @@
"LabelAutoRegister": "Автоматическая регистрация",
"LabelAutoRegisterDescription": "Автоматическое создание новых пользователей после входа в систему",
"LabelBackToUser": "Назад к пользователю",
+ "LabelBackupAudioFiles": "Резервное копирование аудиофайлов",
"LabelBackupLocation": "Путь для бэкапов",
"LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование",
"LabelBackupsEnableAutomaticBackupsHelp": "Бэкапы сохраняются в /metadata/backups",
@@ -244,15 +253,18 @@
"LabelBackupsNumberToKeep": "Сохранять бэкапов",
"LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.",
"LabelBitrate": "Битрейт",
+ "LabelBonus": "Бонус",
"LabelBooks": "Книги",
"LabelButtonText": "Текст кнопки",
"LabelByAuthor": "{0}",
"LabelChangePassword": "Изменить пароль",
"LabelChannels": "Каналы",
+ "LabelChapterCount": "{0} Главы",
"LabelChapterTitle": "Название главы",
"LabelChapters": "Главы",
"LabelChaptersFound": "глав найдено",
"LabelClickForMoreInfo": "Нажмите, чтобы узнать больше",
+ "LabelClickToUseCurrentValue": "Нажмите, чтобы использовать текущее значение",
"LabelClosePlayer": "Закрыть проигрыватель",
"LabelCodec": "Кодек",
"LabelCollapseSeries": "Свернуть серии",
@@ -302,12 +314,25 @@
"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": "URL-адрес эпизода из RSS-ленты",
"LabelEpisodes": "Эпизодов",
+ "LabelEpisodic": "Эпизодический",
"LabelExample": "Пример",
"LabelExpandSeries": "Развернуть серию",
"LabelExpandSubSeries": "Развернуть подсерию",
@@ -335,12 +360,13 @@
"LabelFontScale": "Масштаб шрифта",
"LabelFontStrikethrough": "Зачеркнутый",
"LabelFormat": "Формат",
+ "LabelFull": "Полный",
"LabelGenre": "Жанр",
"LabelGenres": "Жанры",
"LabelHardDeleteFile": "Жесткое удаление файла",
"LabelHasEbook": "Есть e-книга",
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
- "LabelHideSubtitles": "Скрыть субтитры",
+ "LabelHideSubtitles": "Скрыть серии",
"LabelHighestPriority": "Наивысший приоритет",
"LabelHost": "Хост",
"LabelHour": "Часы",
@@ -390,6 +416,10 @@
"LabelLowestPriority": "Самый низкий приоритет",
"LabelMatchExistingUsersBy": "Сопоставление существующих пользователей по",
"LabelMatchExistingUsersByDescription": "Используется для подключения существующих пользователей. После подключения пользователям будет присвоен уникальный идентификатор от поставщика единого входа",
+ "LabelMaxEpisodesToDownload": "Максимальное количество эпизодов для загрузки. Используйте 0 для неограниченного количества.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Максимальное количество новых эпизодов для загрузки за одну проверку",
+ "LabelMaxEpisodesToKeep": "Максимальное количество сохраняемых эпизодов",
+ "LabelMaxEpisodesToKeepHelp": "Значение 0 не устанавливает максимального ограничения. После автоматической загрузки нового эпизода самый старый эпизод будет удален, если у вас более X эпизодов. При этом будет удален только 1 эпизод за каждую новую загрузку.",
"LabelMediaPlayer": "Медиа проигрыватель",
"LabelMediaType": "Тип медиа",
"LabelMetaTag": "Мета тег",
@@ -435,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "Имя утверждения OpenID, содержащего список групп пользователя. Обычно их называют groups
. Если эта настройка настроена, приложение будет автоматически назначать роли на основе членства пользователя в группах при условии, что эти группы названы в утверждении без учета регистра \"admin\", \"user\" или \"guest\". Утверждение должно содержать список, и если пользователь принадлежит к нескольким группам, то приложение назначит роль, соответствующую самому высокому уровню доступа. Если ни одна из групп не совпадает, доступ будет запрещен.",
"LabelOpenRSSFeed": "Открыть RSS-канал",
"LabelOverwrite": "Перезаписать",
+ "LabelPaginationPageXOfY": "Страница {0} из {1}",
"LabelPassword": "Пароль",
"LabelPath": "Путь",
"LabelPermanent": "Постоянный",
"LabelPermissionsAccessAllLibraries": "Есть доступ ко всем библиотекам",
"LabelPermissionsAccessAllTags": "Есть доступ ко всем тегам",
"LabelPermissionsAccessExplicitContent": "Есть доступ к явному содержимому",
+ "LabelPermissionsCreateEreader": "Можно создать читалку",
"LabelPermissionsDelete": "Может удалять",
"LabelPermissionsDownload": "Может скачивать",
"LabelPermissionsUpdate": "Может обновлять",
@@ -464,6 +496,8 @@
"LabelPubDate": "Дата публикации",
"LabelPublishYear": "Год публикации",
"LabelPublishedDate": "Опубликовано {0}",
+ "LabelPublishedDecade": "Декада публикации",
+ "LabelPublishedDecades": "Декады публикации",
"LabelPublisher": "Издатель",
"LabelPublishers": "Издатели",
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
@@ -483,21 +517,28 @@
"LabelRedo": "Повторить",
"LabelRegion": "Регион",
"LabelReleaseDate": "Дата выхода",
+ "LabelRemoveAllMetadataAbs": "Удалите все файлы metadata.abs",
+ "LabelRemoveAllMetadataJson": "Удалите все файлы metadata.json",
"LabelRemoveCover": "Удалить обложку",
+ "LabelRemoveMetadataFile": "Удаление файлов метаданных в папках элементов библиотеки",
+ "LabelRemoveMetadataFileHelp": "Удалите все файлы metadata.json и metadata.abs из ваших папок {0}.",
"LabelRowsPerPage": "Строк на странице",
"LabelSearchTerm": "Поисковый запрос",
"LabelSearchTitle": "Поиск по названию",
"LabelSearchTitleOrASIN": "Поиск по названию или ASIN",
"LabelSeason": "Сезон",
+ "LabelSeasonNumber": "Сезон #{0}",
"LabelSelectAll": "Выбрать все",
"LabelSelectAllEpisodes": "Выбрать все эпизоды",
"LabelSelectEpisodesShowing": "Выберите {0} эпизодов для показа",
"LabelSelectUsers": "Выбор пользователей",
"LabelSendEbookToDevice": "Отправить e-книгу в...",
"LabelSequence": "Последовательность",
+ "LabelSerial": "Серийный",
"LabelSeries": "Серия",
"LabelSeriesName": "Имя серии",
"LabelSeriesProgress": "Прогресс серии",
+ "LabelServerLogLevel": "Уровень журнала сервера",
"LabelServerYearReview": "Итоги года всего сервера ({0})",
"LabelSetEbookAsPrimary": "Установить как основную",
"LabelSetEbookAsSupplementary": "Установить как дополнительную",
@@ -522,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Серии, в которых всего одна книга, будут скрыты со страницы серий и полок домашней страницы.",
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице",
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Процент выполнения больше, чем",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Оставшееся время составляет менее (секунд)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Отметьте мультимедийный элемент как законченный, когда",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропустить предыдущие книги в \"Продолжить серию\"",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "На домашней странице \"Продолжить серию\" отображается первая книга, не начатая в серии, в которой закончена хотя бы одна книга и нет начатых книг. При включении этого параметра серия будет продолжена с самой последней завершенной книги, а не с первой, которая не начата.",
"LabelSettingsParseSubtitles": "Разбор подзаголовков",
@@ -544,7 +588,7 @@
"LabelShareURL": "Общедоступный URL",
"LabelShowAll": "Показать все",
"LabelShowSeconds": "Отображать секунды",
- "LabelShowSubtitles": "Показать субтитры",
+ "LabelShowSubtitles": "Показать серии",
"LabelSize": "Размер",
"LabelSleepTimer": "Таймер сна",
"LabelSlug": "Слизень",
@@ -586,13 +630,15 @@
"LabelTimeDurationXMinutes": "{0} минут",
"LabelTimeDurationXSeconds": "{0} секунд",
"LabelTimeInMinutes": "Время в минутах",
+ "LabelTimeLeft": "{0} осталось",
"LabelTimeListened": "Время прослушивания",
"LabelTimeListenedToday": "Время прослушивания сегодня",
"LabelTimeRemaining": "{0} осталось",
- "LabelTimeToShift": "Время смещения в сек.",
+ "LabelTimeToShift": "Время смещения в секундах",
"LabelTitle": "Название",
"LabelToolsEmbedMetadata": "Встроить метаданные",
"LabelToolsEmbedMetadataDescription": "Встроить метаданные в аудио файлы, включая обложку и главы.",
+ "LabelToolsM4bEncoder": "Кодировщик M4B",
"LabelToolsMakeM4b": "Создать M4B файл аудиокниги",
"LabelToolsMakeM4bDescription": "Создает .M4B файл аудиокниги с встроенными метаданными, обложкой и главами.",
"LabelToolsSplitM4b": "Разделить M4B на MP3 файлы",
@@ -605,6 +651,7 @@
"LabelTracksMultiTrack": "Мультитрек",
"LabelTracksNone": "Нет треков",
"LabelTracksSingleTrack": "Один трек",
+ "LabelTrailer": "Трейлер",
"LabelType": "Тип",
"LabelUnabridged": "Полное издание",
"LabelUndo": "Отменить",
@@ -618,8 +665,10 @@
"LabelUploaderDragAndDrop": "Перетащите файлы или каталоги",
"LabelUploaderDropFiles": "Перетащите файлы",
"LabelUploaderItemFetchMetadataHelp": "Автоматическое извлечение названия, автора и серии",
+ "LabelUseAdvancedOptions": "Используйте расширенные опции",
"LabelUseChapterTrack": "Показывать время главы",
"LabelUseFullTrack": "Показывать время книги",
+ "LabelUseZeroForUnlimited": "Используйте 0 для неограниченного количества",
"LabelUser": "Пользователь",
"LabelUsername": "Имя пользователя",
"LabelValue": "Значение",
@@ -666,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "Вы уверены, что хотите удалить пользовательский поставщик метаданных \"{0}\"?",
"MessageConfirmDeleteNotification": "Вы уверены, что хотите удалить это уведомление?",
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "Вы уверены, что хотите вставить метаданные в {0} аудиофайлов?",
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?",
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?",
@@ -677,6 +727,7 @@
"MessageConfirmPurgeCache": "Очистка кэша удалит весь каталог в /metadata/cache
. Вы уверены, что хотите удалить каталог кэша?",
"MessageConfirmPurgeItemsCache": "Очистка кэша элементов удалит весь каталог в /metadata/cache/items
. Вы уверены?",
"MessageConfirmQuickEmbed": "Предупреждение! Быстрое встраивание не позволяет создавать резервные копии аудиофайлов. Убедитесь, что у вас есть резервная копия аудиофайлов. Хотите продолжить?",
+ "MessageConfirmQuickMatchEpisodes": "При обнаружении совпадений информация о эпизодах быстрого поиска будет перезаписана. Будут обновлены только несопоставимые эпизоды. Вы уверены?",
"MessageConfirmReScanLibraryItems": "Вы уверены, что хотите пересканировать {0} элементов?",
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
"MessageConfirmRemoveAuthor": "Вы уверены, что хотите удалить автора \"{0}\"?",
@@ -684,6 +735,7 @@
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?",
"MessageConfirmRemoveListeningSessions": "Вы уверены, что хотите удалить {0} сеансов прослушивания?",
+ "MessageConfirmRemoveMetadataFiles": "Вы уверены, что хотите удалить все файлы metadata. {0} файлов из папок элементов вашей библиотеки?",
"MessageConfirmRemoveNarrator": "Вы уверены, что хотите удалить чтеца \"{0}\"?",
"MessageConfirmRemovePlaylist": "Вы уверены, что хотите удалить плейлист \"{0}\"?",
"MessageConfirmRenameGenre": "Вы уверены, что хотите переименовать жанр \"{0}\" в \"{1}\" для всех элементов?",
@@ -699,6 +751,7 @@
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
"MessageEmbedFailed": "Вставка не удалась!",
"MessageEmbedFinished": "Встраивание завершено!",
+ "MessageEmbedQueue": "Поставлен в очередь для внедрения метаданных ({0} в очереди)",
"MessageEpisodesQueuedForDownload": "{0} Эпизод(ов) запланировано для закачки",
"MessageEreaderDevices": "Чтобы обеспечить доставку электронных книг, вам может потребоваться добавить указанный выше адрес электронной почты в качестве действительного отправителя для каждого устройства, перечисленного ниже.",
"MessageFeedURLWillBe": "URL канала будет {0}",
@@ -743,6 +796,7 @@
"MessageNoLogs": "Нет логов",
"MessageNoMediaProgress": "Нет прогресса медиа",
"MessageNoNotifications": "Нет уведомлений",
+ "MessageNoPodcastFeed": "Недопустимый подкаст: Нет канала",
"MessageNoPodcastsFound": "Подкасты не найдены",
"MessageNoResults": "Нет результатов",
"MessageNoSearchResultsFor": "Нет результатов поиска для \"{0}\"",
@@ -759,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Создать плейлист из коллекции",
"MessagePleaseWait": "Пожалуйста подождите...",
"MessagePodcastHasNoRSSFeedForMatching": "Подкаст не имеет URL-адреса RSS-канала, который можно использовать для поиска",
+ "MessagePodcastSearchField": "Введите поисковый запрос или URL-адрес RSS-канала",
+ "MessageQuickEmbedInProgress": "Быстрое внедрение в процессе выполнения",
+ "MessageQuickEmbedQueue": "Поставлен в очередь для быстрого внедрения ({0} в очереди)",
+ "MessageQuickMatchAllEpisodes": "Быстрое сопоставление всех эпизодов",
"MessageQuickMatchDescription": "Заполняет пустые детали элемента и обложку первым результатом поиска из «{0}». Не перезаписывает сведения, если не включен параметр сервера 'Предпочитать метаданные поиска'.",
"MessageRemoveChapter": "Удалить главу",
"MessageRemoveEpisodes": "Удалить {0} эпизод(ов)",
@@ -776,6 +834,41 @@
"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": "Успешно загружено!",
@@ -793,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Папки с медиафайлами будут обрабатываться как отдельные элементы библиотеки.",
"NoteUploaderOnlyAudioFiles": "Если загружать только аудиофайлы, то каждый аудиофайл будет обрабатываться как отдельная аудиокнига.",
"NoteUploaderUnsupportedFiles": "Неподдерживаемые файлы игнорируются. При выборе или удалении папки другие файлы, не находящиеся в папке элемента, игнорируются.",
+ "NotificationOnBackupCompletedDescription": "Запускается при завершении резервного копирования",
+ "NotificationOnBackupFailedDescription": "Срабатывает при сбое резервного копирования",
+ "NotificationOnEpisodeDownloadedDescription": "Запускается при автоматической загрузке эпизода подкаста",
+ "NotificationOnTestDescription": "Событие для тестирования системы оповещения",
"PlaceholderNewCollection": "Новое имя коллекции",
"PlaceholderNewFolderPath": "Путь к новой папке",
"PlaceholderNewPlaylist": "Новое название плейлиста",
@@ -816,14 +913,13 @@
"StatsTopNarrators": "ТОП ЧТЕЦЫ",
"StatsTotalDuration": "С общей продолжительностью…",
"StatsYearInReview": "ИТОГИ ГОДА",
- "ToastAccountUpdateFailed": "Не удалось обновить учетную запись",
"ToastAccountUpdateSuccess": "Учетная запись обновлена",
"ToastAppriseUrlRequired": "Необходимо ввести URL-адрес Apprise",
+ "ToastAsinRequired": "Требуется ASIN",
"ToastAuthorImageRemoveSuccess": "Изображение автора удалено",
"ToastAuthorNotFound": "Автор \"{0}\" не найден",
"ToastAuthorRemoveSuccess": "Автор удален",
"ToastAuthorSearchNotFound": "Автор не найден",
- "ToastAuthorUpdateFailed": "Не удалось обновить автора",
"ToastAuthorUpdateMerged": "Автор объединен",
"ToastAuthorUpdateSuccess": "Автор обновлен",
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновлен (изображение не найдено)",
@@ -834,29 +930,29 @@
"ToastBackupDeleteSuccess": "Бэкап удален",
"ToastBackupInvalidMaxKeep": "Недопустимое количество резервных копий для хранения",
"ToastBackupInvalidMaxSize": "Недопустимый максимальный размер резервной копии",
- "ToastBackupPathUpdateFailed": "Не удалось обновить путь к резервному копированию",
"ToastBackupRestoreFailed": "Не удалось восстановить из бэкапа",
"ToastBackupUploadFailed": "Не удалось загрузить бэкап",
"ToastBackupUploadSuccess": "Бэкап загружен",
"ToastBatchDeleteFailed": "Не удалось выполнить пакетное удаление",
"ToastBatchDeleteSuccess": "Успешное пакетное удаление",
+ "ToastBatchQuickMatchFailed": "Не удалось выполнить пакетное быстрое сопоставление!",
+ "ToastBatchQuickMatchStarted": "Начато пакетное быстрое сопоставление {0} книг!",
"ToastBatchUpdateFailed": "Сбой пакетного обновления",
"ToastBatchUpdateSuccess": "Успешное пакетное обновление",
"ToastBookmarkCreateFailed": "Не удалось создать закладку",
"ToastBookmarkCreateSuccess": "Добавлена закладка",
"ToastBookmarkRemoveSuccess": "Закладка удалена",
- "ToastBookmarkUpdateFailed": "Не удалось обновить закладку",
"ToastBookmarkUpdateSuccess": "Закладка обновлена",
"ToastCachePurgeFailed": "Не удалось очистить кэш",
"ToastCachePurgeSuccess": "Кэш успешно очищен",
"ToastChaptersHaveErrors": "Главы имеют ошибки",
"ToastChaptersMustHaveTitles": "Главы должны содержать названия",
"ToastChaptersRemoved": "Удалены главы",
+ "ToastChaptersUpdated": "Обновленные главы",
"ToastCollectionItemsAddFailed": "Не удалось добавить элемент(ы) в коллекцию",
"ToastCollectionItemsAddSuccess": "Элемент(ы) добавлены в коллекцию",
"ToastCollectionItemsRemoveSuccess": "Элемент(ы), удалены из коллекции",
"ToastCollectionRemoveSuccess": "Коллекция удалена",
- "ToastCollectionUpdateFailed": "Не удалось обновить коллекцию",
"ToastCollectionUpdateSuccess": "Коллекция обновлена",
"ToastCoverUpdateFailed": "Не удалось обновить обложку",
"ToastDeleteFileFailed": "Не удалось удалить файл",
@@ -865,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "Устройство для чтения электронных книг с таким именем уже существует",
"ToastDeviceTestEmailFailed": "Не удалось отправить тестовое электронное письмо",
"ToastDeviceTestEmailSuccess": "Тестовое письмо отправлено",
- "ToastDeviceUpdateFailed": "Не удалось обновить устройство",
- "ToastEmailSettingsUpdateFailed": "Не удалось обновить настройки электронной почты",
"ToastEmailSettingsUpdateSuccess": "Обновлены настройки электронной почты",
"ToastEncodeCancelFailed": "Не удалось отменить кодирование",
"ToastEncodeCancelSucces": "Кодирование отменено",
"ToastEpisodeDownloadQueueClearFailed": "Не удалось очистить очередь",
"ToastEpisodeDownloadQueueClearSuccess": "Очередь загрузки эпизода очищена",
+ "ToastEpisodeUpdateSuccess": "{0 эпизодов обновлено",
"ToastErrorCannotShare": "Невозможно предоставить общий доступ на этом устройстве",
"ToastFailedToLoadData": "Не удалось загрузить данные",
+ "ToastFailedToMatch": "Не удалось найти совпадения",
"ToastFailedToShare": "Не удалось поделиться",
- "ToastFailedToUpdateAccount": "Не удалось обновить учетную запись",
- "ToastFailedToUpdateUser": "Не удалось обновить пользователя",
+ "ToastFailedToUpdate": "Не удалось обновить",
"ToastInvalidImageUrl": "Неверный URL изображения",
+ "ToastInvalidMaxEpisodesToDownload": "Недопустимое максимальное количество загружаемых эпизодов",
"ToastInvalidUrl": "Неверный URL",
- "ToastItemCoverUpdateFailed": "Не удалось обновить обложку элемента",
"ToastItemCoverUpdateSuccess": "Обложка элемента обновлена",
"ToastItemDeletedFailed": "Не удалось удалить элемент",
"ToastItemDeletedSuccess": "Удаленный элемент",
- "ToastItemDetailsUpdateFailed": "Не удалось обновить сведения об элементе",
"ToastItemDetailsUpdateSuccess": "Обновлены сведения об элементе",
"ToastItemMarkedAsFinishedFailed": "Не удалось пометить как Завершенный",
"ToastItemMarkedAsFinishedSuccess": "Элемент помечен как Завершенный",
"ToastItemMarkedAsNotFinishedFailed": "Не удалось пометить как Незавершенный",
"ToastItemMarkedAsNotFinishedSuccess": "Элемент помечен как Незавершенный",
- "ToastItemUpdateFailed": "Не удалось обновить элемент",
"ToastItemUpdateSuccess": "Элемент обновлен",
"ToastLibraryCreateFailed": "Не удалось создать библиотеку",
"ToastLibraryCreateSuccess": "Библиотека \"{0}\" создана",
@@ -897,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Библиотека удалена",
"ToastLibraryScanFailedToStart": "Не удалось запустить сканирование",
"ToastLibraryScanStarted": "Запущено сканирование библиотеки",
- "ToastLibraryUpdateFailed": "Не удалось обновить библиотеку",
"ToastLibraryUpdateSuccess": "Библиотека \"{0}\" обновлена",
+ "ToastMatchAllAuthorsFailed": "Не удалось найти совпадения со всеми авторами",
+ "ToastMetadataFilesRemovedError": "Ошибка при удалении файлов metadata.{0}",
+ "ToastMetadataFilesRemovedNoneFound": "В библиотеке не найдено файлов metadata.{0}",
+ "ToastMetadataFilesRemovedNoneRemoved": "Нет удаленных файлов metadata.{0}",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} файлов удалено",
+ "ToastMustHaveAtLeastOnePath": "Должен быть хотя бы один путь",
"ToastNameEmailRequired": "Имя и адрес электронной почты обязательны",
"ToastNameRequired": "Имя обязательно для заполнения",
+ "ToastNewEpisodesFound": "{0} новых эпизодов найдено",
"ToastNewUserCreatedFailed": "Не удалось создать учетную запись: \"{0}\"",
"ToastNewUserCreatedSuccess": "Новая учетная запись создана",
"ToastNewUserLibraryError": "Необходимо выбрать хотя бы одну библиотеку",
"ToastNewUserPasswordError": "Должен иметь пароль, только пользователь root может иметь пустой пароль",
"ToastNewUserTagError": "Необходимо выбрать хотя бы один тег",
"ToastNewUserUsernameError": "Введите имя пользователя",
+ "ToastNoNewEpisodesFound": "Новых эпизодов не найдено",
"ToastNoUpdatesNecessary": "Обновления не требуются",
"ToastNotificationCreateFailed": "Не удалось создать уведомление",
"ToastNotificationDeleteFailed": "Не удалось удалить уведомление",
"ToastNotificationFailedMaximum": "Максимальное количество неудачных попыток должно быть >= 0",
"ToastNotificationQueueMaximum": "Максимальная очередь уведомлений должна быть >= 0",
- "ToastNotificationSettingsUpdateFailed": "Не удалось обновить настройки уведомлений",
"ToastNotificationSettingsUpdateSuccess": "Обновлены настройки уведомлений",
"ToastNotificationTestTriggerFailed": "Не удалось активировать тестовое уведомление",
"ToastNotificationTestTriggerSuccess": "Сработавшее уведомление о тестировании",
- "ToastNotificationUpdateFailed": "Не удалось обновить уведомление",
"ToastNotificationUpdateSuccess": "Уведомление обновлено",
"ToastPlaylistCreateFailed": "Не удалось создать плейлист",
"ToastPlaylistCreateSuccess": "Плейлист создан",
"ToastPlaylistRemoveSuccess": "Плейлист удален",
- "ToastPlaylistUpdateFailed": "Не удалось обновить плейлист",
"ToastPlaylistUpdateSuccess": "Плейлист обновлен",
"ToastPodcastCreateFailed": "Не удалось создать подкаст",
"ToastPodcastCreateSuccess": "Подкаст успешно создан",
"ToastPodcastGetFeedFailed": "Не удалось получить ленту подкастов",
"ToastPodcastNoEpisodesInFeed": "В RSS-ленте эпизодов не найдено",
"ToastPodcastNoRssFeed": "В подкасте нет RSS-канала",
+ "ToastProgressIsNotBeingSynced": "Прогресс не синхронизируется, перезапустите воспроизведение",
"ToastProviderCreatedFailed": "Не удалось добавить провайдера",
"ToastProviderCreatedSuccess": "Добавлен новый провайдер",
"ToastProviderNameAndUrlRequired": "Имя и URL обязательные",
@@ -950,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "E-книга отправлена на устройство \"{0}\"",
"ToastSeriesUpdateFailed": "Не удалось обновить серию",
"ToastSeriesUpdateSuccess": "Успешное обновление серии",
- "ToastServerSettingsUpdateFailed": "Не удалось обновить настройки сервера",
"ToastServerSettingsUpdateSuccess": "Обновлены настройки сервера",
"ToastSessionCloseFailed": "Не удалось закрыть сеанс",
"ToastSessionDeleteFailed": "Не удалось удалить сеанс",
"ToastSessionDeleteSuccess": "Сеанс удален",
+ "ToastSleepTimerDone": "Выполнен таймер сна... Хр-р-р-р",
"ToastSlugMustChange": "Slug содержит недопустимые символы",
"ToastSlugRequired": "Требуется Slug",
"ToastSocketConnected": "Сокет подключен",
"ToastSocketDisconnected": "Сокет отключен",
"ToastSocketFailedToConnect": "Не удалось подключить сокет",
"ToastSortingPrefixesEmptyError": "Должен быть хотя бы 1 префикс сортировки",
- "ToastSortingPrefixesUpdateFailed": "Не удалось обновить префиксы сортировки",
"ToastSortingPrefixesUpdateSuccess": "Обновлены префиксы сортировки ({0} элементов)",
"ToastTitleRequired": "Название обязательно",
"ToastUnknownError": "Неизвестная ошибка",
diff --git a/client/strings/sl.json b/client/strings/sl.json
index b6ae7f5219..366c8479b3 100644
--- a/client/strings/sl.json
+++ b/client/strings/sl.json
@@ -56,6 +56,7 @@
"ButtonOpenManager": "Odpri upravljanje",
"ButtonPause": "Premor",
"ButtonPlay": "Predvajaj",
+ "ButtonPlayAll": "Predvajaj vse",
"ButtonPlaying": "Predvajam",
"ButtonPlaylists": "Seznami predvajanj",
"ButtonPrevious": "Prejšnje",
@@ -65,6 +66,7 @@
"ButtonPurgeItemsCache": "Počisti predpomnilnik elementov",
"ButtonQueueAddItem": "Dodaj v čakalno vrsto",
"ButtonQueueRemoveItem": "Odstrani iz čakalne vrste",
+ "ButtonQuickEmbed": "Hitra vdelava",
"ButtonQuickEmbedMetadata": "Hitra vdelava metapodatkov",
"ButtonQuickMatch": "Hitro ujemanje",
"ButtonReScan": "Ponovno pregledovanje",
@@ -96,7 +98,7 @@
"ButtonStartM4BEncode": "Zaženi M4B prekodiranje",
"ButtonStartMetadataEmbed": "Začni vdelavo metapodatkov",
"ButtonStats": "Statistika",
- "ButtonSubmit": "Posreduj",
+ "ButtonSubmit": "Potrdi",
"ButtonTest": "Test",
"ButtonUnlinkOpenId": "Prekini povezavo OpenID",
"ButtonUpload": "Naloži",
@@ -133,8 +135,8 @@
"HeaderEmail": "E-pošta",
"HeaderEmailSettings": "Nastavitve e-pošte",
"HeaderEpisodes": "Epizode",
- "HeaderEreaderDevices": "Ebralne naprave",
- "HeaderEreaderSettings": "Nastavitve ebralnika",
+ "HeaderEreaderDevices": "E-bralniki",
+ "HeaderEreaderSettings": "Nastavitve e-bralnika",
"HeaderFiles": "Datoteke",
"HeaderFindChapters": "Najdi poglavja",
"HeaderIgnoredFiles": "Prezrte datoteke",
@@ -145,7 +147,7 @@
"HeaderLibraries": "Knjižnice",
"HeaderLibraryFiles": "Datoteke knjižnice",
"HeaderLibraryStats": "Statistika knjižnice",
- "HeaderListeningSessions": "Seje poslušanja",
+ "HeaderListeningSessions": "Sej poslušanja",
"HeaderListeningStats": "Statistika poslušanja",
"HeaderLogin": "Prijava",
"HeaderLogs": "Dnevniki",
@@ -160,10 +162,11 @@
"HeaderNotificationCreate": "Ustvari obvestilo",
"HeaderNotificationUpdate": "Posodobi obvestilo",
"HeaderNotifications": "Obvestila",
- "HeaderOpenIDConnectAuthentication": "Preverjanje pristnosti OpenID Connect",
+ "HeaderOpenIDConnectAuthentication": "Prijava z OpenID Connect",
+ "HeaderOpenListeningSessions": "Odprte seje poslušanja",
"HeaderOpenRSSFeed": "Odpri vir RSS",
"HeaderOtherFiles": "Ostale datoteke",
- "HeaderPasswordAuthentication": "Preverjanje pristnosti gesla",
+ "HeaderPasswordAuthentication": "Preverjanje pristnosti z geslom",
"HeaderPermissions": "Dovoljenja",
"HeaderPlayerQueue": "Čakalna vrsta predvajalnika",
"HeaderPlayerSettings": "Nastavitve predvajalnika",
@@ -178,6 +181,7 @@
"HeaderRemoveEpisodes": "Odstrani {0} epizod",
"HeaderSavedMediaProgress": "Shranjen napredek predstavnosti",
"HeaderSchedule": "Načrtovanje",
+ "HeaderScheduleEpisodeDownloads": "Načrtovanje samodejnega prenosa epizod",
"HeaderScheduleLibraryScans": "Načrtuj samodejno pregledovanje knjižnice",
"HeaderSession": "Seja",
"HeaderSetBackupSchedule": "Nastavite urnik varnostnega kopiranja",
@@ -185,7 +189,7 @@
"HeaderSettingsDisplay": "Zaslon",
"HeaderSettingsExperimental": "Eksperimentalne funkcije",
"HeaderSettingsGeneral": "Splošno",
- "HeaderSettingsScanner": "Skener",
+ "HeaderSettingsScanner": "Pregledovalnik",
"HeaderSleepTimer": "Časovnik za izklop",
"HeaderStatsLargestItems": "Največji elementi",
"HeaderStatsLongestItems": "Najdaljši elementi (ure)",
@@ -218,12 +222,16 @@
"LabelAddedAt": "Dodano ob",
"LabelAddedDate": "Dodano {0}",
"LabelAdminUsersOnly": "Samo administratorji",
- "LabelAll": "Vsi",
+ "LabelAll": "Vse",
"LabelAllUsers": "Vsi uporabniki",
"LabelAllUsersExcludingGuests": "Vsi uporabniki razen gosti",
"LabelAllUsersIncludingGuests": "Vsi uporabniki vključno z gosti",
"LabelAlreadyInYourLibrary": "Že v tvoji knjižnici",
+ "LabelApiToken": "API žeton",
"LabelAppend": "Priloži",
+ "LabelAudioBitrate": "Avdio bitna hitrost (npr. 128k)",
+ "LabelAudioChannels": "Avdio kanali (1 ali 2)",
+ "LabelAudioCodec": "Avdio kodek",
"LabelAuthor": "Avtor",
"LabelAuthorFirstLast": "Avtor (ime priimek)",
"LabelAuthorLastFirst": "Avtor (priimek, ime)",
@@ -236,7 +244,8 @@
"LabelAutoRegister": "Samodejna registracija",
"LabelAutoRegisterDescription": "Po prijavi samodejno ustvari nove uporabnike",
"LabelBackToUser": "Nazaj na uporabnika",
- "LabelBackupLocation": "Lokacija rezervne kopije",
+ "LabelBackupAudioFiles": "Varnostno kopiranje zvočnih datotek",
+ "LabelBackupLocation": "Lokacija varnostnih kopij",
"LabelBackupsEnableAutomaticBackups": "Omogoči samodejno varnostno kopiranje",
"LabelBackupsEnableAutomaticBackupsHelp": "Varnostne kopije shranjene v /metadata/backups",
"LabelBackupsMaxBackupSize": "Največja velikost varnostne kopije (v GB) (0 za neomejeno)",
@@ -244,15 +253,18 @@
"LabelBackupsNumberToKeep": "Število varnostnih kopij, ki jih je treba hraniti",
"LabelBackupsNumberToKeepHelp": "Naenkrat bo odstranjena samo ena varnostna kopija, če že imate več varnostnih kopij, jih odstranite ročno.",
"LabelBitrate": "Bitna hitrost",
- "LabelBooks": "Knjige",
+ "LabelBonus": "Bonus",
+ "LabelBooks": "knjig",
"LabelButtonText": "Besedilo gumba",
"LabelByAuthor": "od {0}",
"LabelChangePassword": "Spremeni geslo",
"LabelChannels": "Kanali",
+ "LabelChapterCount": "{0} poglavij",
"LabelChapterTitle": "Naslov poglavja",
"LabelChapters": "Poglavja",
"LabelChaptersFound": "najdenih poglavij",
"LabelClickForMoreInfo": "Klikni za več informacij",
+ "LabelClickToUseCurrentValue": "Klikni za uporabo trenutne vrednosti",
"LabelClosePlayer": "Zapri predvajalnik",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Strni serije",
@@ -302,12 +314,25 @@
"LabelEmailSettingsTestAddress": "Testiraj naslov",
"LabelEmbeddedCover": "Vdelana naslovnica",
"LabelEnable": "Omogoči",
+ "LabelEncodingBackupLocation": "Varnostna kopija vaših izvirnih zvočnih datotek bo shranjena v:",
+ "LabelEncodingChaptersNotEmbedded": "Poglavja niso vdelana v zvočne knjige z večimi sledmi.",
+ "LabelEncodingClearItemCache": "Občasno počistite predpomnilnik elementov.",
+ "LabelEncodingFinishedM4B": "Končana M4B datoteka bo shranjena v vaši mapi z zvočnimi knjigami:",
+ "LabelEncodingInfoEmbedded": "Metapodatki bodo vdelani v zvočne posnetke znotraj vaše mape zvočne knjige.",
+ "LabelEncodingStartedNavigation": "Ko se opravilo začne, lahko zapustite to stran.",
+ "LabelEncodingTimeWarning": "Enkodiranje lahko traja tudi do 30 minut.",
+ "LabelEncodingWarningAdvancedSettings": "Opozorilo: Ne posodabljajte teh nastavitev, razen če poznate možnosti ekodiranja s programom ffmpeg.",
+ "LabelEncodingWatcherDisabled": "Če ste spremljanje datotečnega sistema onemogočili, boste morali pozneje ponovno pregledati to zvočno knjigo.",
"LabelEnd": "Konec",
"LabelEndOfChapter": "Konec poglavja",
"LabelEpisode": "Epizoda",
+ "LabelEpisodeNotLinkedToRssFeed": "Epizoda ni povezana z virom RSS",
+ "LabelEpisodeNumber": "{0}. epizoda",
"LabelEpisodeTitle": "Naslov epizode",
"LabelEpisodeType": "Tip epizode",
+ "LabelEpisodeUrlFromRssFeed": "URL epizode iz vira RSS",
"LabelEpisodes": "Epizode",
+ "LabelEpisodic": "Epizodično",
"LabelExample": "Primer",
"LabelExpandSeries": "Razširi serije",
"LabelExpandSubSeries": "Razširi podserije",
@@ -335,12 +360,13 @@
"LabelFontScale": "Merilo pisave",
"LabelFontStrikethrough": "Prečrtano",
"LabelFormat": "Oblika",
+ "LabelFull": "Polno",
"LabelGenre": "Žanr",
"LabelGenres": "Žanri",
"LabelHardDeleteFile": "Trdo brisanje datoteke",
"LabelHasEbook": "Ima e-knjigo",
"LabelHasSupplementaryEbook": "Ima dodatno e-knjigo",
- "LabelHideSubtitles": "Skrij podnapise",
+ "LabelHideSubtitles": "Skrij podnaslove",
"LabelHighestPriority": "Najvišja prioriteta",
"LabelHost": "Gostitelj",
"LabelHour": "Ura",
@@ -349,7 +375,7 @@
"LabelImageURLFromTheWeb": "URL slike iz spleta",
"LabelInProgress": "V teku",
"LabelIncludeInTracklist": "Vključi v seznam skladb",
- "LabelIncomplete": "Nepopolno",
+ "LabelIncomplete": "Nedokončano",
"LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Dnevno/tedensko po meri",
"LabelIntervalEvery12Hours": "Vsakih 12 ur",
@@ -369,7 +395,7 @@
"LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja posodobljena knjiga",
"LabelLastSeen": "Nazadnje viden",
- "LabelLastTime": "Zadnji čas",
+ "LabelLastTime": "Nazadnje",
"LabelLastUpdate": "Zadnja posodobitev",
"LabelLayout": "Postavitev",
"LabelLayoutSinglePage": "Ena stran",
@@ -381,7 +407,7 @@
"LabelLibraryItem": "Element knjižnice",
"LabelLibraryName": "Ime knjižnice",
"LabelLimit": "Omejitev",
- "LabelLineSpacing": "Razmik med vrsticami",
+ "LabelLineSpacing": "Vrstični razmak",
"LabelListenAgain": "Poslušaj znova",
"LabelLogLevelDebug": "Odpravljanje napak",
"LabelLogLevelInfo": "Info",
@@ -390,6 +416,10 @@
"LabelLowestPriority": "Najnižja prioriteta",
"LabelMatchExistingUsersBy": "Poveži obstoječe uporabnike po",
"LabelMatchExistingUsersByDescription": "Uporablja se za povezovanje obstoječih uporabnikov. Ko se vzpostavi povezava, se bodo uporabniki ujemali z enoličnim ID-jem vašega ponudnika SSO",
+ "LabelMaxEpisodesToDownload": "Največje število epizod za prenos. Uporabite 0 za neomejeno.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Največje število novih epizod za prenos ob preverjanju",
+ "LabelMaxEpisodesToKeep": "Največje število epizod, ki jih lahko obdržite",
+ "LabelMaxEpisodesToKeepHelp": "Vrednost 0 ne omejuje navišjega števila. Ko se nova epizoda samodejno prenese, se bo izbrisala najstarejša epizoda, če imate več kot X epizod. S tem boste izbrisali samo 1 epizodo na nov prenos.",
"LabelMediaPlayer": "Medijski predvajalnik",
"LabelMediaType": "Vrsta medija",
"LabelMetaTag": "Meta oznaka",
@@ -399,8 +429,8 @@
"LabelMinute": "Minuta",
"LabelMinutes": "Minute",
"LabelMissing": "Manjkajoče",
- "LabelMissingEbook": "Nima nobene eknjige",
- "LabelMissingSupplementaryEbook": "Nima nobene dodatne eknjige",
+ "LabelMissingEbook": "Nima nobene e-knjige",
+ "LabelMissingSupplementaryEbook": "Nima nobene dodatne e-knjige",
"LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji",
"LabelMobileRedirectURIsDescription": "To je seznam dovoljenih veljavnih preusmeritvenih URI-jev za mobilne aplikacije. Privzeti je audiobookshelf://oauth
, ki ga lahko odstranite ali dopolnite z dodatnimi URI-ji za integracijo aplikacij tretjih oseb. Uporaba zvezdice (*
) kot edinega vnosa dovoljuje kateri koli URI.",
"LabelMore": "Več",
@@ -421,7 +451,7 @@
"LabelNotes": "Opombe",
"LabelNotificationAppriseURL": "Apprise URL(ji)",
"LabelNotificationAvailableVariables": "Razpoložljive spremenljivke",
- "LabelNotificationBodyTemplate": "Predloga telesa",
+ "LabelNotificationBodyTemplate": "Predloga vsebime",
"LabelNotificationEvent": "Dogodek obvestila",
"LabelNotificationTitleTemplate": "Predloga naslova",
"LabelNotificationsMaxFailedAttempts": "Najvišje število neuspelih poskusov",
@@ -429,18 +459,20 @@
"LabelNotificationsMaxQueueSize": "Največja velikost čakalne vrste za dogodke obvestil",
"LabelNotificationsMaxQueueSizeHelp": "Dogodki so omejeni na sprožitev 1 na sekundo. Dogodki bodo prezrti, če je čakalna vrsta najvišja. To preprečuje neželeno pošiljanje obvestil.",
"LabelNumberOfBooks": "Število knjig",
- "LabelNumberOfEpisodes": "# od epizod",
+ "LabelNumberOfEpisodes": "število epizod",
"LabelOpenIDAdvancedPermsClaimDescription": "Ime zahtevka OpenID, ki vsebuje napredna dovoljenja za uporabniška dejanja v aplikaciji, ki bodo veljala za neskrbniške vloge (če je konfigurirano ). Če trditev manjka v odgovoru, bo dostop do ABS zavrnjen. Če ena možnost manjka, bo obravnavana kot false
. Zagotovite, da se zahtevek ponudnika identitete ujema s pričakovano strukturo:",
"LabelOpenIDClaims": "Pustite naslednje možnosti prazne, da onemogočite napredno dodeljevanje skupin in dovoljenj, nato pa samodejno dodelite skupino 'Uporabnik'.",
"LabelOpenIDGroupClaimDescription": "Ime zahtevka OpenID, ki vsebuje seznam uporabnikovih skupin. Običajno imenovane skupine
. Če je konfigurirana , bo aplikacija samodejno dodelila vloge na podlagi članstva v skupini uporabnika, pod pogojem, da so te skupine v zahtevku poimenovane 'admin', 'user' ali 'guest' brez razlikovanja med velikimi in malimi črkami. Zahtevek mora vsebovati seznam in če uporabnik pripada več skupinam, mu aplikacija dodeli vlogo, ki ustreza najvišjemu nivoju dostopa. Če se nobena skupina ne ujema, bo dostop zavrnjen.",
"LabelOpenRSSFeed": "Odpri vir RSS",
"LabelOverwrite": "Prepiši",
+ "LabelPaginationPageXOfY": "Stran {0} od {1}",
"LabelPassword": "Geslo",
"LabelPath": "Pot",
"LabelPermanent": "Trajno",
"LabelPermissionsAccessAllLibraries": "Lahko dostopa do vseh knjižnic",
"LabelPermissionsAccessAllTags": "Lahko dostopa do vseh oznak",
"LabelPermissionsAccessExplicitContent": "Lahko dostopa do eksplicitne vsebine",
+ "LabelPermissionsCreateEreader": "Lahko ustvari e-bralnik",
"LabelPermissionsDelete": "Lahko briše",
"LabelPermissionsDownload": "Lahko prenaša",
"LabelPermissionsUpdate": "Lahko posodablja",
@@ -462,10 +494,12 @@
"LabelProvider": "Ponudnik",
"LabelProviderAuthorizationValue": "Vrednost glave avtorizacije",
"LabelPubDate": "Datum objave",
- "LabelPublishYear": "Leto objave",
+ "LabelPublishYear": "Leto izdaje",
"LabelPublishedDate": "Objavljeno {0}",
- "LabelPublisher": "Založnik",
- "LabelPublishers": "Založniki",
+ "LabelPublishedDecade": "Desetletje izdaje",
+ "LabelPublishedDecades": "Desetletja izdaje",
+ "LabelPublisher": "Izdajatelj",
+ "LabelPublishers": "Izdajatelji",
"LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri",
"LabelRSSFeedCustomOwnerName": "Ime lastnika po meri",
"LabelRSSFeedOpen": "Odprt vir RSS",
@@ -483,21 +517,28 @@
"LabelRedo": "Ponovi",
"LabelRegion": "Regija",
"LabelReleaseDate": "Datum izdaje",
+ "LabelRemoveAllMetadataAbs": "Odstrani vse datoteke metadata.abs",
+ "LabelRemoveAllMetadataJson": "Odstrani vse datoteke metadata.json",
"LabelRemoveCover": "Odstrani naslovnico",
+ "LabelRemoveMetadataFile": "Odstrani datoteke z metapodatki v mapah elementov knjižnice",
+ "LabelRemoveMetadataFileHelp": "Odstrani vse datoteke metadata.json in metadata.abs v svojih mapah {0}.",
"LabelRowsPerPage": "Vrstic na stran",
"LabelSearchTerm": "Iskalni pojem",
"LabelSearchTitle": "Naslov iskanja",
"LabelSearchTitleOrASIN": "Naslov iskanja ali ASIN",
"LabelSeason": "Sezona",
+ "LabelSeasonNumber": "Sezona #{0}",
"LabelSelectAll": "Izberite vse",
"LabelSelectAllEpisodes": "Izberite vse epizode",
"LabelSelectEpisodesShowing": "Izberi {0} prikazanih epizod",
"LabelSelectUsers": "Izberite uporabnike",
"LabelSendEbookToDevice": "Pošlji eknjigo k...",
"LabelSequence": "Zaporedje",
+ "LabelSerial": "Serija",
"LabelSeries": "Serije",
"LabelSeriesName": "Ime serije",
"LabelSeriesProgress": "Napredek serije",
+ "LabelServerLogLevel": "Raven dnevnika strežnika",
"LabelServerYearReview": "Pregled leta strežnika ({0})",
"LabelSetEbookAsPrimary": "Nastavi kot primarno",
"LabelSetEbookAsSupplementary": "Nastavi kot dodatno",
@@ -506,11 +547,11 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami",
"LabelSettingsChromecastSupport": "Podpora za Chromecast",
"LabelSettingsDateFormat": "Oblika datuma",
- "LabelSettingsDisableWatcher": "Onemogoči pregledovalca",
- "LabelSettingsDisableWatcherForLibrary": "Onemogoči pregledovalca map za knjižnico",
+ "LabelSettingsDisableWatcher": "Onemogoči spremljanje datotečnega sistema",
+ "LabelSettingsDisableWatcherForLibrary": "Onemogoči spremljanje map za knjižnico",
"LabelSettingsDisableWatcherHelp": "Onemogoči samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
- "LabelSettingsEnableWatcher": "Omogoči pregledovalca",
- "LabelSettingsEnableWatcherForLibrary": "Omogoči pregledovalca map za knjižnico",
+ "LabelSettingsEnableWatcher": "Omogoči spremljanje sprememb",
+ "LabelSettingsEnableWatcherForLibrary": "Omogoči spremljanje sprememb v mapi knjižnice",
"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.",
@@ -522,15 +563,18 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serije, ki imajo eno knjigo, bodo skrite na strani serije in policah domače strani.",
"LabelSettingsHomePageBookshelfView": "Domača stran bo imela pogled knjižne police",
"LabelSettingsLibraryBookshelfView": "Knjižnična uporaba pogleda knjižne police",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Odstotek dokončanega je večji od",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Preostali čas je manj kot (sekund)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Označi medijski element kot končan, ko",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Preskoči prejšnje knjige v nadaljevanju serije",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polica z domačo stranjo Nadaljuj serijo prikazuje prvo nezačeto knjigo v seriji, ki ima vsaj eno dokončano knjigo in ni nobene knjige v teku. Če omogočite to nastavitev, se bo serija nadaljevala od najbolj dokončane knjige namesto od prve nezačete knjige.",
- "LabelSettingsParseSubtitles": "Uporabi podnapise",
- "LabelSettingsParseSubtitlesHelp": "Izvleci podnapise iz imen map zvočnih knjig. Podnaslov mora biti ločen z \" - \" npr. »Naslov knjige – Tu podnapis« ima podnaslov »Tu podnapis«",
+ "LabelSettingsParseSubtitles": "Razčleni podnaslove",
+ "LabelSettingsParseSubtitlesHelp": "Izvleci padnaslove iz imen map zvočnih knjig. Podnaslov mora biti ločen z \" - \" npr. \"Naslov knjige – tu podnaslove\" ima podnaslov \"tu podnaslov\"",
"LabelSettingsPreferMatchedMetadata": "Prednost imajo ujemajoči se metapodatki",
"LabelSettingsPreferMatchedMetadataHelp": "Pri uporabi hitrega ujemanja bodo ujemajoči se podatki preglasili podrobnosti artikla. Hitro ujemanje bo privzeto izpolnil samo manjkajoče podrobnosti.",
"LabelSettingsSkipMatchingBooksWithASIN": "Preskoči ujemajoče se knjige, ki že imajo ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Preskoči ujemajoče se knjige, ki že imajo oznako ISBN",
- "LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevajte predpon",
+ "LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevaj predpon",
"LabelSettingsSortingIgnorePrefixesHelp": "npr. za naslov knjige s predpono \"the\" bi se \"The Book Title\" razvrstil kot \"Book Title, The\"",
"LabelSettingsSquareBookCovers": "Uporabi kvadratne platnice knjig",
"LabelSettingsSquareBookCoversHelp": "Raje uporabi kvadratne platnice kot standardne knjižne platnice 1.6:1",
@@ -544,12 +588,12 @@
"LabelShareURL": "Deli URL",
"LabelShowAll": "Prikaži vse",
"LabelShowSeconds": "Prikaži sekunde",
- "LabelShowSubtitles": "Prikaži podnapise",
+ "LabelShowSubtitles": "Prikaži podnaslove",
"LabelSize": "Velikost",
"LabelSleepTimer": "Časovnik za spanje",
"LabelSlug": "Slug",
"LabelStart": "Začetek",
- "LabelStartTime": "Začetni čas",
+ "LabelStartTime": "Čas začetka",
"LabelStarted": "Začeto",
"LabelStartedAt": "Začeto ob",
"LabelStatsAudioTracks": "Zvočni posnetki",
@@ -557,17 +601,17 @@
"LabelStatsBestDay": "Najboljši dan",
"LabelStatsDailyAverage": "Dnevno povprečje",
"LabelStatsDays": "Dnevi",
- "LabelStatsDaysListened": "Poslušani dnevi",
+ "LabelStatsDaysListened": "Dnevi poslušanja",
"LabelStatsHours": "Ure",
"LabelStatsInARow": "v vrsti",
"LabelStatsItemsFinished": "Končani elementi",
"LabelStatsItemsInLibrary": "Elementi v knjižnici",
"LabelStatsMinutes": "minute",
- "LabelStatsMinutesListening": "Poslušane minute",
+ "LabelStatsMinutesListening": "Minut poslušanja",
"LabelStatsOverallDays": "Skupaj dnevi",
- "LabelStatsOverallHours": "Skupaj ure",
+ "LabelStatsOverallHours": "Skupaj ur",
"LabelStatsWeekListening": "Tednov poslušanja",
- "LabelSubtitle": "Podnapis",
+ "LabelSubtitle": "Podnaslov",
"LabelSupportedFileTypes": "Podprte vrste datotek",
"LabelTag": "Oznaka",
"LabelTags": "Oznake",
@@ -581,11 +625,12 @@
"LabelTheme": "Tema",
"LabelThemeDark": "Temna",
"LabelThemeLight": "Svetla",
- "LabelTimeBase": "Odvisna od časa",
+ "LabelTimeBase": "Osnovni čas",
"LabelTimeDurationXHours": "{0} ur",
"LabelTimeDurationXMinutes": "{0} minut",
"LabelTimeDurationXSeconds": "{0} sekund",
"LabelTimeInMinutes": "Čas v minutah",
+ "LabelTimeLeft": "{0} še preostane",
"LabelTimeListened": "Čas poslušanja",
"LabelTimeListenedToday": "Čas poslušanja danes",
"LabelTimeRemaining": "Še {0}",
@@ -593,8 +638,9 @@
"LabelTitle": "Naslov",
"LabelToolsEmbedMetadata": "Vdelaj metapodatke",
"LabelToolsEmbedMetadataDescription": "Vdelajte metapodatke v zvočne datoteke, vključno s sliko naslovnice in poglavji.",
- "LabelToolsMakeM4b": "Ustvari datoteko zvočne knjige M4B",
- "LabelToolsMakeM4bDescription": "Ustvarite datoteko zvočne knjige .M4B z vdelanimi metapodatki, sliko naslovnice in poglavji.",
+ "LabelToolsM4bEncoder": "M4B enkoder",
+ "LabelToolsMakeM4b": "Ustvari M4B datoteko zvočne knjige",
+ "LabelToolsMakeM4bDescription": "Ustvari zvočno knjigo v .M4B obliki z vdelanimi metapodatki, sliko naslovnice in poglavji.",
"LabelToolsSplitM4b": "Razdeli M4B v MP3 datoteke",
"LabelToolsSplitM4bDescription": "Ustvarite MP3 datoteke iz datoteke M4B, razdeljene po poglavjih z vdelanimi metapodatki, naslovno sliko in poglavji.",
"LabelTotalDuration": "Skupno trajanje",
@@ -605,11 +651,12 @@
"LabelTracksMultiTrack": "Več posnetkov",
"LabelTracksNone": "Brez posnetka",
"LabelTracksSingleTrack": "Enojni posnetek",
+ "LabelTrailer": "Napovednik",
"LabelType": "Vrsta",
"LabelUnabridged": "Neskrajšano",
"LabelUndo": "Razveljavi",
"LabelUnknown": "Neznano",
- "LabelUnknownPublishDate": "Neznan datum objave",
+ "LabelUnknownPublishDate": "Neznan datum izdaje",
"LabelUpdateCover": "Posodobi naslovnico",
"LabelUpdateCoverHelp": "Dovoli prepisovanje obstoječih naslovnic za izbrane knjige, ko se najde ujemanje",
"LabelUpdateDetails": "Posodobi podrobnosti",
@@ -618,8 +665,10 @@
"LabelUploaderDragAndDrop": "Povleci in spusti datoteke ali mape",
"LabelUploaderDropFiles": "Spusti datoteke",
"LabelUploaderItemFetchMetadataHelp": "Samodejno pridobi naslov, avtorja in serijo",
+ "LabelUseAdvancedOptions": "Uporabi napredne možnosti",
"LabelUseChapterTrack": "Uporabi posnetek poglavij",
"LabelUseFullTrack": "Uporabi celoten posnetek",
+ "LabelUseZeroForUnlimited": "Uporabi 0 za neomejeno",
"LabelUser": "Uporabnik",
"LabelUsername": "Uporabniško ime",
"LabelValue": "Vrednost",
@@ -633,13 +682,13 @@
"LabelXBooks": "{0} knjig",
"LabelXItems": "{0} elementov",
"LabelYearReviewHide": "Skrij pregled leta",
- "LabelYearReviewShow": "Poglej pregled leta",
+ "LabelYearReviewShow": "Poglej si pregled leta",
"LabelYourAudiobookDuration": "Trajanje tvojih zvočnih knjig",
"LabelYourBookmarks": "Tvoji zaznamki",
"LabelYourPlaylists": "Tvoje seznami predvajanj",
"LabelYourProgress": "Tvoj napredek",
"MessageAddToPlayerQueue": "Dodaj v čakalno vrsto predvajalnika",
- "MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnan primerek API Apprise ali API, ki bo obravnaval te iste zahteve. Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaš primerek API-ja postrežen na http://192.168.1.1:8337
, bi morali vnesti http://192.168.1.1:8337/notify
.",
+ "MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnano namestitev API Apprise ali API, ki bo obravnavala te iste zahteve. Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaša namestitev API-ja postrežena na http://192.168.1.1:8337
, bi morali vnesti http://192.168.1.1:8337/notify
.",
"MessageBackupsDescription": "Varnostne kopije vključujejo uporabnike, napredek uporabnikov, podrobnosti elementov knjižnice, nastavitve strežnika in slike, shranjene v /metadata/items
& /metadata/authors
. Varnostne kopije ne vključujejo datotek, shranjenih v mapah vaše knjižnice.",
"MessageBackupsLocationEditNote": "Opomba: Posodabljanje lokacije varnostne kopije ne bo premaknilo ali spremenilo obstoječih varnostnih kopij",
"MessageBackupsLocationNoEditNote": "Opomba: Lokacija varnostne kopije je nastavljena s spremenljivko okolja in je tu ni mogoče spremeniti.",
@@ -650,9 +699,9 @@
"MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo",
"MessageBookshelfNoSeries": "Nimate serij",
- "MessageChapterEndIsAfter": "Konec poglavja je za koncem vaše zvočne knjige",
+ "MessageChapterEndIsAfter": "Konec poglavja je po koncu zvočne knjige",
"MessageChapterErrorFirstNotZero": "Prvo poglavje se mora začeti pri 0",
- "MessageChapterErrorStartGteDuration": "Neveljaven začetni čas mora biti krajši od trajanja zvočne knjige",
+ "MessageChapterErrorStartGteDuration": "Neveljaven začetni čas, mora biti krajši od trajanja zvočne knjige",
"MessageChapterErrorStartLtPrev": "Neveljaven začetni čas mora biti večji od ali enak začetnemu času prejšnjega poglavja",
"MessageChapterStartIsAfter": "Začetek poglavja je po koncu vaše zvočne knjige",
"MessageCheckingCron": "Preverjam cron...",
@@ -666,7 +715,8 @@
"MessageConfirmDeleteMetadataProvider": "Ali ste prepričani, da želite izbrisati ponudnika metapodatkov po meri \"{0}\"?",
"MessageConfirmDeleteNotification": "Ali ste prepričani, da želite izbrisati to obvestilo?",
"MessageConfirmDeleteSession": "Ali ste prepričani, da želite izbrisati to sejo?",
- "MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno iskanje?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "Ali ste prepričani, da želite vdelati metapodatke v {0} zvočnih datotek?",
+ "MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno pregledovanje?",
"MessageConfirmMarkAllEpisodesFinished": "Ali ste prepričani, da želite označiti vse epizode kot dokončane?",
"MessageConfirmMarkAllEpisodesNotFinished": "Ali ste prepričani, da želite vse epizode označiti kot nedokončane?",
"MessageConfirmMarkItemFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot dokončanega?",
@@ -677,13 +727,15 @@
"MessageConfirmPurgeCache": "Čiščenje predpomnilnika bo izbrisalo celoten imenik v /metadata/cache
. Ali ste prepričani, da želite odstraniti imenik predpomnilnika?",
"MessageConfirmPurgeItemsCache": "Čiščenje predpomnilnika elementov bo izbrisalo celoten imenik na /metadata/cache/items
. Ste prepričani?",
"MessageConfirmQuickEmbed": "Opozorilo! Hitra vdelava ne bo varnostno kopirala vaših zvočnih datotek. Prepričajte se, da imate varnostno kopijo zvočnih datotek. Ali želite nadaljevati?",
- "MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno poiskati {0} elementov?",
+ "MessageConfirmQuickMatchEpisodes": "Hitro ujemanja epizod bo prepisalo podrobnosti, če se najde ujemanje. Posodobljene bodo samo epizode, ki se ne ujemajo. Ste prepričani?",
+ "MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno pregledati {0} elementov?",
"MessageConfirmRemoveAllChapters": "Ali ste prepričani, da želite odstraniti vsa poglavja?",
"MessageConfirmRemoveAuthor": "Ali ste prepričani, da želite odstraniti avtorja \"{0}\"?",
"MessageConfirmRemoveCollection": "Ali ste prepričani, da želite odstraniti zbirko \"{0}\"?",
"MessageConfirmRemoveEpisode": "Ali ste prepričani, da želite odstraniti epizodo \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Ali ste prepričani, da želite odstraniti {0} epizod?",
"MessageConfirmRemoveListeningSessions": "Ali ste prepričani, da želite odstraniti {0} sej poslušanja?",
+ "MessageConfirmRemoveMetadataFiles": "Ali ste prepričani, da želite odstraniti vse metapodatke.{0} v mapah elementov knjižnice?",
"MessageConfirmRemoveNarrator": "Ali ste prepričani, da želite odstraniti bralca \"{0}\"?",
"MessageConfirmRemovePlaylist": "Ali ste prepričani, da želite odstraniti svoj seznam predvajanja \"{0}\"?",
"MessageConfirmRenameGenre": "Ali ste prepričani, da želite preimenovati žanr \"{0}\" v \"{1}\" za vse elemente?",
@@ -699,11 +751,12 @@
"MessageDragFilesIntoTrackOrder": "Povlecite datoteke v pravilen vrstni red posnetkov",
"MessageEmbedFailed": "Vdelava ni uspela!",
"MessageEmbedFinished": "Vdelava končana!",
+ "MessageEmbedQueue": "V čakalni vrsta za vdelavo metapodatkov ({0} v čakalni vrsti)",
"MessageEpisodesQueuedForDownload": "{0} epizod v čakalni vrsti za prenos",
"MessageEreaderDevices": "Da zagotovite dostavo e-knjig, boste morda morali dodati zgornji e-poštni naslov kot veljavnega pošiljatelja za vsako spodaj navedeno napravo.",
"MessageFeedURLWillBe": "URL vira bo {0}",
"MessageFetching": "Pridobivam...",
- "MessageForceReScanDescription": "bo znova pregledal vse datoteke kot nov pregled. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.",
+ "MessageForceReScanDescription": "bo znova pregledal vse datoteke kot pregled od začetka. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.",
"MessageImportantNotice": "Pomembno obvestilo!",
"MessageInsertChapterBelow": "Spodaj vstavite poglavje",
"MessageItemsSelected": "{0} izbranih elementov",
@@ -715,12 +768,12 @@
"MessageLogsDescription": "Dnevniki so shranjeni v /metadata/logs
kot datoteke JSON. Dnevniki zrušitev so shranjeni v /metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B ni uspel!",
"MessageM4BFinished": "M4B končan!",
- "MessageMapChapterTitles": "Preslikajte naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih žigov",
+ "MessageMapChapterTitles": "Preslikaj naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih indentifikatorjev",
"MessageMarkAllEpisodesFinished": "Označi vse epizode kot končane",
"MessageMarkAllEpisodesNotFinished": "Označi vse epizode kot nedokončane",
"MessageMarkAsFinished": "Označi kot dokončano",
"MessageMarkAsNotFinished": "Označi kot nedokončano",
- "MessageMatchBooksDescription": "bo poskušal povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisujejo se pa podrobnosti.",
+ "MessageMatchBooksDescription": "bo poskušalo povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisuje čez obstoječe podatke.",
"MessageNoAudioTracks": "Ni zvočnih posnetkov",
"MessageNoAuthors": "Brez avtorjev",
"MessageNoBackups": "Brez varnostnih kopij",
@@ -743,6 +796,7 @@
"MessageNoLogs": "Ni dnevnikov",
"MessageNoMediaProgress": "Ni medijskega napredka",
"MessageNoNotifications": "Ni obvestil",
+ "MessageNoPodcastFeed": "Neveljaven podcast: Ni vira",
"MessageNoPodcastsFound": "Ni podcastov",
"MessageNoResults": "Ni rezultatov",
"MessageNoSearchResultsFor": "Ni rezultatov iskanja za \"{0}\"",
@@ -759,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Ustvari seznam predvajanja iz zbirke",
"MessagePleaseWait": "Prosim počakajte...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nima URL-ja vira RSS, ki bi ga lahko uporabil za ujemanje",
+ "MessagePodcastSearchField": "Vnesite iskalni izraz ali URL vira RSS",
+ "MessageQuickEmbedInProgress": "Hitra vdelava je v teku",
+ "MessageQuickEmbedQueue": "V čakalni vrsti za hitro vdelavo ({0} v čakalni vrsti)",
+ "MessageQuickMatchAllEpisodes": "Hitro ujemanje vseh epizod",
"MessageQuickMatchDescription": "Izpolni prazne podrobnosti elementa in naslovnico s prvim rezultatom ujemanja iz '{0}'. Ne prepiše podrobnosti, razen če je omogočena nastavitev strežnika 'Prednostno ujemajoči se metapodatki'.",
"MessageRemoveChapter": "Odstrani poglavje",
"MessageRemoveEpisodes": "Odstrani toliko epizod: {0}",
@@ -776,12 +834,47 @@
"MessageShareExpiresIn": "Poteče čez {0}",
"MessageShareURLWillBe": "URL za skupno rabo bo {0} ",
"MessageStartPlaybackAtTime": "Začni predvajanje za \"{0}\" ob {1}?",
+ "MessageTaskAudioFileNotWritable": "Zvočna datoteka \"{0}\" ni zapisljiva",
+ "MessageTaskCanceledByUser": "Nalogo je preklical uporabnik",
+ "MessageTaskDownloadingEpisodeDescription": "Prenašanje epizode \"{0}\"",
+ "MessageTaskEmbeddingMetadata": "Vdelujem metapodatke",
+ "MessageTaskEmbeddingMetadataDescription": "Vdelujem metapodatke v zvočno knjigo \"{0}\"",
+ "MessageTaskEncodingM4b": "Enkodiranje M4B",
+ "MessageTaskEncodingM4bDescription": "Enkodiranje zvočne knjige \"{0}\" v samo eno datoteko m4b",
+ "MessageTaskFailed": "Neuspešno",
+ "MessageTaskFailedToBackupAudioFile": "Varnostno kopiranje zvočne datoteke \"{0}\" ni uspelo",
+ "MessageTaskFailedToCreateCacheDirectory": "Imenika predpomnilnika ni bilo mogoče ustvariti",
+ "MessageTaskFailedToEmbedMetadataInFile": "Metapodatkov ni bilo mogoče vdelati v datoteko \"{0}\"",
+ "MessageTaskFailedToMergeAudioFiles": "Zvočnih datotek ni bilo mogoče združiti",
+ "MessageTaskFailedToMoveM4bFile": "Datoteke m4b ni bilo mogoče premakniti",
+ "MessageTaskFailedToWriteMetadataFile": "Metapodatke ni bilo mogoče zapisati v datoteke",
+ "MessageTaskMatchingBooksInLibrary": "Prepoznavam knjige v knjižnici \"{0}\"",
+ "MessageTaskNoFilesToScan": "Ni datotek za pregledovanje",
+ "MessageTaskOpmlImport": "Uvoz OPML",
+ "MessageTaskOpmlImportDescription": "Ustvarjanje podcastov iz {0} virov RSS",
+ "MessageTaskOpmlImportFeed": "Vir za uvoz OPML",
+ "MessageTaskOpmlImportFeedDescription": "Uvažanje vira RSS \"{0}\"",
+ "MessageTaskOpmlImportFeedFailed": "Vira podcasta ni bilo mogoče pridobiti",
+ "MessageTaskOpmlImportFeedPodcastDescription": "Ustvarjanje podcasta \"{0}\"",
+ "MessageTaskOpmlImportFeedPodcastExists": "Podcast že obstaja na tej poti",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Podcasta ni bilo mogoče ustvariti",
+ "MessageTaskOpmlImportFinished": "Dodanih {0} podcastov",
+ "MessageTaskOpmlParseFailed": "Datoteke OPML ni bilo mogoče razčleniti",
+ "MessageTaskOpmlParseFastFail": "Neveljavna OPMPL datoteka, oznake ni bilo mogoče najti ALI oznake ni bilo mogoče najti",
+ "MessageTaskOpmlParseNoneFound": "V datoteki OPML ni virov",
+ "MessageTaskScanItemsAdded": "{0} dodano",
+ "MessageTaskScanItemsMissing": "{0} manjka",
+ "MessageTaskScanItemsUpdated": "{0} posodobljeno",
+ "MessageTaskScanNoChangesNeeded": "Spremembe niso potrebne",
+ "MessageTaskScanningFileChanges": "Pregledovanje sprememb v datoteki \"{0}\"",
+ "MessageTaskScanningLibrary": "Pregledujem knjižnico \"{0}\"",
+ "MessageTaskTargetDirectoryNotWritable": "Ciljni imenik ni zapisljiv",
"MessageThinking": "Razmišljam...",
"MessageUploaderItemFailed": "Nalaganje ni uspelo",
"MessageUploaderItemSuccess": "Uspešno naloženo!",
"MessageUploading": "Nalaganje...",
"MessageValidCronExpression": "Veljaven cron izraz",
- "MessageWatcherIsDisabledGlobally": "Pregledovalec je globalno onemogočen v nastavitvah strežnika",
+ "MessageWatcherIsDisabledGlobally": "Spremljanje sprememb datotek je globalno onemogočeno v nastavitvah strežnika",
"MessageXLibraryIsEmpty": "{0} Knjižnica je prazna!",
"MessageYourAudiobookDurationIsLonger": "Trajanje vaše zvočne knjige je daljše od ugotovljenega trajanja",
"MessageYourAudiobookDurationIsShorter": "Trajanje vaše zvočne knjige je krajše od ugotovljenega trajanja",
@@ -793,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Mape z predstavnostnimi datotekami bodo obravnavane kot ločene postavke knjižnice.",
"NoteUploaderOnlyAudioFiles": "Če nalagate samo zvočne datoteke, bo vsaka zvočna datoteka obravnavana kot ločena zvočna knjiga.",
"NoteUploaderUnsupportedFiles": "Nepodprte datoteke so prezrte. Ko izberete ali spustite mapo, se druge datoteke, ki niso v mapi elementov, prezrejo.",
+ "NotificationOnBackupCompletedDescription": "Sproži se, ko je varnostno kopiranje končano",
+ "NotificationOnBackupFailedDescription": "Sproži se, ko varnostno kopiranje ne uspe",
+ "NotificationOnEpisodeDownloadedDescription": "Sproži se, ko se epizoda podcasta samodejno prenese",
+ "NotificationOnTestDescription": "Dogodek za testiranje sistema obveščanja",
"PlaceholderNewCollection": "Novo ime zbirke",
"PlaceholderNewFolderPath": "Pot nove mape",
"PlaceholderNewPlaylist": "Novo ime seznama predvajanja",
@@ -801,9 +898,9 @@
"StatsAuthorsAdded": "dodanih avtorjev",
"StatsBooksAdded": "dodanih knjig",
"StatsBooksAdditional": "Nekateri dodatki vključujejo…",
- "StatsBooksFinished": "končane knjige",
+ "StatsBooksFinished": "končanih knjig",
"StatsBooksFinishedThisYear": "Nekaj knjig, ki so bile dokončane letos…",
- "StatsBooksListenedTo": "poslušane knjige",
+ "StatsBooksListenedTo": "poslušanih knjig",
"StatsCollectionGrewTo": "Vaša zbirka knjig se je povečala na …",
"StatsSessions": "seje",
"StatsSpentListening": "porabil za poslušanje",
@@ -816,14 +913,13 @@
"StatsTopNarrators": "TOP BRALCI",
"StatsTotalDuration": "S skupnim trajanjem…",
"StatsYearInReview": "PREGLED LETA",
- "ToastAccountUpdateFailed": "Računa ni bilo mogoče posodobiti",
"ToastAccountUpdateSuccess": "Račun posodobljen",
"ToastAppriseUrlRequired": "Vnesti morate Apprise URL",
+ "ToastAsinRequired": "ASIN koda je obvezen podatek",
"ToastAuthorImageRemoveSuccess": "Slika avtorja je odstranjena",
"ToastAuthorNotFound": "Avtor \"{0}\" ni bil najden",
"ToastAuthorRemoveSuccess": "Avtor odstranjen",
"ToastAuthorSearchNotFound": "Ne najdem avtorja",
- "ToastAuthorUpdateFailed": "Avtorja ni bilo mogoče posodobiti",
"ToastAuthorUpdateMerged": "Avtor združen",
"ToastAuthorUpdateSuccess": "Avtor posodobljen",
"ToastAuthorUpdateSuccessNoImageFound": "Avtor posodobljen (ne najdem slike)",
@@ -834,29 +930,29 @@
"ToastBackupDeleteSuccess": "Varnostna kopija izbrisana",
"ToastBackupInvalidMaxKeep": "Neveljavno število varnostnih kopij za ohranjanje",
"ToastBackupInvalidMaxSize": "Neveljavna največja velikost varnostne kopije",
- "ToastBackupPathUpdateFailed": "Posodobitev poti varnostnih kopij ni uspela",
"ToastBackupRestoreFailed": "Varnostne kopije ni bilo mogoče obnoviti",
"ToastBackupUploadFailed": "Nalaganje varnostne kopije ni uspelo",
"ToastBackupUploadSuccess": "Varnostna kopija je naložena",
"ToastBatchDeleteFailed": "Paketno brisanje ni uspelo",
"ToastBatchDeleteSuccess": "Paketno brisanje je bilo uspešno",
+ "ToastBatchQuickMatchFailed": "Paketno hitro ujemanje ni uspelo!",
+ "ToastBatchQuickMatchStarted": "Paketno hitro ujemanje {0} knjig se je začelo!",
"ToastBatchUpdateFailed": "Paketna posodobitev ni uspela",
"ToastBatchUpdateSuccess": "Paketna posodobitev je uspela",
"ToastBookmarkCreateFailed": "Zaznamka ni bilo mogoče ustvariti",
"ToastBookmarkCreateSuccess": "Zaznamek dodan",
"ToastBookmarkRemoveSuccess": "Zaznamek odstranjen",
- "ToastBookmarkUpdateFailed": "Zaznamka ni bilo mogoče posodobiti",
"ToastBookmarkUpdateSuccess": "Zaznamek posodobljen",
"ToastCachePurgeFailed": "Čiščenje predpomnilnika ni uspelo",
"ToastCachePurgeSuccess": "Predpomnilnik je bil uspešno očiščen",
"ToastChaptersHaveErrors": "Poglavja imajo napake",
"ToastChaptersMustHaveTitles": "Poglavja morajo imeti naslove",
"ToastChaptersRemoved": "Poglavja so odstranjena",
+ "ToastChaptersUpdated": "Poglavja so posodobljena",
"ToastCollectionItemsAddFailed": "Dodajanje elementov v zbirko ni uspelo",
"ToastCollectionItemsAddSuccess": "Dodajanje elementov v zbirko je bilo uspešno",
"ToastCollectionItemsRemoveSuccess": "Elementi so bili odstranjeni iz zbirke",
"ToastCollectionRemoveSuccess": "Zbirka je bila odstranjena",
- "ToastCollectionUpdateFailed": "Zbirke ni bilo mogoče posodobiti",
"ToastCollectionUpdateSuccess": "Zbirka je bila posodobljena",
"ToastCoverUpdateFailed": "Posodobitev naslovnice ni uspela",
"ToastDeleteFileFailed": "Brisanje datoteke ni uspelo",
@@ -865,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "Elektronska naprava s tem imenom že obstaja",
"ToastDeviceTestEmailFailed": "Pošiljanje testnega e-poštnega sporočila ni uspelo",
"ToastDeviceTestEmailSuccess": "Testno e-poštno sporočilo je poslano",
- "ToastDeviceUpdateFailed": "Naprave ni bilo mogoče posodobiti",
- "ToastEmailSettingsUpdateFailed": "E-poštnih nastavitev ni bilo mogoče posodobiti",
"ToastEmailSettingsUpdateSuccess": "E-poštne nastavitve so bile posodobljene",
"ToastEncodeCancelFailed": "Napaka pri preklicu prekodiranja",
"ToastEncodeCancelSucces": "Prekodiranje prekinjeno",
"ToastEpisodeDownloadQueueClearFailed": "Čiščenje čakalne vrste ni uspelo",
"ToastEpisodeDownloadQueueClearSuccess": "Čakalna vrsta za prenos epizod je počiščena",
+ "ToastEpisodeUpdateSuccess": "Število posodobljenih epizod: {0}",
"ToastErrorCannotShare": "V tej napravi ni mogoče dati v skupno rabo",
"ToastFailedToLoadData": "Podatkov ni bilo mogoče naložiti",
+ "ToastFailedToMatch": "Ujemanje ni uspelo",
"ToastFailedToShare": "Skupna raba ni uspela",
- "ToastFailedToUpdateAccount": "Računa ni bilo mogoče posodobiti",
- "ToastFailedToUpdateUser": "Uporabnika ni bilo mogoče posodobiti",
+ "ToastFailedToUpdate": "Napaka pri posodobitvi",
"ToastInvalidImageUrl": "Neveljaven URL slike",
+ "ToastInvalidMaxEpisodesToDownload": "Neveljavno največje število epizod za prenos",
"ToastInvalidUrl": "Neveljaven URL",
- "ToastItemCoverUpdateFailed": "Naslovnice elementa ni bilo mogoče posodobiti",
"ToastItemCoverUpdateSuccess": "Naslovnica elementa je bila posodobljena",
"ToastItemDeletedFailed": "Elementa ni bilo mogoče izbrisati",
"ToastItemDeletedSuccess": "Element je bil izbrisan",
- "ToastItemDetailsUpdateFailed": "Posodobitev podrobnosti elementa ni uspela",
"ToastItemDetailsUpdateSuccess": "Podrobnosti elementa so bile posodobjene",
"ToastItemMarkedAsFinishedFailed": "Označevanje kot dokončano ni uspelo",
"ToastItemMarkedAsFinishedSuccess": "Element je označen kot dokončan",
"ToastItemMarkedAsNotFinishedFailed": "Ni bilo mogoče označiti kot nedokončano",
"ToastItemMarkedAsNotFinishedSuccess": "Element označen kot nedokončan",
- "ToastItemUpdateFailed": "Elementa ni bilo mogoče posodobiti",
"ToastItemUpdateSuccess": "Element je bil posodobljen",
"ToastLibraryCreateFailed": "Knjižnice ni bilo mogoče ustvariti",
"ToastLibraryCreateSuccess": "Knjižnica \"{0}\" je bila ustvarjena",
@@ -897,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Knjižnica je bila izbrisana",
"ToastLibraryScanFailedToStart": "Pregleda ni bilo mogoče začeti",
"ToastLibraryScanStarted": "Pregled knjižnice se je začel",
- "ToastLibraryUpdateFailed": "Knjižnice ni bilo mogoče posodobiti",
"ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" je bila posodobljena",
+ "ToastMatchAllAuthorsFailed": "Ujemanje vseh avtorjev ni bilo uspešno",
+ "ToastMetadataFilesRemovedError": "Napaka pri odstranjevanju metapodatkov.{0} datotek",
+ "ToastMetadataFilesRemovedNoneFound": "Ni metapodatkov.{0} datotek, najdenih v knjižnici",
+ "ToastMetadataFilesRemovedNoneRemoved": "Ni metapodatkov.{0} datotek odstranjenih",
+ "ToastMetadataFilesRemovedSuccess": "{0} metapodatki.{1} datotek odstranjenih",
+ "ToastMustHaveAtLeastOnePath": "Imeti mora vsaj eno pot",
"ToastNameEmailRequired": "Ime in e-pošta sta obvezna",
"ToastNameRequired": "Ime je obvezno",
+ "ToastNewEpisodesFound": "Število najdenih novih epizod: {0}",
"ToastNewUserCreatedFailed": "Računa ni bilo mogoče ustvariti: \"{0}\"",
"ToastNewUserCreatedSuccess": "Nov račun je bil ustvarjen",
"ToastNewUserLibraryError": "Izbrati morate vsaj eno knjižnico",
"ToastNewUserPasswordError": "Mora imeti geslo, samo korenski uporabnik ima lahko prazno geslo",
"ToastNewUserTagError": "Izbrati morate vsaj eno oznako",
"ToastNewUserUsernameError": "Vnesite uporabniško ime",
+ "ToastNoNewEpisodesFound": "Ni novih epizod",
"ToastNoUpdatesNecessary": "Posodobitve niso potrebne",
"ToastNotificationCreateFailed": "Obvestila ni bilo mogoče ustvariti",
"ToastNotificationDeleteFailed": "Brisanje obvestila ni uspelo",
"ToastNotificationFailedMaximum": "Največje število neuspelih poskusov mora biti >= 0",
"ToastNotificationQueueMaximum": "Največja čakalna vrsta obvestil mora biti >= 0",
- "ToastNotificationSettingsUpdateFailed": "Nastavitev obvestil ni bilo mogoče posodobiti",
"ToastNotificationSettingsUpdateSuccess": "Nastavitve obvestil so bile posodobljene",
"ToastNotificationTestTriggerFailed": "Sprožitev testnega obvestila ni uspela",
"ToastNotificationTestTriggerSuccess": "Sproženo testno obvestilo",
- "ToastNotificationUpdateFailed": "Obvestila ni bilo mogoče posodobiti",
"ToastNotificationUpdateSuccess": "Obvestilo posodobljeno",
"ToastPlaylistCreateFailed": "Seznama predvajanja ni bilo mogoče ustvariti",
"ToastPlaylistCreateSuccess": "Seznam predvajanja je bil ustvarjen",
"ToastPlaylistRemoveSuccess": "Seznam predvajanja odstranjen",
- "ToastPlaylistUpdateFailed": "Seznama predvajanja ni bilo mogoče posodobiti",
"ToastPlaylistUpdateSuccess": "Seznam predvajanja je bil posodobljen",
"ToastPodcastCreateFailed": "Podcasta ni bilo mogoče ustvariti",
"ToastPodcastCreateSuccess": "Podcast je bil uspešno ustvarjen",
"ToastPodcastGetFeedFailed": "Vira podcasta ni bilo mogoče pridobiti",
"ToastPodcastNoEpisodesInFeed": "V viru RSS ni bilo mogoče najti nobene epizode",
"ToastPodcastNoRssFeed": "Podcast nima vira RSS",
+ "ToastProgressIsNotBeingSynced": "Napredek se ne sinhronizira, znova zaženite predvajanje",
"ToastProviderCreatedFailed": "Ponudnika ni bilo mogoče dodati",
"ToastProviderCreatedSuccess": "Dodan je bil nov ponudnik",
"ToastProviderNameAndUrlRequired": "Obvezen podatek sta ime in URL",
@@ -950,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "E-knjiga je bila poslana v napravo \"{0}\"",
"ToastSeriesUpdateFailed": "Posodobitev serije ni uspela",
"ToastSeriesUpdateSuccess": "Uspešna posodobitev serije",
- "ToastServerSettingsUpdateFailed": "Nastavitev strežnika ni bilo mogoče posodobiti",
"ToastServerSettingsUpdateSuccess": "Nastavitve strežnika so bile posodobljene",
"ToastSessionCloseFailed": "Seje ni bilo mogoče zapreti",
"ToastSessionDeleteFailed": "Brisanje seje ni uspelo",
"ToastSessionDeleteSuccess": "Seja je bila izbrisana",
+ "ToastSleepTimerDone": "Časovnik za spanje se je končal... zZzzZz",
"ToastSlugMustChange": "Slug vsebuje neveljavne znake",
"ToastSlugRequired": "Slug je obvezen podatek",
"ToastSocketConnected": "Omrežna povezava je priklopljena",
"ToastSocketDisconnected": "Omrežna povezava je odklopljena",
"ToastSocketFailedToConnect": "Omrežna povezava ni uspela vzpostaviti priklopa",
"ToastSortingPrefixesEmptyError": "Imeti mora vsaj 1 predpono za razvrščanje",
- "ToastSortingPrefixesUpdateFailed": "Posodobitev predpon za razvrščanje ni uspela",
"ToastSortingPrefixesUpdateSuccess": "Predpone za razvrščanje so bile posodobljene ({0} elementov)",
"ToastTitleRequired": "Naslov je obvezen",
"ToastUnknownError": "Neznana napaka",
diff --git a/client/strings/sv.json b/client/strings/sv.json
index 0db5cbd3a2..0d156efd70 100644
--- a/client/strings/sv.json
+++ b/client/strings/sv.json
@@ -45,6 +45,7 @@
"ButtonOk": "Okej",
"ButtonOpenFeed": "Öppna flöde",
"ButtonOpenManager": "Öppna Manager",
+ "ButtonPause": "Pausa",
"ButtonPlay": "Spela",
"ButtonPlaying": "Spelar",
"ButtonPlaylists": "Spellistor",
@@ -263,8 +264,10 @@
"LabelFinished": "Avslutad",
"LabelFolder": "Mapp",
"LabelFolders": "Mappar",
+ "LabelFontBoldness": "Fetstil",
"LabelFontFamily": "Teckensnittsfamilj",
"LabelFontScale": "Teckensnittsskala",
+ "LabelGenre": "Genre",
"LabelGenres": "Genrer",
"LabelHardDeleteFile": "Hård radering av fil",
"LabelHasEbook": "Har E-bok",
@@ -294,6 +297,7 @@
"LabelLastSeen": "Senast sedd",
"LabelLastTime": "Senaste gången",
"LabelLastUpdate": "Senaste uppdatering",
+ "LabelLayout": "Layout",
"LabelLayoutSinglePage": "En sida",
"LabelLayoutSplitPage": "Dela sida",
"LabelLess": "Mindre",
@@ -322,8 +326,8 @@
"LabelNarrators": "Berättare",
"LabelNew": "Ny",
"LabelNewPassword": "Nytt lösenord",
- "LabelNewestAuthors": "Nyaste författare",
- "LabelNewestEpisodes": "Nyaste avsnitt",
+ "LabelNewestAuthors": "Senast tillagda författare",
+ "LabelNewestEpisodes": "Senast tillagda avsnitt",
"LabelNextBackupDate": "Nästa säkerhetskopia datum",
"LabelNextScheduledRun": "Nästa schemalagda körning",
"LabelNoEpisodesSelected": "Inga avsnitt valda",
@@ -355,8 +359,10 @@
"LabelPhotoPathURL": "Bildsökväg/URL",
"LabelPlayMethod": "Spelläge",
"LabelPlaylists": "Spellistor",
+ "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcast-sökområde",
"LabelPodcastType": "Podcasttyp",
+ "LabelPodcasts": "Podcasts",
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
"LabelPreventIndexing": "Förhindra att ditt flöde indexeras av iTunes och Google-podcastsökmotorer",
"LabelPrimaryEbook": "Primär e-bok",
@@ -371,6 +377,7 @@
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
"LabelRSSFeedSlug": "RSS-flödesslag",
"LabelRSSFeedURL": "RSS-flöde URL",
+ "LabelRandomly": "Slumpartat",
"LabelRead": "Läst",
"LabelReadAgain": "Läs igen",
"LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg",
@@ -430,6 +437,7 @@
"LabelShowAll": "Visa alla",
"LabelSize": "Storlek",
"LabelSleepTimer": "Sleeptimer",
+ "LabelStart": "Starta",
"LabelStartTime": "Starttid",
"LabelStarted": "Startad",
"LabelStartedAt": "Startad vid",
@@ -637,10 +645,8 @@
"PlaceholderNewPlaylist": "Nytt spellistanamn",
"PlaceholderSearch": "Sök...",
"PlaceholderSearchEpisode": "Sök avsnitt...",
- "ToastAccountUpdateFailed": "Det gick inte att uppdatera kontot",
"ToastAccountUpdateSuccess": "Kontot uppdaterat",
"ToastAuthorImageRemoveSuccess": "Författarens bild borttagen",
- "ToastAuthorUpdateFailed": "Det gick inte att uppdatera författaren",
"ToastAuthorUpdateMerged": "Författaren sammanslagen",
"ToastAuthorUpdateSuccess": "Författaren uppdaterad",
"ToastAuthorUpdateSuccessNoImageFound": "Författaren uppdaterad (ingen bild hittad)",
@@ -656,17 +662,13 @@
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",
"ToastBookmarkCreateSuccess": "Bokmärket tillagt",
"ToastBookmarkRemoveSuccess": "Bokmärket borttaget",
- "ToastBookmarkUpdateFailed": "Det gick inte att uppdatera bokmärket",
"ToastBookmarkUpdateSuccess": "Bokmärket uppdaterat",
"ToastChaptersHaveErrors": "Kapitlen har fel",
"ToastChaptersMustHaveTitles": "Kapitel måste ha titlar",
"ToastCollectionItemsRemoveSuccess": "Objekt borttagna från samlingen",
"ToastCollectionRemoveSuccess": "Samlingen borttagen",
- "ToastCollectionUpdateFailed": "Det gick inte att uppdatera samlingen",
"ToastCollectionUpdateSuccess": "Samlingen uppdaterad",
- "ToastItemCoverUpdateFailed": "Det gick inte att uppdatera objektets omslag",
"ToastItemCoverUpdateSuccess": "Objektets omslag uppdaterat",
- "ToastItemDetailsUpdateFailed": "Det gick inte att uppdatera objektdetaljerna",
"ToastItemDetailsUpdateSuccess": "Objektdetaljer uppdaterade",
"ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera som färdig",
"ToastItemMarkedAsFinishedSuccess": "Objekt markerat som färdig",
@@ -678,12 +680,10 @@
"ToastLibraryDeleteSuccess": "Biblioteket borttaget",
"ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen",
"ToastLibraryScanStarted": "Skanning av biblioteket påbörjad",
- "ToastLibraryUpdateFailed": "Det gick inte att uppdatera biblioteket",
"ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" uppdaterat",
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
"ToastPlaylistCreateSuccess": "Spellistan skapad",
"ToastPlaylistRemoveSuccess": "Spellistan borttagen",
- "ToastPlaylistUpdateFailed": "Det gick inte att uppdatera spellistan",
"ToastPlaylistUpdateSuccess": "Spellistan uppdaterad",
"ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten",
"ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt",
diff --git a/client/strings/uk.json b/client/strings/uk.json
index d1a161c853..81cd13f4b7 100644
--- a/client/strings/uk.json
+++ b/client/strings/uk.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Обрати файли",
"ButtonClearFilter": "Очистити фільтр",
"ButtonCloseFeed": "Закрити стрічку",
+ "ButtonCloseSession": "Закрити відкритий сеанс",
"ButtonCollections": "Добірки",
"ButtonConfigureScanner": "Налаштувати сканер",
"ButtonCreate": "Створити",
@@ -28,6 +29,9 @@
"ButtonEdit": "Редагувати",
"ButtonEditChapters": "Редагувати глави",
"ButtonEditPodcast": "Редагувати подкаст",
+ "ButtonEnable": "Увімкнути",
+ "ButtonFireAndFail": "Вогонь і невдача",
+ "ButtonFireOnTest": "Випробування на вогнестійкість",
"ButtonForceReScan": "Примусово сканувати",
"ButtonFullPath": "Повний шлях",
"ButtonHide": "Приховати",
@@ -46,19 +50,23 @@
"ButtonNevermind": "Скасувати",
"ButtonNext": "Наступний",
"ButtonNextChapter": "Наступна глава",
+ "ButtonNextItemInQueue": "Наступний елемент у черзі",
"ButtonOk": "Гаразд",
"ButtonOpenFeed": "Відкрити стрічку",
"ButtonOpenManager": "Відкрити менеджер",
"ButtonPause": "Пауза",
"ButtonPlay": "Слухати",
+ "ButtonPlayAll": "Відтворити все",
"ButtonPlaying": "Відтворюється",
"ButtonPlaylists": "Списки відтворення",
"ButtonPrevious": "Попередній",
"ButtonPreviousChapter": "Попередня глава",
+ "ButtonProbeAudioFile": "Перевірити аудіофайл",
"ButtonPurgeAllCache": "Очистити весь кеш",
"ButtonPurgeItemsCache": "Очистити кеш елементів",
"ButtonQueueAddItem": "Додати до черги",
"ButtonQueueRemoveItem": "Вилучити з черги",
+ "ButtonQuickEmbed": "Швидке вбудовування",
"ButtonQuickEmbedMetadata": "Швидко вбудувати метадані",
"ButtonQuickMatch": "Швидкий пошук",
"ButtonReScan": "Пересканувати",
@@ -92,6 +100,7 @@
"ButtonStats": "Статистика",
"ButtonSubmit": "Надіслати",
"ButtonTest": "Перевірити",
+ "ButtonUnlinkOpenId": "Вимкнути OpenID",
"ButtonUpload": "Завантажити",
"ButtonUploadBackup": "Завантажити резервну копію",
"ButtonUploadCover": "Завантажити обкладинку",
@@ -104,6 +113,7 @@
"ErrorUploadFetchMetadataNoResults": "Не вдалося отримати метадані — спробуйте оновити заголовок та/або автора",
"ErrorUploadLacksTitle": "Назва обов'язкова",
"HeaderAccount": "Профіль",
+ "HeaderAddCustomMetadataProvider": "Додати користувацький постачальник метаданих",
"HeaderAdvanced": "Розширені",
"HeaderAppriseNotificationSettings": "Налаштування сповіщень Apprise",
"HeaderAudioTracks": "Аудіодоріжки",
@@ -149,8 +159,11 @@
"HeaderMetadataToEmbed": "Вбудувати метадані",
"HeaderNewAccount": "Новий профіль",
"HeaderNewLibrary": "Нова бібліотека",
+ "HeaderNotificationCreate": "Створити сповіщення",
+ "HeaderNotificationUpdate": "Оновити сповіщення",
"HeaderNotifications": "Сповіщення",
"HeaderOpenIDConnectAuthentication": "Автентифікація OpenID Connect",
+ "HeaderOpenListeningSessions": "Відкриті сеанси прослуховування",
"HeaderOpenRSSFeed": "Відкрити RSS-канал",
"HeaderOtherFiles": "Інші файли",
"HeaderPasswordAuthentication": "Автентифікація за паролем",
@@ -168,6 +181,7 @@
"HeaderRemoveEpisodes": "Видалити епізодів: {0}",
"HeaderSavedMediaProgress": "Збережений прогрес медіа",
"HeaderSchedule": "Розклад",
+ "HeaderScheduleEpisodeDownloads": "Запланувати автоматичне завантаження епізодів",
"HeaderScheduleLibraryScans": "Розклад автосканування бібліотеки",
"HeaderSession": "Сеанс",
"HeaderSetBackupSchedule": "Встановити розклад резервного копіювання",
@@ -206,13 +220,18 @@
"LabelAddToPlaylist": "Додати до списку відтворення",
"LabelAddToPlaylistBatch": "Додано елементів у список відтворення: {0}",
"LabelAddedAt": "Дата додавання",
+ "LabelAddedDate": "Додано {0}",
"LabelAdminUsersOnly": "Тільки для адміністраторів",
"LabelAll": "Усе",
"LabelAllUsers": "Усі користувачі",
"LabelAllUsersExcludingGuests": "Усі, крім гостей",
"LabelAllUsersIncludingGuests": "Усі, включно з гостями",
"LabelAlreadyInYourLibrary": "Вже у вашій бібліотеці",
+ "LabelApiToken": "Токен API",
"LabelAppend": "Додати",
+ "LabelAudioBitrate": "Бітрейт аудіо (напр. 128k)",
+ "LabelAudioChannels": "Канали аудіо (1 або 2)",
+ "LabelAudioCodec": "Аудіокодек",
"LabelAuthor": "Автор",
"LabelAuthorFirstLast": "Автор (за ім'ям)",
"LabelAuthorLastFirst": "Автор (за прізвищем)",
@@ -225,6 +244,7 @@
"LabelAutoRegister": "Автореєстрація",
"LabelAutoRegisterDescription": "Автоматично створювати нових користувачів після входу",
"LabelBackToUser": "Повернутися до користувача",
+ "LabelBackupAudioFiles": "Резервне копіювання аудіофайлів",
"LabelBackupLocation": "Розташування резервних копій",
"LabelBackupsEnableAutomaticBackups": "Автоматичне резервне копіювання",
"LabelBackupsEnableAutomaticBackupsHelp": "Резервні копії збережено у /metadata/backups",
@@ -233,18 +253,22 @@
"LabelBackupsNumberToKeep": "Кількість резервних копій",
"LabelBackupsNumberToKeepHelp": "Лиш 1 резервну копію буде видалено за раз, тож якщо їх багато, то вам варто видалити їх вручну.",
"LabelBitrate": "Бітрейт",
+ "LabelBonus": "Бонус",
"LabelBooks": "Книги",
"LabelButtonText": "Текст кнопки",
"LabelByAuthor": "від {0}",
"LabelChangePassword": "Змінити пароль",
"LabelChannels": "Канали",
+ "LabelChapterCount": "{0} Глав",
"LabelChapterTitle": "Назва глави",
"LabelChapters": "Глави",
"LabelChaptersFound": "глав знайдено",
"LabelClickForMoreInfo": "Натисніть, щоб дізнатися більше",
+ "LabelClickToUseCurrentValue": "Натисніть, щоб використати поточне значення",
"LabelClosePlayer": "Закрити програвач",
"LabelCodec": "Кодек",
"LabelCollapseSeries": "Згорнути серії",
+ "LabelCollapseSubSeries": "Згорнути підсерії",
"LabelCollection": "Добірка",
"LabelCollections": "Добірки",
"LabelComplete": "Завершити",
@@ -290,13 +314,28 @@
"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": "URL епізоду з RSS-каналу",
+ "LabelEpisodes": "Епізодов",
+ "LabelEpisodic": "Епізодичний",
"LabelExample": "Приклад",
"LabelExpandSeries": "Розгорнути серії",
+ "LabelExpandSubSeries": "Розгорнути підсерії",
"LabelExplicit": "Відверта",
"LabelExplicitChecked": "Відверта (з прапорцем)",
"LabelExplicitUnchecked": "Не відверта (без прапорця)",
@@ -305,7 +344,9 @@
"LabelFetchingMetadata": "Отримання метаданих",
"LabelFile": "Файл",
"LabelFileBirthtime": "Дата створення",
+ "LabelFileBornDate": "Народився {0}",
"LabelFileModified": "Дата змінення",
+ "LabelFileModifiedDate": "Змінено {0}",
"LabelFilename": "Ім'я файлу",
"LabelFilterByUser": "Фільтрувати за користувачем",
"LabelFindEpisodes": "Знайти епізоди",
@@ -319,6 +360,7 @@
"LabelFontScale": "Розмір шрифту",
"LabelFontStrikethrough": "Закреслений",
"LabelFormat": "Формат",
+ "LabelFull": "Повний",
"LabelGenre": "Жанр",
"LabelGenres": "Жанри",
"LabelHardDeleteFile": "Остаточно видалити файл",
@@ -361,6 +403,7 @@
"LabelLess": "Менше",
"LabelLibrariesAccessibleToUser": "Бібліотеки, доступні користувачу",
"LabelLibrary": "Бібліотека",
+ "LabelLibraryFilterSublistEmpty": "Ні {0}",
"LabelLibraryItem": "Елемент бібліотеки",
"LabelLibraryName": "Назва бібліотеки",
"LabelLimit": "Обмеження",
@@ -373,6 +416,10 @@
"LabelLowestPriority": "Найнижчий пріоритет",
"LabelMatchExistingUsersBy": "Шукати наявних користувачів за",
"LabelMatchExistingUsersByDescription": "Використовується для підключення наявних користувачів. Після підключення користувач отримає унікальний id від вашого сервісу SSO",
+ "LabelMaxEpisodesToDownload": "Максимальна кількість епізодів для завантаження. Використовуйте 0 для необмеженої кількості.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Максимальна кількість нових епізодів для завантаження за перевірку",
+ "LabelMaxEpisodesToKeep": "Максимальна кількість епізодів для зберігання",
+ "LabelMaxEpisodesToKeepHelp": "Значення 0 не встановлює обмеження. Після автоматичного завантаження нового епізоду, буде видалено найстаріший епізод, якщо у вас більше ніж X епізодів. Видаляється лише 1 епізод за одне нове завантаження.",
"LabelMediaPlayer": "Програвач медіа",
"LabelMediaType": "Тип медіа",
"LabelMetaTag": "Метатег",
@@ -418,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "Ім'я OpenID claim, що містить список груп користувачів. Зазвичай їх називають групами
. Якщо налаштовано , застосунок автоматично призначатиме ролі на основі членства користувача в групах, за умови, що ці групи названі в claim'і без урахування реєстру 'admin', 'user' або 'guest'. Claim мусить містити список, і якщо користувач належить до кількох груп, програма призначить йому роль, що відповідає найвищому рівню доступу. Якщо жодна група не збігається, у доступі буде відмовлено.",
"LabelOpenRSSFeed": "Відкрити RSS-канал",
"LabelOverwrite": "Перезаписати",
+ "LabelPaginationPageXOfY": "Сторінка {0} з {1}",
"LabelPassword": "Пароль",
"LabelPath": "Шлях",
"LabelPermanent": "Постійний",
"LabelPermissionsAccessAllLibraries": "Доступ до усіх бібліотек",
"LabelPermissionsAccessAllTags": "Доступ до усіх міток",
"LabelPermissionsAccessExplicitContent": "Доступ до відвертого вмісту",
+ "LabelPermissionsCreateEreader": "Можна створити читалку",
"LabelPermissionsDelete": "Може видаляти",
"LabelPermissionsDownload": "Може завантажувати",
"LabelPermissionsUpdate": "Може оновлювати",
@@ -431,6 +480,7 @@
"LabelPersonalYearReview": "Ваші підсумки року ({0})",
"LabelPhotoPathURL": "Шлях/URL фото",
"LabelPlayMethod": "Метод відтворення",
+ "LabelPlayerChapterNumberMarker": "{0} з {1}",
"LabelPlaylists": "Списки відтворення",
"LabelPodcast": "Подкаст",
"LabelPodcastSearchRegion": "Регіон пошуку подкасту",
@@ -442,8 +492,12 @@
"LabelPrimaryEbook": "Основна електронна книга",
"LabelProgress": "Прогрес",
"LabelProvider": "Джерело",
+ "LabelProviderAuthorizationValue": "Значення заголовка авторизації",
"LabelPubDate": "Дата публікації",
"LabelPublishYear": "Рік публікації",
+ "LabelPublishedDate": "Опубліковано {0}",
+ "LabelPublishedDecade": "Десятиліття публікації",
+ "LabelPublishedDecades": "Опубліковані десятиліття",
"LabelPublisher": "Видавець",
"LabelPublishers": "Видавці",
"LabelRSSFeedCustomOwnerEmail": "Користувацька електронна адреса власника",
@@ -463,21 +517,28 @@
"LabelRedo": "Повторити",
"LabelRegion": "Регіон",
"LabelReleaseDate": "Дата публікації",
+ "LabelRemoveAllMetadataAbs": "Видалити всі файли metadata.abs",
+ "LabelRemoveAllMetadataJson": "Видалити всі файли metadata.json",
"LabelRemoveCover": "Видалити обкладинку",
+ "LabelRemoveMetadataFile": "Видалити файли метаданих у папках елементів бібліотеки",
+ "LabelRemoveMetadataFileHelp": "Видалити всі файли metadata.json та metadata.abs у ваших папках {0}.",
"LabelRowsPerPage": "Рядків на сторінку",
"LabelSearchTerm": "Пошуковий запит",
"LabelSearchTitle": "Пошук за назвою",
"LabelSearchTitleOrASIN": "Пошук назви або ASIN",
"LabelSeason": "Сезон",
+ "LabelSeasonNumber": "Сезон #{0}",
"LabelSelectAll": "Вибрати все",
"LabelSelectAllEpisodes": "Вибрати всі серії",
"LabelSelectEpisodesShowing": "Обрати показані епізоди: {0}",
"LabelSelectUsers": "Вибрати користувачів",
"LabelSendEbookToDevice": "Надіслати електронну книгу на...",
"LabelSequence": "Послідовність",
+ "LabelSerial": "Серійний",
"LabelSeries": "Серії",
"LabelSeriesName": "Назва серії",
"LabelSeriesProgress": "Прогрес серії",
+ "LabelServerLogLevel": "Рівень журналу сервера",
"LabelServerYearReview": "Підсумки року сервера ({0})",
"LabelSetEbookAsPrimary": "Зробити основною",
"LabelSetEbookAsSupplementary": "Зробити додатковою",
@@ -502,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Серії, що містять одну книгу, будуть приховані зі сторінки серій та полиць головної сторінки.",
"LabelSettingsHomePageBookshelfView": "Полиці на головній сторінці",
"LabelSettingsLibraryBookshelfView": "Показувати полиці у бібліотеці",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Відсоток виконання більше ніж",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, що залишився, менше ніж (секунди)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Позначити медіа-елемент як завершений, коли",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропускати попередні книги у Продовжити серії",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Полиця Продовжити серії на головній сторінці показує найпершу непочату книгу з тих серій, у яких ви завершили хоча б одну книгу та не маєте книг у процесі. Якщо увімкнути це налаштування, то серії продовжуватимуться з останньої завершеної книги, а не з першої непочатої.",
"LabelSettingsParseSubtitles": "Дістати підзаголовки",
@@ -566,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} хвилини",
"LabelTimeDurationXSeconds": "{0} секунди",
"LabelTimeInMinutes": "Час у хвилинах",
+ "LabelTimeLeft": "{0} залишилось",
"LabelTimeListened": "Часу прослухано",
"LabelTimeListenedToday": "Сьогодні прослухано",
"LabelTimeRemaining": "Лишилося: {0}",
@@ -573,6 +638,7 @@
"LabelTitle": "Назва",
"LabelToolsEmbedMetadata": "Вбудувати метадані",
"LabelToolsEmbedMetadataDescription": "Вбудувати метадані в аудіофайли, включно з обкладинками та главами.",
+ "LabelToolsM4bEncoder": "Кодувальник M4B",
"LabelToolsMakeM4b": "Створити M4B-файл аудіокниги",
"LabelToolsMakeM4bDescription": "Створити .M4B-аудіокнигу з вбудованими метаданими, обкладинкою та главами.",
"LabelToolsSplitM4b": "Розділити M4B на MP3",
@@ -585,10 +651,12 @@
"LabelTracksMultiTrack": "Декілька доріжок",
"LabelTracksNone": "Доріжки відсутні",
"LabelTracksSingleTrack": "Одна доріжка",
+ "LabelTrailer": "Трейлер",
"LabelType": "Тип",
"LabelUnabridged": "Повна",
"LabelUndo": "Скасувати",
"LabelUnknown": "Невідомо",
+ "LabelUnknownPublishDate": "Невідома дата публікації",
"LabelUpdateCover": "Оновити обкладинку",
"LabelUpdateCoverHelp": "Дозволити перезапис наявних обкладинок обраних книг після віднайдення",
"LabelUpdateDetails": "Оновити подробиці",
@@ -597,8 +665,10 @@
"LabelUploaderDragAndDrop": "Перетягніть файли або теки",
"LabelUploaderDropFiles": "Перетягніть файли",
"LabelUploaderItemFetchMetadataHelp": "Автоматично шукати назву, автора та серію",
+ "LabelUseAdvancedOptions": "Використовувати розширені налаштування",
"LabelUseChapterTrack": "Прогрес глави",
"LabelUseFullTrack": "Використовувати доріжку повністю",
+ "LabelUseZeroForUnlimited": "Використовуйте 0 для необмеженої кількості",
"LabelUser": "Користувач",
"LabelUsername": "Ім’я користувача",
"LabelValue": "Значення",
@@ -637,19 +707,27 @@
"MessageCheckingCron": "Перевірка планувальника...",
"MessageConfirmCloseFeed": "Ви дійсно бажаєте закрити цей канал?",
"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}\"?",
@@ -657,6 +735,7 @@
"MessageConfirmRemoveEpisode": "Ви дійсно бажаєте видалити епізод \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Ви дійсно бажаєте видалити епізодів: {0}?",
"MessageConfirmRemoveListeningSessions": "Ви дійсно бажаєте видалити сеанси прослуховування: {0}?",
+ "MessageConfirmRemoveMetadataFiles": "Ви впевнені, що хочете видалити всі файли metadata.{0} у папках елементів вашої бібліотеки?",
"MessageConfirmRemoveNarrator": "Ви дійсно бажаєте видалити читця \"{0}\"?",
"MessageConfirmRemovePlaylist": "Ви дійсно бажаєте видалити список відтворення \"{0}\"?",
"MessageConfirmRenameGenre": "Ви дійсно бажаєте замінити жанр \"{0}\" на \"{1}\" для усіх елементів?",
@@ -665,11 +744,14 @@
"MessageConfirmRenameTag": "Ви дійсно бажаєте замінити мітку \"{0}\" на \"{1}\" для усіх елементів?",
"MessageConfirmRenameTagMergeNote": "Примітка: така мітка вже існує, тож їх буде об'єднано.",
"MessageConfirmRenameTagWarning": "Увага! Вже існує схожа мітка у іншому регістрі \"{0}\".",
+ "MessageConfirmResetProgress": "Ви впевнені, що хочете скинути свій прогрес?",
"MessageConfirmSendEbookToDevice": "Ви дійсно хочете відправити на пристрій \"{2}\" електроні книги: {0}, \"{1}\"?",
+ "MessageConfirmUnlinkOpenId": "Ви впевнені, що хочете відв'язати цього користувача від OpenID?",
"MessageDownloadingEpisode": "Завантаження епізоду",
"MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку",
"MessageEmbedFailed": "Не вдалося вбудувати!",
"MessageEmbedFinished": "Вбудовано!",
+ "MessageEmbedQueue": "В черзі на вбудовування метаданих ({0} в черзі)",
"MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}",
"MessageEreaderDevices": "Аби гарантувати отримання електронних книг, вам може знадобитися додати вказану вище адресу електронної пошти як правильного відправника на кожному з пристроїв зі списку нижче.",
"MessageFeedURLWillBe": "URL-адреса каналу буде {0}",
@@ -700,6 +782,7 @@
"MessageNoCollections": "Добірки відсутні",
"MessageNoCoversFound": "Обкладинок не знайдено",
"MessageNoDescription": "Без опису",
+ "MessageNoDevices": "Немає пристроїв",
"MessageNoDownloadsInProgress": "Немає активних завантажень",
"MessageNoDownloadsQueued": "Немає завантажень у черзі",
"MessageNoEpisodeMatchesFound": "Відповідних епізодів не знайдено",
@@ -713,6 +796,7 @@
"MessageNoLogs": "Журнал порожній",
"MessageNoMediaProgress": "Прогрес відсутній",
"MessageNoNotifications": "Сповіщення відсутні",
+ "MessageNoPodcastFeed": "Невірний подкаст: Немає каналу",
"MessageNoPodcastsFound": "Подкастів не знайдено",
"MessageNoResults": "Немає результатів",
"MessageNoSearchResultsFor": "Немає результатів пошуку для \"{0}\"",
@@ -727,7 +811,12 @@
"MessagePauseChapter": "Призупинити відтворення глави",
"MessagePlayChapter": "Слухати початок глави",
"MessagePlaylistCreateFromCollection": "Створити список відтворення з добірки",
+ "MessagePleaseWait": "Будь ласка, зачекайте...",
"MessagePodcastHasNoRSSFeedForMatching": "Подкаст не має RSS-каналу для пошуку",
+ "MessagePodcastSearchField": "Введіть пошуковий запит або URL RSS-стрічки",
+ "MessageQuickEmbedInProgress": "Швидке вбудовування в процесі",
+ "MessageQuickEmbedQueue": "В черзі на швидке вбудовування ({0} в черзі)",
+ "MessageQuickMatchAllEpisodes": "Швидке співставлення всіх епізодів",
"MessageQuickMatchDescription": "Заповнити відсутні подробиці та обкладинку першим результатом пошуку '{0}'. Не перезаписує подробиці, якщо не увімкнено параметр \"Надавати перевагу віднайденим метаданим\".",
"MessageRemoveChapter": "Видалити главу",
"MessageRemoveEpisodes": "Видалити епізодів: {0}",
@@ -745,6 +834,41 @@
"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": "Успішно завантажено!",
@@ -762,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Теки з медіафайлами буде оброблено як окремі елементи бібліотеки.",
"NoteUploaderOnlyAudioFiles": "Якщо завантажувати лише аудіофайли, то кожен файл буде оброблено як окрему книгу.",
"NoteUploaderUnsupportedFiles": "Непідтримувані файли пропущено. Під час вибору або перетягування теки, файли, що знаходяться поза текою, пропускаються.",
+ "NotificationOnBackupCompletedDescription": "Запускається після завершення резервного копіювання",
+ "NotificationOnBackupFailedDescription": "Срабатывает при збої резервного копіювання",
+ "NotificationOnEpisodeDownloadedDescription": "Запускається при автоматичному завантаженні епізоду подкасту",
+ "NotificationOnTestDescription": "Подія для тестування системи сповіщень",
"PlaceholderNewCollection": "Нова назва добірки",
"PlaceholderNewFolderPath": "Новий шлях до теки",
"PlaceholderNewPlaylist": "Нова назва списку",
@@ -785,80 +913,161 @@
"StatsTopNarrators": "УЛЮБЛЕНІ ЧИТЦІ",
"StatsTotalDuration": "Загальною довжиною…",
"StatsYearInReview": "ОГЛЯД РОКУ",
- "ToastAccountUpdateFailed": "Не вдалося оновити профіль",
"ToastAccountUpdateSuccess": "Профіль оновлено",
+ "ToastAppriseUrlRequired": "Необхідно ввести URL для Apprise",
+ "ToastAsinRequired": "ASIN є обов'язковим",
"ToastAuthorImageRemoveSuccess": "Фото автора видалено",
- "ToastAuthorUpdateFailed": "Не вдалося оновити автора",
+ "ToastAuthorNotFound": "Автор \"{0}\" не знайдений",
+ "ToastAuthorRemoveSuccess": "Автор видалений",
+ "ToastAuthorSearchNotFound": "Автор не знайдений",
"ToastAuthorUpdateMerged": "Автора об'єднано",
"ToastAuthorUpdateSuccess": "Автора оновлено",
"ToastAuthorUpdateSuccessNoImageFound": "Автора оновлено (фото не знайдено)",
+ "ToastBackupAppliedSuccess": "Резервна копія застосована",
"ToastBackupCreateFailed": "Не вдалося створити резервну копію",
"ToastBackupCreateSuccess": "Резервну копію створено",
"ToastBackupDeleteFailed": "Не вдалося видалити резервну копію",
"ToastBackupDeleteSuccess": "Резервну копію видалено",
+ "ToastBackupInvalidMaxKeep": "Профіль оновленоПрофіль оновлено",
+ "ToastBackupInvalidMaxSize": "Невірний максимальний розмір резервної копії",
"ToastBackupRestoreFailed": "Не вдалося відновити резервну копію",
"ToastBackupUploadFailed": "Не вдалося завантажити резервну копію",
"ToastBackupUploadSuccess": "Резервну копію завантажено",
+ "ToastBatchDeleteFailed": "Помилка при пакетному видаленні",
+ "ToastBatchDeleteSuccess": "Пакетне видалення успішне",
+ "ToastBatchQuickMatchFailed": "Не вдалося виконати пакетне швидке співпадіння!",
+ "ToastBatchQuickMatchStarted": "Пакетне швидке співпадіння {0} книг розпочато!",
"ToastBatchUpdateFailed": "Не вдалося оновити обрані",
"ToastBatchUpdateSuccess": "Обрані успішно оновлено",
"ToastBookmarkCreateFailed": "Не вдалося створити закладку",
"ToastBookmarkCreateSuccess": "Закладку додано",
"ToastBookmarkRemoveSuccess": "Закладку видалено",
- "ToastBookmarkUpdateFailed": "Не вдалося оновити закладку",
"ToastBookmarkUpdateSuccess": "Закладку оновлено",
"ToastCachePurgeFailed": "Не вдалося очистити кеш",
"ToastCachePurgeSuccess": "Кеш очищено",
"ToastChaptersHaveErrors": "Глави містять помилки",
"ToastChaptersMustHaveTitles": "Глави повинні мати назви",
+ "ToastChaptersRemoved": "Розділи видалені",
+ "ToastChaptersUpdated": "Розділи оновлені",
+ "ToastCollectionItemsAddFailed": "Не вдалося додати елемент(и) до колекції",
+ "ToastCollectionItemsAddSuccess": "Елемент(и) успішно додано до колекції",
"ToastCollectionItemsRemoveSuccess": "Елемент(и) видалено з добірки",
"ToastCollectionRemoveSuccess": "Добірку видалено",
- "ToastCollectionUpdateFailed": "Не вдалося оновити добірку",
"ToastCollectionUpdateSuccess": "Добірку оновлено",
+ "ToastCoverUpdateFailed": "Не вдалося оновити обкладинку",
"ToastDeleteFileFailed": "Не вдалося видалити файл",
"ToastDeleteFileSuccess": "Файл видалено",
+ "ToastDeviceAddFailed": "Не вдалося додати пристрій",
+ "ToastDeviceNameAlreadyExists": "Пристрій для електронних книг з таким ім'ям вже існує",
+ "ToastDeviceTestEmailFailed": "Не вдалося надіслати тестовий електронний лист",
+ "ToastDeviceTestEmailSuccess": "Тестовий електронний лист надіслано",
+ "ToastEmailSettingsUpdateSuccess": "Налаштування електронної пошти оновлено",
+ "ToastEncodeCancelFailed": "Не вдалося скасувати кодування",
+ "ToastEncodeCancelSucces": "Кодування скасовано",
+ "ToastEpisodeDownloadQueueClearFailed": "Не вдалося очистити чергу",
+ "ToastEpisodeDownloadQueueClearSuccess": "Чергу на завантаження епізодів очищено",
+ "ToastEpisodeUpdateSuccess": "{0} епізодів оновлено",
"ToastErrorCannotShare": "Не можна типово поширити на цей пристрій",
"ToastFailedToLoadData": "Не вдалося завантажити дані",
- "ToastItemCoverUpdateFailed": "Не вдалося оновити обкладинку",
+ "ToastFailedToMatch": "Не вдалося знайти відповідність",
+ "ToastFailedToShare": "Не вдалося поділитися",
+ "ToastFailedToUpdate": "Не вдалося оновити",
+ "ToastInvalidImageUrl": "Невірний URL зображення",
+ "ToastInvalidMaxEpisodesToDownload": "Невірна кількість епізодів для завантаження",
+ "ToastInvalidUrl": "Невірний URL",
"ToastItemCoverUpdateSuccess": "Обкладинку елемента оновлено",
- "ToastItemDetailsUpdateFailed": "Не вдалося оновити подробиці елемента",
+ "ToastItemDeletedFailed": "Не вдалося видалити елемент",
+ "ToastItemDeletedSuccess": "Видалений елемент",
"ToastItemDetailsUpdateSuccess": "Подробиці про елемент оновлено",
"ToastItemMarkedAsFinishedFailed": "Не вдалося позначити як завершене",
"ToastItemMarkedAsFinishedSuccess": "Елемент позначено як завершений",
"ToastItemMarkedAsNotFinishedFailed": "Не вдалося позначити незавершеним",
"ToastItemMarkedAsNotFinishedSuccess": "Елемент позначено незавершеним",
+ "ToastItemUpdateSuccess": "Елемент оновлено",
"ToastLibraryCreateFailed": "Не вдалося створити бібліотеку",
"ToastLibraryCreateSuccess": "Бібліотеку \"{0}\" створено",
"ToastLibraryDeleteFailed": "Не вдалося видалити бібліотеку",
"ToastLibraryDeleteSuccess": "Бібліотеку видалено",
"ToastLibraryScanFailedToStart": "Не вдалося розпочати сканування",
"ToastLibraryScanStarted": "Почалося сканування бібліотеки",
- "ToastLibraryUpdateFailed": "Не вдалося оновити бібліотеку",
"ToastLibraryUpdateSuccess": "Бібліотеку \"{0}\" оновлено",
+ "ToastMatchAllAuthorsFailed": "Не вдалось знайти відповідності з усіма авторами",
+ "ToastMetadataFilesRemovedError": "Помилка при видаленні metadata.{0} файли",
+ "ToastMetadataFilesRemovedNoneFound": "У бібліотеці не знайдено metadata.{0} файлів",
+ "ToastMetadataFilesRemovedNoneRemoved": "Не видалено metadata.{0} файлів",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} файлів видалено",
+ "ToastMustHaveAtLeastOnePath": "Повинен бути хоча б один шлях",
+ "ToastNameEmailRequired": "Ім'я та електронна пошта обов'язкові",
+ "ToastNameRequired": "Ім'я обов'язкове",
+ "ToastNewEpisodesFound": "{0} нових епізодів знайдено",
+ "ToastNewUserCreatedFailed": "Не вдалося створити акаунт: \"{0}\"",
+ "ToastNewUserCreatedSuccess": "Новий акаунт створено",
+ "ToastNewUserLibraryError": "Потрібно вибрати хоча б одну бібліотеку",
+ "ToastNewUserPasswordError": "Пароль обов'язковий, лише користувач з правами root може мати порожній пароль",
+ "ToastNewUserTagError": "Потрібно вибрати хоча б один тег",
+ "ToastNewUserUsernameError": "Введіть ім'я користувача",
+ "ToastNoNewEpisodesFound": "Нових епізодів не знайдено",
+ "ToastNoUpdatesNecessary": "Оновлення не потрібні",
+ "ToastNotificationCreateFailed": "Не вдалося створити сповіщення",
+ "ToastNotificationDeleteFailed": "Не вдалося видалити сповіщення",
+ "ToastNotificationFailedMaximum": "Максимальна кількість невдалих спроб повинна бути >= 0",
+ "ToastNotificationQueueMaximum": "Максимальна кількість сповіщень у черзі повинна бути >= 0",
+ "ToastNotificationSettingsUpdateSuccess": "Налаштування сповіщень оновлено",
+ "ToastNotificationTestTriggerFailed": "Не вдалося ініціювати тестове сповіщення",
+ "ToastNotificationTestTriggerSuccess": "Спрацьовувало сповіщення про тестування",
+ "ToastNotificationUpdateSuccess": "Сповіщення оновлено",
"ToastPlaylistCreateFailed": "Не вдалося створити список",
"ToastPlaylistCreateSuccess": "Список відтворення створено",
"ToastPlaylistRemoveSuccess": "Список відтворення видалено",
- "ToastPlaylistUpdateFailed": "Не вдалося оновити список",
"ToastPlaylistUpdateSuccess": "Список відтворення оновлено",
"ToastPodcastCreateFailed": "Не вдалося створити подкаст",
"ToastPodcastCreateSuccess": "Подкаст успішно створено",
+ "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}\"",
"ToastSeriesUpdateFailed": "Не вдалося оновити серію",
"ToastSeriesUpdateSuccess": "Серію успішно оновлено",
- "ToastServerSettingsUpdateFailed": "Не вдалося оновити налаштування сервера",
"ToastServerSettingsUpdateSuccess": "Налаштування сервера оновлено",
+ "ToastSessionCloseFailed": "Не вдалося закрити сесію",
"ToastSessionDeleteFailed": "Не вдалося видалити сесію",
"ToastSessionDeleteSuccess": "Сесію видалено",
+ "ToastSleepTimerDone": "Час сну завершено... зЗзЗз",
+ "ToastSlugMustChange": "Slug містить недопустимі символи",
+ "ToastSlugRequired": "Slug обов'язковий",
"ToastSocketConnected": "Сокет під'єднано",
"ToastSocketDisconnected": "Сокет від'єднано",
"ToastSocketFailedToConnect": "Не вдалося під'єднатися до сокета",
"ToastSortingPrefixesEmptyError": "Мусить мати хоча б 1 префікс сортування",
- "ToastSortingPrefixesUpdateFailed": "Не вдалося оновити префікси сортування",
"ToastSortingPrefixesUpdateSuccess": "Префікси сортування оновлено ({0})",
+ "ToastTitleRequired": "Заголовок обов'язковий",
+ "ToastUnknownError": "Невідома помилка",
+ "ToastUnlinkOpenIdFailed": "Не вдалося відв'язати користувача від OpenID",
+ "ToastUnlinkOpenIdSuccess": "Користувача відв'язано від OpenID",
"ToastUserDeleteFailed": "Не вдалося видалити користувача",
- "ToastUserDeleteSuccess": "Користувача видалено"
+ "ToastUserDeleteSuccess": "Користувача видалено",
+ "ToastUserPasswordChangeSuccess": "Пароль успішно змінено",
+ "ToastUserPasswordMismatch": "Паролі не збігаються",
+ "ToastUserPasswordMustChange": "Новий пароль не може співпадати з попереднім",
+ "ToastUserRootRequireName": "Потрібно ввести ім'я користувача root"
}
diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json
index 02fa75f58d..8b0ca165c8 100644
--- a/client/strings/vi-vn.json
+++ b/client/strings/vi-vn.json
@@ -663,10 +663,8 @@
"PlaceholderNewPlaylist": "Tên danh sách phát mới",
"PlaceholderSearch": "Tìm kiếm..",
"PlaceholderSearchEpisode": "Tìm kiếm tập..",
- "ToastAccountUpdateFailed": "Cập nhật tài khoản thất bại",
"ToastAccountUpdateSuccess": "Tài khoản đã được cập nhật",
"ToastAuthorImageRemoveSuccess": "Ảnh tác giả đã được xóa",
- "ToastAuthorUpdateFailed": "Cập nhật tác giả thất bại",
"ToastAuthorUpdateMerged": "Tác giả đã được hợp nhất",
"ToastAuthorUpdateSuccess": "Cập nhật tác giả thành công",
"ToastAuthorUpdateSuccessNoImageFound": "Cập nhật tác giả thành công (không tìm thấy ảnh)",
@@ -682,17 +680,13 @@
"ToastBookmarkCreateFailed": "Tạo đánh dấu thất bại",
"ToastBookmarkCreateSuccess": "Đã thêm đánh dấu",
"ToastBookmarkRemoveSuccess": "Đánh dấu đã được xóa",
- "ToastBookmarkUpdateFailed": "Cập nhật đánh dấu thất bại",
"ToastBookmarkUpdateSuccess": "Đánh dấu đã được cập nhật",
"ToastChaptersHaveErrors": "Các chương có lỗi",
"ToastChaptersMustHaveTitles": "Các chương phải có tiêu đề",
"ToastCollectionItemsRemoveSuccess": "Mục đã được xóa khỏi bộ sưu tập",
"ToastCollectionRemoveSuccess": "Bộ sưu tập đã được xóa",
- "ToastCollectionUpdateFailed": "Cập nhật bộ sưu tập thất bại",
"ToastCollectionUpdateSuccess": "Bộ sưu tập đã được cập nhật",
- "ToastItemCoverUpdateFailed": "Cập nhật ảnh bìa mục thất bại",
"ToastItemCoverUpdateSuccess": "Ảnh bìa mục đã được cập nhật",
- "ToastItemDetailsUpdateFailed": "Cập nhật chi tiết mục thất bại",
"ToastItemDetailsUpdateSuccess": "Chi tiết mục đã được cập nhật",
"ToastItemMarkedAsFinishedFailed": "Đánh dấu mục là Hoàn thành thất bại",
"ToastItemMarkedAsFinishedSuccess": "Mục đã được đánh dấu là Hoàn thành",
@@ -704,12 +698,10 @@
"ToastLibraryDeleteSuccess": "Thư viện đã được xóa",
"ToastLibraryScanFailedToStart": "Không thể bắt đầu quét thư viện",
"ToastLibraryScanStarted": "Quét thư viện đã được bắt đầu",
- "ToastLibraryUpdateFailed": "Cập nhật thư viện thất bại",
"ToastLibraryUpdateSuccess": "Thư viện \"{0}\" đã được cập nhật",
"ToastPlaylistCreateFailed": "Tạo danh sách phát thất bại",
"ToastPlaylistCreateSuccess": "Danh sách phát đã được tạo",
"ToastPlaylistRemoveSuccess": "Danh sách phát đã được xóa",
- "ToastPlaylistUpdateFailed": "Cập nhật danh sách phát thất bại",
"ToastPlaylistUpdateSuccess": "Danh sách phát đã được cập nhật",
"ToastPodcastCreateFailed": "Tạo podcast thất bại",
"ToastPodcastCreateSuccess": "Podcast đã được tạo thành công",
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index df0d77f7c7..072cbd39ef 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -1,5 +1,5 @@
{
- "ButtonAdd": "增加",
+ "ButtonAdd": "添加",
"ButtonAddChapters": "添加章节",
"ButtonAddDevice": "添加设备",
"ButtonAddLibrary": "添加库",
@@ -47,7 +47,7 @@
"ButtonMapChapterTitles": "章节标题结构",
"ButtonMatchAllAuthors": "匹配所有作者",
"ButtonMatchBooks": "匹配图书",
- "ButtonNevermind": "没有关系",
+ "ButtonNevermind": "取消",
"ButtonNext": "下一个",
"ButtonNextChapter": "下一章节",
"ButtonNextItemInQueue": "队列中的下一个项目",
@@ -56,6 +56,7 @@
"ButtonOpenManager": "打开管理器",
"ButtonPause": "暂停",
"ButtonPlay": "播放",
+ "ButtonPlayAll": "播放",
"ButtonPlaying": "正在播放",
"ButtonPlaylists": "播放列表",
"ButtonPrevious": "上一个",
@@ -65,11 +66,12 @@
"ButtonPurgeItemsCache": "清理项目缓存",
"ButtonQueueAddItem": "添加到队列",
"ButtonQueueRemoveItem": "从队列中移除",
+ "ButtonQuickEmbed": "快速嵌入",
"ButtonQuickEmbedMetadata": "快速嵌入元数据",
"ButtonQuickMatch": "快速匹配",
"ButtonReScan": "重新扫描",
"ButtonRead": "读取",
- "ButtonReadLess": "阅读更少",
+ "ButtonReadLess": "阅读较少",
"ButtonReadMore": "阅读更多",
"ButtonRefresh": "刷新",
"ButtonRemove": "移除",
@@ -134,7 +136,7 @@
"HeaderEmailSettings": "邮箱设置",
"HeaderEpisodes": "剧集",
"HeaderEreaderDevices": "Ereader 设备",
- "HeaderEreaderSettings": "Ereader 设置",
+ "HeaderEreaderSettings": "电子阅读器设置",
"HeaderFiles": "文件",
"HeaderFindChapters": "查找章节",
"HeaderIgnoredFiles": "忽略的文件",
@@ -161,6 +163,7 @@
"HeaderNotificationUpdate": "更新通知",
"HeaderNotifications": "通知",
"HeaderOpenIDConnectAuthentication": "OpenID 连接身份验证",
+ "HeaderOpenListeningSessions": "打开收听会话",
"HeaderOpenRSSFeed": "打开 RSS 源",
"HeaderOtherFiles": "其他文件",
"HeaderPasswordAuthentication": "密码认证",
@@ -178,6 +181,7 @@
"HeaderRemoveEpisodes": "移除 {0} 剧集",
"HeaderSavedMediaProgress": "保存媒体进度",
"HeaderSchedule": "计划任务",
+ "HeaderScheduleEpisodeDownloads": "设置自动下载剧集",
"HeaderScheduleLibraryScans": "自动扫描媒体库",
"HeaderSession": "会话",
"HeaderSetBackupSchedule": "设置备份计划任务",
@@ -216,14 +220,18 @@
"LabelAddToPlaylist": "添加到播放列表",
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
"LabelAddedAt": "添加于",
- "LabelAddedDate": "添加 {0}",
+ "LabelAddedDate": "已添加 {0}",
"LabelAdminUsersOnly": "仅限管理员用户",
"LabelAll": "全部",
"LabelAllUsers": "所有用户",
"LabelAllUsersExcludingGuests": "除访客外的所有用户",
"LabelAllUsersIncludingGuests": "包括访客的所有用户",
"LabelAlreadyInYourLibrary": "已存在你的库中",
+ "LabelApiToken": "API 令牌",
"LabelAppend": "附加",
+ "LabelAudioBitrate": "音频比特率 (例如: 128k)",
+ "LabelAudioChannels": "音频通道 (1 或 2)",
+ "LabelAudioCodec": "音频编解码器",
"LabelAuthor": "作者",
"LabelAuthorFirstLast": "作者 (姓 名)",
"LabelAuthorLastFirst": "作者 (名, 姓)",
@@ -236,6 +244,7 @@
"LabelAutoRegister": "自动注册",
"LabelAutoRegisterDescription": "登录后自动创建新用户",
"LabelBackToUser": "返回到用户",
+ "LabelBackupAudioFiles": "备份音频文件",
"LabelBackupLocation": "备份位置",
"LabelBackupsEnableAutomaticBackups": "启用自动备份",
"LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups",
@@ -244,15 +253,18 @@
"LabelBackupsNumberToKeep": "要保留的备份个数",
"LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.",
"LabelBitrate": "比特率",
+ "LabelBonus": "额外",
"LabelBooks": "图书",
"LabelButtonText": "按钮文本",
"LabelByAuthor": "由 {0}",
"LabelChangePassword": "修改密码",
"LabelChannels": "声道",
+ "LabelChapterCount": "{0} 章节",
"LabelChapterTitle": "章节标题",
"LabelChapters": "章节",
"LabelChaptersFound": "找到的章节",
"LabelClickForMoreInfo": "点击了解更多信息",
+ "LabelClickToUseCurrentValue": "点击使用当前值",
"LabelClosePlayer": "关闭播放器",
"LabelCodec": "编解码",
"LabelCollapseSeries": "折叠系列",
@@ -302,12 +314,25 @@
"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": "展开子系列",
@@ -335,6 +360,7 @@
"LabelFontScale": "字体比例",
"LabelFontStrikethrough": "删除线",
"LabelFormat": "编码格式",
+ "LabelFull": "完整",
"LabelGenre": "流派",
"LabelGenres": "流派",
"LabelHardDeleteFile": "完全删除文件",
@@ -390,6 +416,10 @@
"LabelLowestPriority": "最低优先级",
"LabelMatchExistingUsersBy": "匹配现有用户",
"LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过 SSO 提供商提供的唯一 id 进行匹配",
+ "LabelMaxEpisodesToDownload": "可下载的最大集数. 输入 0 表示无限制.",
+ "LabelMaxEpisodesToDownloadPerCheck": "每次检查最多可下载新剧集数",
+ "LabelMaxEpisodesToKeep": "要保留的最大剧集数",
+ "LabelMaxEpisodesToKeepHelp": "值为 0 时, 不设置最大限制. 自动下载新剧集后, 如果您有超过 X 个剧集, 它将删除最旧的剧集. 每次新下载时, 只会删除 1 个剧集.",
"LabelMediaPlayer": "媒体播放器",
"LabelMediaType": "媒体类型",
"LabelMetaTag": "元数据标签",
@@ -435,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "OpenID 声明的名称, 该声明包含用户组的列表. 通常称为组
如果已配置 , 应用程序将根据用户的组成员身份自动分配角色, 前提是这些组在声明中以不区分大小写的方式命名为 'Admin', 'User' 或 'Guest'. 声明应包含一个列表, 如果用户属于多个组, 则应用程序将分配与最高访问级别相对应的角色. 如果没有组匹配, 访问将被拒绝.",
"LabelOpenRSSFeed": "打开 RSS 源",
"LabelOverwrite": "覆盖",
+ "LabelPaginationPageXOfY": "第 {0} 页 共 {1} 页",
"LabelPassword": "密码",
"LabelPath": "路径",
"LabelPermanent": "永久的",
"LabelPermissionsAccessAllLibraries": "可以访问所有媒体库",
"LabelPermissionsAccessAllTags": "可以访问所有标签",
"LabelPermissionsAccessExplicitContent": "可以访问显式内容",
+ "LabelPermissionsCreateEreader": "可以创建电子阅读器",
"LabelPermissionsDelete": "可以删除",
"LabelPermissionsDownload": "可以下载",
"LabelPermissionsUpdate": "可以更新",
@@ -464,6 +496,8 @@
"LabelPubDate": "出版日期",
"LabelPublishYear": "发布年份",
"LabelPublishedDate": "已发布 {0}",
+ "LabelPublishedDecade": "出版年代",
+ "LabelPublishedDecades": "出版年代",
"LabelPublisher": "出版商",
"LabelPublishers": "出版商",
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
@@ -483,21 +517,28 @@
"LabelRedo": "重做",
"LabelRegion": "区域",
"LabelReleaseDate": "发布日期",
+ "LabelRemoveAllMetadataAbs": "删除所有 metadata.abs 文件",
+ "LabelRemoveAllMetadataJson": "删除所有 metadata.json 文件",
"LabelRemoveCover": "移除封面",
+ "LabelRemoveMetadataFile": "删除库项目文件夹中的元数据文件",
+ "LabelRemoveMetadataFileHelp": "删除 {0} 文件夹中的所有 metadata.json 和 metadata.abs 文件.",
"LabelRowsPerPage": "每页行数",
"LabelSearchTerm": "搜索项",
"LabelSearchTitle": "搜索标题",
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
"LabelSeason": "季",
+ "LabelSeasonNumber": "第 {0} 季",
"LabelSelectAll": "全选",
"LabelSelectAllEpisodes": "选择所有剧集",
"LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集",
"LabelSelectUsers": "选择用户",
"LabelSendEbookToDevice": "发送电子书到...",
"LabelSequence": "序列",
+ "LabelSerial": "系列",
"LabelSeries": "系列",
"LabelSeriesName": "系列名称",
"LabelSeriesProgress": "系列进度",
+ "LabelServerLogLevel": "服务器日志级别",
"LabelServerYearReview": "服务器年度回顾 ({0})",
"LabelSetEbookAsPrimary": "设置为主",
"LabelSetEbookAsSupplementary": "设置为补充",
@@ -522,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "只有一本书的系列将从系列页面和主页书架中隐藏.",
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
"LabelSettingsLibraryBookshelfView": "媒体库使用书架视图",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "完成百分比大于",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "剩余时间少于 (秒)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "当发生以下情况时将媒体项目标记为已完成",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "跳过继续系列中的早期书籍",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "继续系列主页书架显示系列中未开始的第一本书, 该系列至少有一本书已完成且没有正在进行的书. 启用此设置将从最远完成的书开始系列, 而不是从第一本书开始.",
"LabelSettingsParseSubtitles": "解析副标题",
@@ -586,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} 分钟",
"LabelTimeDurationXSeconds": "{0} 秒",
"LabelTimeInMinutes": "时间 (分钟)",
+ "LabelTimeLeft": "剩余 {0}",
"LabelTimeListened": "收听时间",
"LabelTimeListenedToday": "今日收听的时间",
"LabelTimeRemaining": "剩余 {0}",
@@ -593,6 +638,7 @@
"LabelTitle": "标题",
"LabelToolsEmbedMetadata": "嵌入元数据",
"LabelToolsEmbedMetadataDescription": "将元数据嵌入音频文件, 包括封面图像和章节.",
+ "LabelToolsM4bEncoder": "M4B 编码器",
"LabelToolsMakeM4b": "制作 M4B 有声读物文件",
"LabelToolsMakeM4bDescription": "生成带有嵌入元数据, 封面图像和章节的 .M4B 有声读物文件.",
"LabelToolsSplitM4b": "将 M4B 文件拆分为 MP3 文件",
@@ -605,6 +651,7 @@
"LabelTracksMultiTrack": "多轨",
"LabelTracksNone": "没有音轨",
"LabelTracksSingleTrack": "单轨",
+ "LabelTrailer": "预告",
"LabelType": "类型",
"LabelUnabridged": "未删节",
"LabelUndo": "撤消",
@@ -618,8 +665,10 @@
"LabelUploaderDragAndDrop": "拖放文件或文件夹",
"LabelUploaderDropFiles": "删除文件",
"LabelUploaderItemFetchMetadataHelp": "自动获取标题, 作者和系列",
+ "LabelUseAdvancedOptions": "使用高级选项",
"LabelUseChapterTrack": "使用章节音轨",
"LabelUseFullTrack": "使用完整音轨",
+ "LabelUseZeroForUnlimited": "使用 0 表示无限制",
"LabelUser": "用户",
"LabelUsername": "用户名",
"LabelValue": "值",
@@ -658,25 +707,27 @@
"MessageCheckingCron": "检查计划任务...",
"MessageConfirmCloseFeed": "你确定要关闭此订阅源吗?",
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
- "MessageConfirmDeleteDevice": "您确定要删除电子阅读器设备 \"{0}\" 吗?",
+ "MessageConfirmDeleteDevice": "你确定要删除电子阅读器设备 \"{0}\" 吗?",
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "这将从数据库和文件系统中删除库项目. 你确定吗?",
"MessageConfirmDeleteLibraryItems": "这将从数据库和文件系统中删除 {0} 个库项目. 你确定吗?",
"MessageConfirmDeleteMetadataProvider": "是否确实要删除自定义元数据提供商 \"{0}\" ?",
- "MessageConfirmDeleteNotification": "您确定要删除此通知吗?",
+ "MessageConfirmDeleteNotification": "你确定要删除此通知吗?",
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "你确定要将元数据嵌入到 {0} 个音频文件中吗?",
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
"MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?",
"MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?",
- "MessageConfirmMarkItemFinished": "您确定要将 \"{0}\" 标记为已完成吗?",
- "MessageConfirmMarkItemNotFinished": "您确定要将 \"{0}\" 标记为未完成吗?",
+ "MessageConfirmMarkItemFinished": "你确定要将 \"{0}\" 标记为已完成吗?",
+ "MessageConfirmMarkItemNotFinished": "你确定要将 \"{0}\" 标记为未完成吗?",
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
"MessageConfirmNotificationTestTrigger": "使用测试数据触发此通知吗?",
"MessageConfirmPurgeCache": "清除缓存将删除 /metadata/cache
整个目录. 你确定要删除缓存目录吗?",
"MessageConfirmPurgeItemsCache": "清除项目缓存将删除 /metadata/cache/items
整个目录. 你确定吗?",
"MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份. 你是否想继续吗?",
+ "MessageConfirmQuickMatchEpisodes": "如果找到匹配项, 快速匹配的剧集将覆盖详细信息. 只有不匹配的剧集才会更新. 你确定吗?",
"MessageConfirmReScanLibraryItems": "你确定要重新扫描 {0} 个项目吗?",
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
"MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?",
@@ -684,6 +735,7 @@
"MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?",
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
"MessageConfirmRemoveListeningSessions": "你确定要移除 {0} 收听会话吗?",
+ "MessageConfirmRemoveMetadataFiles": "你确实要删除库项目文件夹中的所有 metadata.{0} 文件吗?",
"MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?",
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
@@ -694,11 +746,12 @@
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
"MessageConfirmResetProgress": "你确定要重置进度吗?",
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
- "MessageConfirmUnlinkOpenId": "您确定要取消该用户与 OpenID 的链接吗?",
+ "MessageConfirmUnlinkOpenId": "你确定要取消该用户与 OpenID 的链接吗?",
"MessageDownloadingEpisode": "正在下载剧集",
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
"MessageEmbedFailed": "嵌入失败!",
"MessageEmbedFinished": "嵌入完成!",
+ "MessageEmbedQueue": "已排队等待元数据嵌入 (队列中有 {0} 个)",
"MessageEpisodesQueuedForDownload": "{0} 个剧集排队等待下载",
"MessageEreaderDevices": "为了确保电子书的送达, 你可能需要将上述电子邮件地址添加为下列每台设备的有效发件人.",
"MessageFeedURLWillBe": "源 URL 将改为 {0}",
@@ -743,6 +796,7 @@
"MessageNoLogs": "无日志",
"MessageNoMediaProgress": "无媒体进度",
"MessageNoNotifications": "无通知",
+ "MessageNoPodcastFeed": "无效播客: 无源",
"MessageNoPodcastsFound": "未找到播客",
"MessageNoResults": "无结果",
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
@@ -759,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "从收藏中创建播放列表",
"MessagePleaseWait": "请稍等...",
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
+ "MessagePodcastSearchField": "输入搜索词或 RSS 源 URL",
+ "MessageQuickEmbedInProgress": "正在进行快速嵌入",
+ "MessageQuickEmbedQueue": "已排队等待快速嵌入 (队列中有 {0} 个)",
+ "MessageQuickMatchAllEpisodes": "快速匹配所有剧集",
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
"MessageRemoveChapter": "移除章节",
"MessageRemoveEpisodes": "移除 {0} 剧集",
@@ -776,6 +834,41 @@
"MessageShareExpiresIn": "到期时间 {0}",
"MessageShareURLWillBe": "分享网址是 {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": "上传成功!",
@@ -793,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "包含媒体文件的文件夹将作为单独的媒体库项目处理.",
"NoteUploaderOnlyAudioFiles": "如果只上传音频文件, 则每个音频文件将作为单独的有声读物处理.",
"NoteUploaderUnsupportedFiles": "不支持的文件将被忽略. 选择或删除文件夹时, 将忽略不在项目文件夹中的其他文件.",
+ "NotificationOnBackupCompletedDescription": "备份完成时触发",
+ "NotificationOnBackupFailedDescription": "备份失败时触发",
+ "NotificationOnEpisodeDownloadedDescription": "当播客节目自动下载时触发",
+ "NotificationOnTestDescription": "测试通知系统的事件",
"PlaceholderNewCollection": "输入收藏夹名称",
"PlaceholderNewFolderPath": "输入文件夹路径",
"PlaceholderNewPlaylist": "输入播放列表名称",
@@ -804,7 +901,7 @@
"StatsBooksFinished": "已完成书籍",
"StatsBooksFinishedThisYear": "今年完成的一些书…",
"StatsBooksListenedTo": "听过的书",
- "StatsCollectionGrewTo": "您的藏书已增长到…",
+ "StatsCollectionGrewTo": "你的藏书已增长到…",
"StatsSessions": "会话",
"StatsSpentListening": "花时间聆听",
"StatsTopAuthor": "热门作者",
@@ -816,14 +913,13 @@
"StatsTopNarrators": "最佳叙述者",
"StatsTotalDuration": "总时长为…",
"StatsYearInReview": "年度回顾",
- "ToastAccountUpdateFailed": "账户更新失败",
"ToastAccountUpdateSuccess": "帐户已更新",
"ToastAppriseUrlRequired": "必须输入 Apprise URL",
+ "ToastAsinRequired": "需要 ASIN",
"ToastAuthorImageRemoveSuccess": "作者图像已删除",
"ToastAuthorNotFound": "未找到作者 \"{0}\"",
"ToastAuthorRemoveSuccess": "作者已删除",
"ToastAuthorSearchNotFound": "未找到作者",
- "ToastAuthorUpdateFailed": "作者更新失败",
"ToastAuthorUpdateMerged": "作者已合并",
"ToastAuthorUpdateSuccess": "作者已更新",
"ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到图像)",
@@ -834,29 +930,29 @@
"ToastBackupDeleteSuccess": "备份已删除",
"ToastBackupInvalidMaxKeep": "要保留的备份数无效",
"ToastBackupInvalidMaxSize": "最大备份大小无效",
- "ToastBackupPathUpdateFailed": "无法更新备份路径",
"ToastBackupRestoreFailed": "备份还原失败",
"ToastBackupUploadFailed": "上传备份失败",
"ToastBackupUploadSuccess": "备份已上传",
"ToastBatchDeleteFailed": "批量删除失败",
"ToastBatchDeleteSuccess": "批量删除成功",
+ "ToastBatchQuickMatchFailed": "批量快速匹配失败!",
+ "ToastBatchQuickMatchStarted": "批量快速匹配 {0} 图书已开始!",
"ToastBatchUpdateFailed": "批量更新失败",
"ToastBatchUpdateSuccess": "批量更新成功",
"ToastBookmarkCreateFailed": "创建书签失败",
"ToastBookmarkCreateSuccess": "书签已添加",
"ToastBookmarkRemoveSuccess": "书签已删除",
- "ToastBookmarkUpdateFailed": "书签更新失败",
"ToastBookmarkUpdateSuccess": "书签已更新",
"ToastCachePurgeFailed": "清除缓存失败",
"ToastCachePurgeSuccess": "缓存清除成功",
"ToastChaptersHaveErrors": "章节有错误",
"ToastChaptersMustHaveTitles": "章节必须有标题",
"ToastChaptersRemoved": "已删除章节",
+ "ToastChaptersUpdated": "章节已更新",
"ToastCollectionItemsAddFailed": "项目添加到收藏夹失败",
"ToastCollectionItemsAddSuccess": "项目添加到收藏夹成功",
"ToastCollectionItemsRemoveSuccess": "项目从收藏夹移除",
"ToastCollectionRemoveSuccess": "收藏夹已删除",
- "ToastCollectionUpdateFailed": "更新收藏夹失败",
"ToastCollectionUpdateSuccess": "收藏夹已更新",
"ToastCoverUpdateFailed": "封面更新失败",
"ToastDeleteFileFailed": "删除文件失败",
@@ -865,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "同名的电子阅读器设备已存在",
"ToastDeviceTestEmailFailed": "无法发送测试电子邮件",
"ToastDeviceTestEmailSuccess": "测试邮件已发送",
- "ToastDeviceUpdateFailed": "无法更新设备",
- "ToastEmailSettingsUpdateFailed": "无法更新电子邮件设置",
"ToastEmailSettingsUpdateSuccess": "电子邮件设置已更新",
"ToastEncodeCancelFailed": "取消编码失败",
"ToastEncodeCancelSucces": "编码已取消",
"ToastEpisodeDownloadQueueClearFailed": "无法清除队列",
"ToastEpisodeDownloadQueueClearSuccess": "剧集下载队列已清空",
+ "ToastEpisodeUpdateSuccess": "已更新 {0} 剧集",
"ToastErrorCannotShare": "无法在此设备上本地共享",
"ToastFailedToLoadData": "加载数据失败",
+ "ToastFailedToMatch": "匹配失败",
"ToastFailedToShare": "分享失败",
- "ToastFailedToUpdateAccount": "无法更新账户",
- "ToastFailedToUpdateUser": "无法更新用户",
+ "ToastFailedToUpdate": "更新失败",
"ToastInvalidImageUrl": "图片网址无效",
+ "ToastInvalidMaxEpisodesToDownload": "可下载的最大集数无效",
"ToastInvalidUrl": "网址无效",
- "ToastItemCoverUpdateFailed": "更新项目封面失败",
"ToastItemCoverUpdateSuccess": "项目封面已更新",
"ToastItemDeletedFailed": "删除项目失败",
"ToastItemDeletedSuccess": "已删除项目",
- "ToastItemDetailsUpdateFailed": "更新项目详细信息失败",
"ToastItemDetailsUpdateSuccess": "项目详细信息已更新",
"ToastItemMarkedAsFinishedFailed": "无法标记为已听完",
"ToastItemMarkedAsFinishedSuccess": "标记为已听完的项目",
"ToastItemMarkedAsNotFinishedFailed": "无法标记为未听完",
"ToastItemMarkedAsNotFinishedSuccess": "标记为未听完的项目",
- "ToastItemUpdateFailed": "更新项目失败",
"ToastItemUpdateSuccess": "项目已更新",
"ToastLibraryCreateFailed": "创建媒体库失败",
"ToastLibraryCreateSuccess": "媒体库 \"{0}\" 创建成功",
@@ -897,37 +990,42 @@
"ToastLibraryDeleteSuccess": "媒体库已删除",
"ToastLibraryScanFailedToStart": "无法启动扫描",
"ToastLibraryScanStarted": "媒体库扫描已启动",
- "ToastLibraryUpdateFailed": "更新图书库失败",
"ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新",
+ "ToastMatchAllAuthorsFailed": "无法匹配所有作者",
+ "ToastMetadataFilesRemovedError": "删除 metadata.{0} 文件时出错",
+ "ToastMetadataFilesRemovedNoneFound": "在库中没有找到 metadata.{0} 文件",
+ "ToastMetadataFilesRemovedNoneRemoved": "没有 metadata.{0} 文件被删除",
+ "ToastMetadataFilesRemovedSuccess": "{0} 个 metadata.{1} 文件被删除",
+ "ToastMustHaveAtLeastOnePath": "必须至少有一个路径",
"ToastNameEmailRequired": "姓名和电子邮件为必填项",
"ToastNameRequired": "姓名为必填项",
+ "ToastNewEpisodesFound": "找到 {0} 个新剧集",
"ToastNewUserCreatedFailed": "无法创建帐户: \"{0}\"",
"ToastNewUserCreatedSuccess": "已创建新帐户",
"ToastNewUserLibraryError": "必须至少选择一个图书馆",
"ToastNewUserPasswordError": "必须有密码, 只有root用户可以有空密码",
"ToastNewUserTagError": "必须至少选择一个标签",
"ToastNewUserUsernameError": "输入用户名",
+ "ToastNoNewEpisodesFound": "没有找到新剧集",
"ToastNoUpdatesNecessary": "无需更新",
"ToastNotificationCreateFailed": "无法创建通知",
"ToastNotificationDeleteFailed": "删除通知失败",
"ToastNotificationFailedMaximum": "最大失败尝试次数必须 >= 0",
"ToastNotificationQueueMaximum": "最大通知队列必须 >= 0",
- "ToastNotificationSettingsUpdateFailed": "无法更新通知设置",
"ToastNotificationSettingsUpdateSuccess": "通知设置已更新",
"ToastNotificationTestTriggerFailed": "无法触发测试通知",
"ToastNotificationTestTriggerSuccess": "触发测试通知",
- "ToastNotificationUpdateFailed": "更新通知失败",
"ToastNotificationUpdateSuccess": "通知已更新",
"ToastPlaylistCreateFailed": "创建播放列表失败",
"ToastPlaylistCreateSuccess": "已成功创建播放列表",
"ToastPlaylistRemoveSuccess": "播放列表已删除",
- "ToastPlaylistUpdateFailed": "更新播放列表失败",
"ToastPlaylistUpdateSuccess": "播放列表已更新",
"ToastPodcastCreateFailed": "创建播客失败",
"ToastPodcastCreateSuccess": "已成功创建播客",
"ToastPodcastGetFeedFailed": "无法获取播客信息",
"ToastPodcastNoEpisodesInFeed": "RSS 订阅中未找到任何剧集",
"ToastPodcastNoRssFeed": "播客没有 RSS 源",
+ "ToastProgressIsNotBeingSynced": "进度未同步, 请重新开始播放",
"ToastProviderCreatedFailed": "无法添加提供商",
"ToastProviderCreatedSuccess": "已添加新提供商",
"ToastProviderNameAndUrlRequired": "名称和网址必需填写",
@@ -950,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "电子书已经发送到设备 \"{0}\"",
"ToastSeriesUpdateFailed": "更新系列失败",
"ToastSeriesUpdateSuccess": "系列已更新",
- "ToastServerSettingsUpdateFailed": "无法更新服务器设置",
"ToastServerSettingsUpdateSuccess": "服务器设置已更新",
"ToastSessionCloseFailed": "关闭会话失败",
"ToastSessionDeleteFailed": "删除会话失败",
"ToastSessionDeleteSuccess": "会话已删除",
+ "ToastSleepTimerDone": "睡眠定时完成... zZzzZz",
"ToastSlugMustChange": "Slug 包含无效字符",
"ToastSlugRequired": "Slug 是必填项",
"ToastSocketConnected": "网络已连接",
"ToastSocketDisconnected": "网络已断开",
"ToastSocketFailedToConnect": "网络连接失败",
"ToastSortingPrefixesEmptyError": "必须至少有 1 个排序前缀",
- "ToastSortingPrefixesUpdateFailed": "无法更新排序前缀",
"ToastSortingPrefixesUpdateSuccess": "排序前缀已更新 ({0} 项)",
"ToastTitleRequired": "标题为必填项",
"ToastUnknownError": "未知错误",
diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json
index be023813be..080d1bac04 100644
--- a/client/strings/zh-tw.json
+++ b/client/strings/zh-tw.json
@@ -1,5 +1,5 @@
{
- "ButtonAdd": "增加",
+ "ButtonAdd": "添加",
"ButtonAddChapters": "新增章節",
"ButtonAddDevice": "新增設備",
"ButtonAddLibrary": "新增庫",
@@ -9,6 +9,7 @@
"ButtonApply": "應用",
"ButtonApplyChapters": "應用到章節",
"ButtonAuthors": "作者",
+ "ButtonBack": "返回",
"ButtonBrowseForFolder": "瀏覽資料夾",
"ButtonCancel": "取消",
"ButtonCancelEncode": "取消編碼",
@@ -16,8 +17,9 @@
"ButtonCheckAndDownloadNewEpisodes": "檢查並下載新劇集",
"ButtonChooseAFolder": "選擇資料夾",
"ButtonChooseFiles": "選擇檔案",
- "ButtonClearFilter": "清除過濾器",
+ "ButtonClearFilter": "清楚過濾器",
"ButtonCloseFeed": "關閉源",
+ "ButtonCloseSession": "關閉開放會話",
"ButtonCollections": "收藏",
"ButtonConfigureScanner": "配置掃描",
"ButtonCreate": "創建",
@@ -27,11 +29,14 @@
"ButtonEdit": "編輯",
"ButtonEditChapters": "編輯章節",
"ButtonEditPodcast": "編輯播客",
+ "ButtonEnable": "啟用",
"ButtonForceReScan": "強制重新掃描",
"ButtonFullPath": "完整路徑",
"ButtonHide": "隱藏",
"ButtonHome": "首頁",
"ButtonIssues": "問題",
+ "ButtonJumpBackward": "向後跳轉",
+ "ButtonJumpForward": "向前跳轉",
"ButtonLatest": "最新",
"ButtonLibrary": "媒體庫",
"ButtonLogout": "登出",
@@ -50,6 +55,7 @@
"ButtonPlay": "播放",
"ButtonPlaying": "正在播放",
"ButtonPlaylists": "播放列表",
+ "ButtonPrevious": "上一個",
"ButtonPreviousChapter": "過去的章節",
"ButtonPurgeAllCache": "清理所有快取",
"ButtonPurgeItemsCache": "清理項目快取",
@@ -73,7 +79,7 @@
"ButtonSaveTracklist": "保存音軌列表",
"ButtonScan": "掃描",
"ButtonScanLibrary": "掃描庫",
- "ButtonSearch": "查找",
+ "ButtonSearch": "搜索",
"ButtonSelectFolderPath": "選擇資料夾路徑",
"ButtonSeries": "系列",
"ButtonSetChaptersFromTracks": "將音軌設定為章節",
@@ -94,7 +100,7 @@
"ErrorUploadFetchMetadataAPI": "獲取元數據時出錯",
"ErrorUploadFetchMetadataNoResults": "無法獲取元數據 - 嘗試更新標題和/或作者",
"ErrorUploadLacksTitle": "必須有標題",
- "HeaderAccount": "帳號",
+ "HeaderAccount": "賬號",
"HeaderAdvanced": "高級",
"HeaderAppriseNotificationSettings": "測試通知設定",
"HeaderAudioTracks": "音軌",
@@ -108,6 +114,7 @@
"HeaderCollectionItems": "收藏項目",
"HeaderCover": "封面",
"HeaderCurrentDownloads": "當前下載",
+ "HeaderCustomMessageOnLogin": "登錄時的自定義信息",
"HeaderCustomMetadataProviders": "自訂 Metadata 提供者",
"HeaderDetails": "詳情",
"HeaderDownloadQueue": "下載佇列",
@@ -141,7 +148,7 @@
"HeaderNewLibrary": "新建媒體庫",
"HeaderNotifications": "通知",
"HeaderOpenIDConnectAuthentication": "OpenID 連接身份驗證",
- "HeaderOpenRSSFeed": "打開 RSS 源",
+ "HeaderOpenRSSFeed": "打開 Rss 源",
"HeaderOtherFiles": "其他檔案",
"HeaderPasswordAuthentication": "密碼認證",
"HeaderPermissions": "權限",
@@ -165,7 +172,7 @@
"HeaderSettingsExperimental": "實驗功能",
"HeaderSettingsGeneral": "通用",
"HeaderSettingsScanner": "掃描",
- "HeaderSleepTimer": "睡眠計時",
+ "HeaderSleepTimer": "睡眠定時",
"HeaderStatsLargestItems": "最大的項目",
"HeaderStatsLongestItems": "項目時長(小時)",
"HeaderStatsMinutesListeningChart": "收聽分鐘數(最近7天)",
@@ -179,8 +186,12 @@
"HeaderUpdateDetails": "更新詳情",
"HeaderUpdateLibrary": "更新媒體庫",
"HeaderUsers": "使用者",
+ "HeaderYearReview": "{0} 年回顧",
"HeaderYourStats": "你的統計數據",
"LabelAbridged": "概要",
+ "LabelAbridgedChecked": "刪節版(已勾選)",
+ "LabelAbridgedUnchecked": "未刪節版(未勾選)",
+ "LabelAccessibleBy": "可訪問",
"LabelAccountType": "帳號類型",
"LabelAccountTypeAdmin": "管理員",
"LabelAccountTypeGuest": "來賓",
@@ -257,26 +268,32 @@
"LabelDownload": "下載",
"LabelDownloadNEpisodes": "下載 {0} 集",
"LabelDuration": "持續時間",
+ "LabelDurationComparisonExactMatch": "(完全匹配)",
+ "LabelDurationComparisonLonger": "({0} 更長)",
+ "LabelDurationComparisonShorter": "({0} 更短)",
"LabelDurationFound": "找到持續時間:",
"LabelEbook": "電子書",
"LabelEbooks": "電子書",
"LabelEdit": "編輯",
"LabelEmail": "郵箱",
"LabelEmailSettingsFromAddress": "發件人位址",
+ "LabelEmailSettingsRejectUnauthorized": "拒絕未經授權的證書",
+ "LabelEmailSettingsRejectUnauthorizedHelp": "停用 SSL 證書驗證可能會使您的連接暴露於安全風險中,例如中間人攻擊。僅在您了解其含義並信任您所連接的郵件伺服器的情況下才停用此選項。",
"LabelEmailSettingsSecure": "安全",
"LabelEmailSettingsSecureHelp": "如果選是, 則連接將在連接到伺服器時使用TLS. 如果選否, 則若伺服器支援STARTTLS擴展, 則使用TLS. 在大多數情況下, 如果連接到465埠, 請將該值設定為是. 對於587或25埠, 請保持為否. (來自nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "測試位址",
"LabelEmbeddedCover": "嵌入封面",
"LabelEnable": "啟用",
"LabelEnd": "結束",
+ "LabelEndOfChapter": "章節結束",
"LabelEpisode": "劇集",
"LabelEpisodeTitle": "劇集標題",
"LabelEpisodeType": "劇集類型",
"LabelExample": "示例",
"LabelExplicit": "信息準確",
- "LabelFeedURL": "源 URL",
+ "LabelFeedURL": "源鏈接",
"LabelFetchingMetadata": "正在獲取元數據",
- "LabelFile": "檔案",
+ "LabelFile": "文件",
"LabelFileBirthtime": "檔案創建時間",
"LabelFileModified": "檔案修改時間",
"LabelFilename": "檔名",
@@ -285,6 +302,7 @@
"LabelFinished": "已聽完",
"LabelFolder": "資料夾",
"LabelFolders": "資料夾",
+ "LabelFontBoldness": "字體粗細",
"LabelFontFamily": "字體系列",
"LabelFontItalic": "斜體",
"LabelFontScale": "字體比例",
@@ -350,7 +368,7 @@
"LabelMobileRedirectURIs": "允許移動應用重定向 URI",
"LabelMobileRedirectURIsDescription": "這是移動應用程序的有效重定向 URI 白名單. 預設值為 audiobookshelf://oauth
,您可以刪除它或加入其他 URI 以進行第三方應用集成. 使用星號 (*
) 作為唯一條目允許任何 URI.",
"LabelMore": "更多",
- "LabelMoreInfo": "更多..",
+ "LabelMoreInfo": "更多信息",
"LabelName": "名稱",
"LabelNarrator": "講述者",
"LabelNarrators": "講述者",
@@ -396,7 +414,7 @@
"LabelPodcasts": "播客",
"LabelPort": "埠",
"LabelPrefixesToIgnore": "忽略的前綴 (不區分大小寫)",
- "LabelPreventIndexing": "防止 iTunes 和 Google 播客目錄對你的源進行索引",
+ "LabelPreventIndexing": "防止您的訂閱源被 iTunes 和 Google 播客目錄索引",
"LabelPrimaryEbook": "主電子書",
"LabelProgress": "進度",
"LabelProvider": "供應商",
@@ -409,6 +427,7 @@
"LabelRSSFeedPreventIndexing": "防止索引",
"LabelRSSFeedSlug": "RSS 源段",
"LabelRSSFeedURL": "RSS 源 URL",
+ "LabelRandomly": "隨機",
"LabelRead": "閱讀",
"LabelReadAgain": "再次閱讀",
"LabelReadEbookWithoutProgress": "閱讀電子書而不保存進度",
@@ -632,20 +651,20 @@
"MessageNoFoldersAvailable": "沒有可用資料夾",
"MessageNoGenres": "無流派",
"MessageNoIssues": "無問題",
- "MessageNoItems": "無項目",
- "MessageNoItemsFound": "未找到任何項目",
- "MessageNoListeningSessions": "無收聽會話",
+ "MessageNoItems": "沒有項目",
+ "MessageNoItemsFound": "沒有找到任何項目",
+ "MessageNoListeningSessions": "沒有收聽會話",
"MessageNoLogs": "無日誌",
"MessageNoMediaProgress": "無媒體進度",
"MessageNoNotifications": "無通知",
- "MessageNoPodcastsFound": "未找到播客",
+ "MessageNoPodcastsFound": "沒有找到播客",
"MessageNoResults": "無結果",
"MessageNoSearchResultsFor": "沒有搜尋到結果 \"{0}\"",
"MessageNoSeries": "無系列",
"MessageNoTags": "無標籤",
"MessageNoTasksRunning": "沒有正在運行的任務",
"MessageNoUpdatesWereNecessary": "無需更新",
- "MessageNoUserPlaylists": "你沒有播放列表",
+ "MessageNoUserPlaylists": "您沒有播放列表",
"MessageNotYetImplemented": "尚未實施",
"MessageOr": "或",
"MessagePauseChapter": "暫停章節播放",
@@ -657,7 +676,7 @@
"MessageRemoveEpisodes": "移除 {0} 劇集",
"MessageRemoveFromPlayerQueue": "從播放佇列中移除",
"MessageRemoveUserWarning": "是否確實要永久刪除使用者 \"{0}\"?",
- "MessageReportBugsAndContribute": "報告錯誤、請求功能和貢獻在",
+ "MessageReportBugsAndContribute": "報告錯誤、請求功能和做出貢獻",
"MessageResetChaptersConfirm": "你確定要重置章節並撤消你所做的更改嗎?",
"MessageRestoreBackupConfirm": "你確定要恢復創建的這個備份",
"MessageRestoreBackupWarning": "恢復備份將覆蓋位於 /config 的整個資料庫並覆蓋 /metadata/items & /metadata/authors 中的圖像. 備份不會修改媒體庫資料夾中的任何檔案. 如果您已啟用伺服器設定將封面和元數據存儲在庫資料夾中,則不會備份或覆蓋這些內容. 將自動刷新使用伺服器的所有客戶端.",
@@ -678,8 +697,8 @@
"NoteChangeRootPassword": "Root 是唯一可以擁有空密碼的使用者",
"NoteChapterEditorTimes": "注意: 第一章開始時間必須保持在 0:00, 最後一章開始時間不能超過有聲書持續時間.",
"NoteFolderPicker": "注意: 將不顯示已映射的資料夾",
- "NoteRSSFeedPodcastAppsHttps": "警告: 大多數播客應用程序都需要 RSS 源 URL 使用 HTTPS",
- "NoteRSSFeedPodcastAppsPubDate": "警告: 您的一集或多集沒有發布日期. 一些播客應用程序要求這樣做.",
+ "NoteRSSFeedPodcastAppsHttps": "警告:大多數播客應用程式要求 RSS 訂閱源 URL 使用 HTTPS",
+ "NoteRSSFeedPodcastAppsPubDate": "警告:您的一個或多個劇集沒有發布日期。某些播客應用程式要求提供此資訊。",
"NoteUploaderFoldersWithMediaFiles": "包含媒體檔案的資料夾將作為單獨的媒體庫項目處理.",
"NoteUploaderOnlyAudioFiles": "如果只上傳音頻檔, 則每個音頻檔將作為單獨的有聲書處理.",
"NoteUploaderUnsupportedFiles": "不支援的檔案將被忽略. 選擇或刪除資料夾時, 將忽略不在項目資料夾中的其他檔案.",
@@ -688,10 +707,8 @@
"PlaceholderNewPlaylist": "輸入播放列表名稱",
"PlaceholderSearch": "查找..",
"PlaceholderSearchEpisode": "搜尋劇集..",
- "ToastAccountUpdateFailed": "帳號更新失敗",
"ToastAccountUpdateSuccess": "帳號已更新",
"ToastAuthorImageRemoveSuccess": "作者圖像已刪除",
- "ToastAuthorUpdateFailed": "作者更新失敗",
"ToastAuthorUpdateMerged": "作者已合併",
"ToastAuthorUpdateSuccess": "作者已更新",
"ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到圖像)",
@@ -704,20 +721,16 @@
"ToastBackupUploadSuccess": "備份已上傳",
"ToastBatchUpdateFailed": "批量更新失敗",
"ToastBatchUpdateSuccess": "批量更新成功",
- "ToastBookmarkCreateFailed": "創建書籤失敗",
+ "ToastBookmarkCreateFailed": "創建書簽失敗",
"ToastBookmarkCreateSuccess": "書籤已新增",
"ToastBookmarkRemoveSuccess": "書籤已刪除",
- "ToastBookmarkUpdateFailed": "書籤更新失敗",
"ToastBookmarkUpdateSuccess": "書籤已更新",
"ToastChaptersHaveErrors": "章節有錯誤",
"ToastChaptersMustHaveTitles": "章節必須有標題",
"ToastCollectionItemsRemoveSuccess": "項目從收藏夾移除",
"ToastCollectionRemoveSuccess": "收藏夾已刪除",
- "ToastCollectionUpdateFailed": "更新收藏夾失敗",
"ToastCollectionUpdateSuccess": "收藏夾已更新",
- "ToastItemCoverUpdateFailed": "更新項目封面失敗",
"ToastItemCoverUpdateSuccess": "項目封面已更新",
- "ToastItemDetailsUpdateFailed": "更新項目詳細信息失敗",
"ToastItemDetailsUpdateSuccess": "項目詳細信息已更新",
"ToastItemMarkedAsFinishedFailed": "標記為聽完失敗",
"ToastItemMarkedAsFinishedSuccess": "標記為聽完的項目",
@@ -729,12 +742,10 @@
"ToastLibraryDeleteSuccess": "媒體庫已刪除",
"ToastLibraryScanFailedToStart": "無法啟動掃描",
"ToastLibraryScanStarted": "媒體庫掃描已啟動",
- "ToastLibraryUpdateFailed": "更新圖書庫失敗",
"ToastLibraryUpdateSuccess": "媒體庫 \"{0}\" 已更新",
"ToastPlaylistCreateFailed": "創建播放列表失敗",
"ToastPlaylistCreateSuccess": "已成功創建播放列表",
"ToastPlaylistRemoveSuccess": "播放列表已刪除",
- "ToastPlaylistUpdateFailed": "更新播放列表失敗",
"ToastPlaylistUpdateSuccess": "播放列表已更新",
"ToastPodcastCreateFailed": "創建播客失敗",
"ToastPodcastCreateSuccess": "已成功創建播客",
diff --git a/docker-compose.yml b/docker-compose.yml
index 68e012fb87..b8d428a233 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,4 @@
### EXAMPLE DOCKER COMPOSE ###
-version: "3.7"
-
services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
@@ -23,8 +21,7 @@ services:
# you are running ABS on
- ./config:/config
restart: unless-stopped
- # You can use the following environment variable to run the ABS
+ # You can use the following user directive to run the ABS
# docker container as a specific user. You will need to change
# the UID and GID to the correct values for your user.
- #environment:
- # - user=1000:1000
+ # user: 1000:1000
diff --git a/index.js b/index.js
index 141c5826ed..de1ed5c302 100644
--- a/index.js
+++ b/index.js
@@ -9,6 +9,7 @@ if (isDev) {
if (devEnv.MetadataPath) process.env.METADATA_PATH = devEnv.MetadataPath
if (devEnv.FFmpegPath) process.env.FFMPEG_PATH = devEnv.FFmpegPath
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
+ if (devEnv.NunicodePath) process.env.NUSQLITE3_PATH = devEnv.NunicodePath
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
process.env.SOURCE = 'local'
diff --git a/package-lock.json b/package-lock.json
index 6f0a3587fd..3f9f7a44ca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
- "version": "2.13.4",
+ "version": "2.17.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
- "version": "2.13.4",
+ "version": "2.17.2",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
diff --git a/package.json b/package.json
index 752b2f8df5..8cbbb029f3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "2.13.4",
+ "version": "2.17.2",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
diff --git a/readme.md b/readme.md
index ce2781ccd6..e62aba033f 100644
--- a/readme.md
+++ b/readme.md
@@ -41,6 +41,13 @@ Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/
Join us on [Discord](https://discord.gg/HQgCbd6E75)
+### Demo
+
+Check out the web client demo: https://audiobooks.dev/ (thanks for hosting [@Vito0912](https://github.com/Vito0912)!)
+
+Username/password: `demo`/`demo` (user account)
+
+
### Android App (beta)
Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
@@ -92,29 +99,33 @@ Toggle websockets support.
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 ..;
+server {
+ listen 443 ssl;
+ server_name ..;
+
+ access_log /var/log/nginx/audiobookshelf.access.log;
+ error_log /var/log/nginx/audiobookshelf.error.log;
- 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;
- 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 $host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
- location / {
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
+ proxy_http_version 1.1;
- proxy_http_version 1.1;
+ proxy_pass http://;
+ proxy_redirect http:// https://;
- 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;
+ }
}
```
@@ -335,7 +346,7 @@ This application is built using [NodeJs](https://nodejs.org/).
### 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).
+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).
### Dev Container Setup
diff --git a/server/Auth.js b/server/Auth.js
index 60af2a1e05..b0046799b9 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -18,6 +18,26 @@ class Auth {
constructor() {
// Map of openId sessions indexed by oauth2 state-variable
this.openIdAuthSession = new Map()
+ this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/]
+ }
+
+ /**
+ * Checks if the request should not be authenticated.
+ * @param {Request} req
+ * @returns {boolean}
+ * @private
+ */
+ authNotNeeded(req) {
+ return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.test(req.originalUrl))
+ }
+
+ ifAuthNeeded(middleware) {
+ return (req, res, next) => {
+ if (this.authNotNeeded(req)) {
+ return next()
+ }
+ middleware(req, res, next)
+ }
}
/**
@@ -970,28 +990,18 @@ class Auth {
})
}
}
-
- Database.userModel
- .update(
- {
- pash: pw
- },
- {
- where: { id: matchingUser.id }
- }
- )
- .then(() => {
- Logger.info(`[Auth] User "${matchingUser.username}" changed password`)
- res.json({
- success: true
- })
+ try {
+ await matchingUser.update({ pash: pw })
+ Logger.info(`[Auth] User "${matchingUser.username}" changed password`)
+ res.json({
+ success: true
})
- .catch((error) => {
- Logger.error(`[Auth] User "${matchingUser.username}" failed to change password`, error)
- res.json({
- error: 'Unknown error'
- })
+ } catch (error) {
+ Logger.error(`[Auth] User "${matchingUser.username}" failed to change password`, error)
+ res.json({
+ error: 'Unknown error'
})
+ }
}
}
diff --git a/server/Database.js b/server/Database.js
index e7bad49ba7..95e13c6b4c 100644
--- a/server/Database.js
+++ b/server/Database.js
@@ -28,6 +28,9 @@ class Database {
this.notificationSettings = null
/** @type {import('./objects/settings/EmailSettings')} */
this.emailSettings = null
+
+ this.supportsUnaccent = false
+ this.supportsUnicodeFoldings = false
}
get models() {
@@ -223,6 +226,12 @@ class Database {
try {
await this.sequelize.authenticate()
+ if (process.env.NUSQLITE3_PATH) {
+ await this.loadExtension(process.env.NUSQLITE3_PATH)
+ Logger.info(`[Database] Db supports unaccent and unicode foldings`)
+ this.supportsUnaccent = true
+ this.supportsUnicodeFoldings = true
+ }
Logger.info(`[Database] Db connection was successful`)
return true
} catch (error) {
@@ -232,10 +241,9 @@ class Database {
}
/**
- * TODO: Temporarily disabled
- * @param {string[]} extensions paths to extension binaries
+ * @param {string} extension paths to extension binary
*/
- async loadExtensions(extensions) {
+ async loadExtension(extension) {
// This is a hack to get the db connection for loading extensions.
// The proper way would be to use the 'afterConnect' hook, but that hook is never called for sqlite due to a bug in sequelize.
// See https://github.com/sequelize/sequelize/issues/12487
@@ -243,20 +251,18 @@ class Database {
const db = await this.sequelize.dialect.connectionManager.getConnection()
if (typeof db?.loadExtension !== 'function') throw new Error('Failed to get db connection for loading extensions')
- for (const ext of extensions) {
- Logger.info(`[Database] Loading extension ${ext}`)
- await new Promise((resolve, reject) => {
- db.loadExtension(ext, (err) => {
- if (err) {
- Logger.error(`[Database] Failed to load extension ${ext}`, err)
- reject(err)
- return
- }
- Logger.info(`[Database] Successfully loaded extension ${ext}`)
- resolve()
- })
+ Logger.info(`[Database] Loading extension ${extension}`)
+ await new Promise((resolve, reject) => {
+ db.loadExtension(extension, (err) => {
+ if (err) {
+ Logger.error(`[Database] Failed to load extension ${extension}`, err)
+ reject(err)
+ return
+ }
+ Logger.info(`[Database] Successfully loaded extension ${extension}`)
+ resolve()
})
- }
+ })
}
/**
@@ -400,11 +406,6 @@ class Database {
return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook)))
}
- removeLibrary(libraryId) {
- if (!this.sequelize) return false
- return this.models.library.removeById(libraryId)
- }
-
createBulkCollectionBooks(collectionBooks) {
if (!this.sequelize) return false
return this.models.collectionBook.bulkCreate(collectionBooks)
@@ -601,6 +602,11 @@ class Database {
this.libraryFilterData[libraryId].publishers.push(publisher)
}
+ addPublishedDecadeToFilterData(libraryId, decade) {
+ if (!this.libraryFilterData[libraryId] || !decade || this.libraryFilterData[libraryId].publishedDecades.includes(decade)) return
+ this.libraryFilterData[libraryId].publishedDecades.push(decade)
+ }
+
addLanguageToFilterData(libraryId, language) {
if (!this.libraryFilterData[libraryId] || !language || this.libraryFilterData[libraryId].languages.includes(language)) return
this.libraryFilterData[libraryId].languages.push(language)
@@ -745,37 +751,57 @@ class Database {
}
}
- /**
- * TODO: Temporarily unused
- * @param {string} value
- * @returns {string}
- */
- normalize(value) {
- return `lower(unaccent(${value}))`
+ async createTextSearchQuery(query) {
+ const textQuery = new this.TextSearchQuery(this.sequelize, this.supportsUnaccent, query)
+ await textQuery.init()
+ return textQuery
}
- /**
- * TODO: Temporarily unused
- * @param {string} query
- * @returns {Promise}
- */
- async getNormalizedQuery(query) {
- const escapedQuery = this.sequelize.escape(query)
- const normalizedQuery = this.normalize(escapedQuery)
- const normalizedQueryResult = await this.sequelize.query(`SELECT ${normalizedQuery} as normalized_query`)
- return normalizedQueryResult[0][0].normalized_query
- }
+ TextSearchQuery = class {
+ constructor(sequelize, supportsUnaccent, query) {
+ this.sequelize = sequelize
+ this.supportsUnaccent = supportsUnaccent
+ this.query = query
+ this.hasAccents = false
+ }
- /**
- *
- * @param {string} column
- * @param {string} normalizedQuery
- * @returns {string}
- */
- matchExpression(column, normalizedQuery) {
- const normalizedPattern = this.sequelize.escape(`%${normalizedQuery}%`)
- const normalizedColumn = column
- return `${normalizedColumn} LIKE ${normalizedPattern}`
+ /**
+ * Returns a normalized (accents-removed) expression for the specified value.
+ *
+ * @param {string} value
+ * @returns {string}
+ */
+ normalize(value) {
+ return `unaccent(${value})`
+ }
+
+ /**
+ * Initialize the text query.
+ *
+ */
+ async init() {
+ if (!this.supportsUnaccent) return
+ const escapedQuery = this.sequelize.escape(this.query)
+ const normalizedQueryExpression = this.normalize(escapedQuery)
+ const normalizedQueryResult = await this.sequelize.query(`SELECT ${normalizedQueryExpression} as normalized_query`)
+ const normalizedQuery = normalizedQueryResult[0][0].normalized_query
+ this.hasAccents = escapedQuery !== this.sequelize.escape(normalizedQuery)
+ }
+
+ /**
+ * Get match expression for the specified column.
+ * If the query contains accents, match against the column as-is (case-insensitive exact match).
+ * otherwise match against a normalized column (case-insensitive match with accents removed).
+ *
+ * @param {string} column
+ * @returns {string}
+ */
+ matchExpression(column) {
+ const pattern = this.sequelize.escape(`%${this.query}%`)
+ if (!this.supportsUnaccent) return `${column} LIKE ${pattern}`
+ const normalizedColumn = this.hasAccents ? column : this.normalize(column)
+ return `${normalizedColumn} LIKE ${pattern}`
+ }
}
}
diff --git a/server/Server.js b/server/Server.js
index 17466e863c..ae9746d8d4 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -23,7 +23,6 @@ const HlsRouter = require('./routers/HlsRouter')
const PublicRouter = require('./routers/PublicRouter')
const LogManager = require('./managers/LogManager')
-const NotificationManager = require('./managers/NotificationManager')
const EmailManager = require('./managers/EmailManager')
const AbMergeManager = require('./managers/AbMergeManager')
const CacheManager = require('./managers/CacheManager')
@@ -63,16 +62,14 @@ class Server {
fs.mkdirSync(global.MetadataPath)
}
- this.watcher = new Watcher()
this.auth = new Auth()
// Managers
- this.notificationManager = new NotificationManager()
this.emailManager = new EmailManager()
- this.backupManager = new BackupManager(this.notificationManager)
+ this.backupManager = new BackupManager()
this.abMergeManager = new AbMergeManager()
this.playbackSessionManager = new PlaybackSessionManager()
- this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
+ this.podcastManager = new PodcastManager()
this.audioMetadataManager = new AudioMetadataMangaer()
this.rssFeedManager = new RssFeedManager()
this.cronManager = new CronManager(this.podcastManager, this.playbackSessionManager)
@@ -149,9 +146,12 @@ class Server {
if (Database.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`)
- this.watcher.disabled = true
+ Watcher.disabled = true
} else {
- this.watcher.initWatcher(libraries)
+ Watcher.initWatcher(libraries)
+ Watcher.on('scanFilesChanged', (pendingFileUpdates, pendingTask) => {
+ LibraryScanner.scanFilesChanged(pendingFileUpdates, pendingTask)
+ })
}
}
@@ -194,18 +194,21 @@ class Server {
const app = express()
- /**
- * @temporary
- * This is necessary for the ebook & cover API endpoint in the mobile apps
- * The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
- * so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
- * The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors
- * @see https://ionicframework.com/docs/troubleshooting/cors
- *
- * Running in development allows cors to allow testing the mobile apps in the browser
- * or env variable ALLOW_CORS = '1'
- */
app.use((req, res, next) => {
+ // Prevent clickjacking by disallowing iframes
+ res.setHeader('Content-Security-Policy', "frame-ancestors 'self'")
+
+ /**
+ * @temporary
+ * This is necessary for the ebook & cover API endpoint in the mobile apps
+ * The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
+ * so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
+ * The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors
+ * @see https://ionicframework.com/docs/troubleshooting/cors
+ *
+ * Running in development allows cors to allow testing the mobile apps in the browser
+ * or env variable ALLOW_CORS = '1'
+ */
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
@@ -240,11 +243,20 @@ class Server {
// init passport.js
app.use(passport.initialize())
// register passport in express-session
- app.use(passport.session())
+ app.use(this.auth.ifAuthNeeded(passport.session()))
// config passport.js
await this.auth.initPassportJs()
const router = express.Router()
+ // if RouterBasePath is set, modify all requests to include the base path
+ if (global.RouterBasePath) {
+ app.use((req, res, next) => {
+ if (!req.url.startsWith(global.RouterBasePath)) {
+ req.url = `${global.RouterBasePath}${req.url}`
+ }
+ next()
+ })
+ }
app.use(global.RouterBasePath, router)
app.disable('x-powered-by')
@@ -261,6 +273,10 @@ class Server {
router.use(express.urlencoded({ extended: true, limit: '5mb' }))
router.use(express.json({ limit: '5mb' }))
+ router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router)
+ router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
+ router.use('/public', this.publicRouter.router)
+
// Static path to generated nuxt
const distPath = Path.join(global.appRoot, '/client/dist')
router.use(express.static(distPath))
@@ -268,10 +284,6 @@ class Server {
// Static folder
router.use(express.static(Path.join(global.appRoot, 'static')))
- router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
- router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
- router.use('/public', this.publicRouter.router)
-
// RSS Feed temp route
router.get('/feed/:slug', (req, res) => {
Logger.info(`[Server] Requesting rss feed ${req.params.slug}`)
@@ -289,7 +301,7 @@ class Server {
await this.auth.initAuthRoutes(router)
// Client dynamic routes
- const dyanimicRoutes = [
+ const dynamicRoutes = [
'/item/:id',
'/author/:id',
'/audiobook/:id/chapters',
@@ -312,7 +324,7 @@ class Server {
'/playlist/:id',
'/share/:slug'
]
- dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
+ dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
router.post('/init', (req, res) => {
if (Database.hasRootUser) {
@@ -342,7 +354,7 @@ class Server {
Logger.info('Received ping')
res.json({ success: true })
})
- app.get('/healthcheck', (req, res) => res.sendStatus(200))
+ router.get('/healthcheck', (req, res) => res.sendStatus(200))
this.server.listen(this.Port, this.Host, () => {
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
@@ -428,7 +440,7 @@ class Server {
*/
async stop() {
Logger.info('=== Stopping Server ===')
- await this.watcher.close()
+ Watcher.close()
Logger.info('Watcher Closed')
return new Promise((resolve) => {
diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js
index af8204c606..a718293617 100644
--- a/server/SocketAuthority.js
+++ b/server/SocketAuthority.js
@@ -103,7 +103,8 @@ class SocketAuthority {
cors: {
origin: '*',
methods: ['GET', 'POST']
- }
+ },
+ path: `${global.RouterBasePath}/socket.io`
})
this.io.on('connection', (socket) => {
diff --git a/server/Watcher.js b/server/Watcher.js
index 0e34fc66bf..85c13e2a49 100644
--- a/server/Watcher.js
+++ b/server/Watcher.js
@@ -2,7 +2,6 @@ const Path = require('path')
const EventEmitter = require('events')
const Watcher = require('./libs/watcher/watcher')
const Logger = require('./Logger')
-const LibraryScanner = require('./scanner/LibraryScanner')
const Task = require('./objects/Task')
const TaskManager = require('./managers/TaskManager')
@@ -31,6 +30,8 @@ class FolderWatcher extends EventEmitter {
this.filesBeingAdded = new Set()
+ /** @type {Set} */
+ this.ignoreFilePathsDownloading = new Set()
/** @type {string[]} */
this.ignoreDirs = []
/** @type {string[]} */
@@ -333,7 +334,7 @@ class FolderWatcher extends EventEmitter {
}
if (this.pendingFileUpdates.length) {
- LibraryScanner.scanFilesChanged(this.pendingFileUpdates, this.pendingTask)
+ this.emit('scanFilesChanged', this.pendingFileUpdates, this.pendingTask)
} else {
const taskFinishedString = {
text: 'No files to scan',
@@ -348,12 +349,29 @@ class FolderWatcher extends EventEmitter {
}, this.pendingDelay)
}
+ /**
+ *
+ * @param {string} path
+ * @returns {boolean}
+ */
checkShouldIgnorePath(path) {
return !!this.ignoreDirs.find((dirpath) => {
return isSameOrSubPath(dirpath, path)
})
}
+ /**
+ * When scanning a library item folder these files should be ignored
+ * Either a podcast episode downloading or a file that is pending by the watcher
+ *
+ * @param {string} path
+ * @returns {boolean}
+ */
+ checkShouldIgnoreFilePath(path) {
+ if (this.pendingFilePaths.includes(path)) return true
+ return this.ignoreFilePathsDownloading.has(path)
+ }
+
/**
* Convert to POSIX and remove trailing slash
* @param {string} path
@@ -409,4 +427,4 @@ class FolderWatcher extends EventEmitter {
}, 5000)
}
}
-module.exports = FolderWatcher
+module.exports = new FolderWatcher()
diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js
index 54a6418563..45bbdf84bd 100644
--- a/server/controllers/AuthorController.js
+++ b/server/controllers/AuthorController.js
@@ -381,16 +381,23 @@ class AuthorController {
*/
async getImage(req, res) {
const {
- query: { width, height, format, raw },
- author
+ query: { width, height, format, raw }
} = req
- if (!author.imagePath || !(await fs.pathExists(author.imagePath))) {
- Logger.warn(`[AuthorController] Author "${author.name}" has invalid imagePath: ${author.imagePath}`)
- return res.sendStatus(404)
- }
+ const authorId = req.params.id
if (raw) {
+ const author = await Database.authorModel.findByPk(authorId)
+ if (!author) {
+ Logger.warn(`[AuthorController] Author "${authorId}" not found`)
+ return res.sendStatus(404)
+ }
+
+ if (!author.imagePath || !(await fs.pathExists(author.imagePath))) {
+ Logger.warn(`[AuthorController] Author "${author.name}" has invalid imagePath: ${author.imagePath}`)
+ return res.sendStatus(404)
+ }
+
return res.sendFile(author.imagePath)
}
@@ -399,7 +406,7 @@ class AuthorController {
height: height ? parseInt(height) : null,
width: width ? parseInt(width) : null
}
- return CacheManager.handleAuthorCache(res, author, options)
+ return CacheManager.handleAuthorCache(res, authorId, options)
}
/**
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 65243acc44..84d6193d5e 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -9,7 +9,6 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
const seriesFilters = require('../utils/queries/seriesFilters')
const fileUtils = require('../utils/fileUtils')
-const { asciiOnlyToLowerCase } = require('../utils/index')
const { createNewSortInstance } = require('../libs/fastSort')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
@@ -18,6 +17,7 @@ const naturalSort = createNewSortInstance({
const LibraryScanner = require('../scanner/LibraryScanner')
const Scanner = require('../scanner/Scanner')
const Database = require('../Database')
+const Watcher = require('../Watcher')
const libraryFilters = require('../utils/queries/libraryFilters')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
const authorFilters = require('../utils/queries/authorFilters')
@@ -159,7 +159,7 @@ class LibraryController {
SocketAuthority.emitter('library_added', library.toOldJSON(), userFilter)
// Add library watcher
- this.watcher.addLibrary(library)
+ Watcher.addLibrary(library)
res.json(library.toOldJSON())
}
@@ -236,12 +236,14 @@ class LibraryController {
for (const key of keysToCheck) {
if (!req.body[key]) continue
if (typeof req.body[key] !== 'string') {
+ Logger.error(`[LibraryController] Invalid request. ${key} must be a string`)
return res.status(400).send(`Invalid request. ${key} must be a string`)
}
updatePayload[key] = req.body[key]
}
if (req.body.displayOrder !== undefined) {
if (isNaN(req.body.displayOrder)) {
+ Logger.error(`[LibraryController] Invalid request. displayOrder must be a number`)
return res.status(400).send('Invalid request. displayOrder must be a number')
}
updatePayload.displayOrder = req.body.displayOrder
@@ -256,18 +258,29 @@ class LibraryController {
}
// Validate settings
+ const defaultLibrarySettings = Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType)
const updatedSettings = {
- ...(req.library.settings || Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType))
+ ...(req.library.settings || defaultLibrarySettings)
}
+ // In case new settings are added in the future, ensure all settings are present
+ for (const key in defaultLibrarySettings) {
+ if (updatedSettings[key] === undefined) {
+ updatedSettings[key] = defaultLibrarySettings[key]
+ }
+ }
+
let hasUpdates = false
let hasUpdatedDisableWatcher = false
let hasUpdatedScanCron = false
if (req.body.settings) {
for (const key in req.body.settings) {
- if (updatedSettings[key] === undefined) continue
+ if (!Object.keys(defaultLibrarySettings).includes(key)) {
+ continue
+ }
if (key === 'metadataPrecedence') {
if (!Array.isArray(req.body.settings[key])) {
+ Logger.error(`[LibraryController] Invalid request. Settings "metadataPrecedence" must be an array`)
return res.status(400).send('Invalid request. Settings "metadataPrecedence" must be an array')
}
if (JSON.stringify(req.body.settings[key]) !== JSON.stringify(updatedSettings[key])) {
@@ -277,6 +290,7 @@ class LibraryController {
}
} else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') {
if (req.body.settings[key] !== null && typeof req.body.settings[key] !== 'string') {
+ Logger.error(`[LibraryController] Invalid request. Settings "${key}" must be a string`)
return res.status(400).send(`Invalid request. Settings "${key}" must be a string`)
}
if (req.body.settings[key] !== updatedSettings[key]) {
@@ -286,8 +300,35 @@ class LibraryController {
updatedSettings[key] = req.body.settings[key]
Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
}
+ } else if (key === 'markAsFinishedPercentComplete') {
+ if (req.body.settings[key] !== null && isNaN(req.body.settings[key])) {
+ Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be a number`)
+ return res.status(400).send(`Invalid request. Setting "${key}" must be a number`)
+ } else if (req.body.settings[key] !== null && (Number(req.body.settings[key]) < 0 || Number(req.body.settings[key]) > 100)) {
+ Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be between 0 and 100`)
+ return res.status(400).send(`Invalid request. Setting "${key}" must be between 0 and 100`)
+ }
+ if (req.body.settings[key] !== updatedSettings[key]) {
+ hasUpdates = true
+ updatedSettings[key] = Number(req.body.settings[key])
+ Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
+ }
+ } else if (key === 'markAsFinishedTimeRemaining') {
+ if (req.body.settings[key] !== null && isNaN(req.body.settings[key])) {
+ Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be a number`)
+ return res.status(400).send(`Invalid request. Setting "${key}" must be a number`)
+ } else if (req.body.settings[key] !== null && Number(req.body.settings[key]) < 0) {
+ Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be greater than or equal to 0`)
+ return res.status(400).send(`Invalid request. Setting "${key}" must be greater than or equal to 0`)
+ }
+ if (req.body.settings[key] !== updatedSettings[key]) {
+ hasUpdates = true
+ updatedSettings[key] = Number(req.body.settings[key])
+ Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
+ }
} else {
if (typeof req.body.settings[key] !== typeof updatedSettings[key]) {
+ Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be of type ${typeof updatedSettings[key]}`)
return res.status(400).send(`Invalid request. Setting "${key}" must be of type ${typeof updatedSettings[key]}`)
}
if (req.body.settings[key] !== updatedSettings[key]) {
@@ -329,6 +370,7 @@ class LibraryController {
return false
})
if (!success) {
+ Logger.error(`[LibraryController] Invalid folder directory "${path}"`)
return res.status(400).send(`Invalid folder directory "${path}"`)
}
}
@@ -399,7 +441,7 @@ class LibraryController {
req.library.libraryFolders = await req.library.getLibraryFolders()
// Update watcher
- this.watcher.updateLibrary(req.library)
+ Watcher.updateLibrary(req.library)
hasUpdates = true
}
@@ -425,7 +467,7 @@ class LibraryController {
*/
async delete(req, res) {
// Remove library watcher
- this.watcher.removeLibrary(req.library)
+ Watcher.removeLibrary(req.library)
// Remove collections for library
const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(req.library.id)
@@ -462,8 +504,21 @@ class LibraryController {
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
}
+ // Set PlaybackSessions libraryId to null
+ const [sessionsUpdated] = await Database.playbackSessionModel.update(
+ {
+ libraryId: null
+ },
+ {
+ where: {
+ libraryId: req.library.id
+ }
+ }
+ )
+ Logger.info(`[LibraryController] Updated ${sessionsUpdated} playback sessions to remove library id`)
+
const libraryJson = req.library.toOldJSON()
- await Database.removeLibrary(req.library.id)
+ await req.library.destroy()
// Re-order libraries
await Database.libraryModel.resetDisplayOrder()
@@ -493,8 +548,8 @@ class LibraryController {
const payload = {
results: [],
total: undefined,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
+ limit: req.query.limit || 0,
+ page: req.query.page || 0,
sortBy: req.query.sort,
sortDesc: req.query.desc === '1',
filterBy: req.query.filter,
@@ -503,6 +558,7 @@ class LibraryController {
collapseseries: req.query.collapseseries === '1',
include: include.join(',')
}
+
payload.offset = payload.page * payload.limit
// TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
@@ -594,8 +650,8 @@ class LibraryController {
const payload = {
results: [],
total: 0,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
+ limit: req.query.limit || 0,
+ page: req.query.page || 0,
sortBy: req.query.sort,
sortDesc: req.query.desc === '1',
filterBy: req.query.filter,
@@ -666,8 +722,8 @@ class LibraryController {
const payload = {
results: [],
total: 0,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
+ limit: req.query.limit || 0,
+ page: req.query.page || 0,
sortBy: req.query.sort,
sortDesc: req.query.desc === '1',
filterBy: req.query.filter,
@@ -702,8 +758,8 @@ class LibraryController {
const payload = {
results: [],
total: playlistsForUser.length,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
+ limit: req.query.limit || 0,
+ page: req.query.page || 0
}
if (payload.limit) {
@@ -734,7 +790,7 @@ class LibraryController {
* @param {Response} res
*/
async getUserPersonalizedShelves(req, res) {
- const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
+ const limitPerShelf = req.query.limit || 10
const include = (req.query.include || '')
.split(',')
.map((v) => v.trim().toLowerCase())
@@ -807,8 +863,8 @@ class LibraryController {
return res.status(400).send('Invalid request. Query param "q" must be a string')
}
- const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
- const query = asciiOnlyToLowerCase(req.query.q.trim())
+ const limit = req.query.limit || 12
+ const query = req.query.q.trim()
const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
res.json(matches)
@@ -865,8 +921,40 @@ class LibraryController {
* @param {Response} res
*/
async getAuthors(req, res) {
+ const isPaginated = req.query.limit && !isNaN(req.query.limit) && !isNaN(req.query.page)
+
+ const payload = {
+ results: [],
+ total: 0,
+ limit: isPaginated ? Number(req.query.limit) : 0,
+ page: isPaginated ? Number(req.query.page) : 0,
+ sortBy: req.query.sort,
+ sortDesc: req.query.desc === '1',
+ filterBy: req.query.filter,
+ minified: req.query.minified === '1',
+ include: req.query.include
+ }
+
+ // create order, limit and offset for pagination
+ let offset = isPaginated ? payload.page * payload.limit : undefined
+ let limit = isPaginated ? payload.limit : undefined
+ let order = undefined
+ const direction = payload.sortDesc ? 'DESC' : 'ASC'
+ if (payload.sortBy === 'name') {
+ order = [[Sequelize.literal('name COLLATE NOCASE'), direction]]
+ } else if (payload.sortBy === 'lastFirst') {
+ order = [[Sequelize.literal('lastFirst COLLATE NOCASE'), direction]]
+ } else if (payload.sortBy === 'addedAt') {
+ order = [['createdAt', direction]]
+ } else if (payload.sortBy === 'updatedAt') {
+ order = [['updatedAt', direction]]
+ } else if (payload.sortBy === 'numBooks') {
+ offset = undefined
+ limit = undefined
+ }
+
const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user)
- const authors = await Database.authorModel.findAll({
+ const { rows: authors, count } = await Database.authorModel.findAndCountAll({
where: {
libraryId: req.library.id
},
@@ -880,10 +968,13 @@ class LibraryController {
attributes: []
}
},
- order: [[Sequelize.literal('name COLLATE NOCASE'), 'ASC']]
+ order: order,
+ limit: limit,
+ offset: offset,
+ distinct: true
})
- const oldAuthors = []
+ let oldAuthors = []
for (const author of authors) {
const oldAuthor = author.toOldJSONExpanded(author.books.length)
@@ -891,9 +982,25 @@ class LibraryController {
oldAuthors.push(oldAuthor)
}
- res.json({
- authors: oldAuthors
- })
+ // numBooks sort is handled post-query
+ if (payload.sortBy === 'numBooks') {
+ oldAuthors.sort((a, b) => (payload.sortDesc ? b.numBooks - a.numBooks : a.numBooks - b.numBooks))
+ if (isPaginated) {
+ const startIndex = payload.page * payload.limit
+ const endIndex = startIndex + payload.limit
+ oldAuthors = oldAuthors.slice(startIndex, endIndex)
+ }
+ }
+
+ payload.results = oldAuthors
+ if (isPaginated) {
+ payload.total = count
+ res.json(payload)
+ } else {
+ res.json({
+ authors: payload.results
+ })
+ }
}
/**
@@ -1088,8 +1195,8 @@ class LibraryController {
const payload = {
episodes: [],
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
+ limit: req.query.limit || 0,
+ page: req.query.page || 0
}
const offset = payload.page * payload.limit
@@ -1175,6 +1282,44 @@ class LibraryController {
})
}
+ /**
+ * GET: /api/libraries/:id/podcast-titles
+ *
+ * Get podcast titles with itunesId and libraryItemId for library
+ * Used on the podcast add page in order to check if a podcast is already in the library and redirect to it
+ *
+ * @param {LibraryControllerRequest} req
+ * @param {Response} res
+ */
+ async getPodcastTitles(req, res) {
+ if (!req.user.isAdminOrUp) {
+ Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to get podcast titles`)
+ return res.sendStatus(403)
+ }
+
+ const podcasts = await Database.podcastModel.findAll({
+ attributes: ['id', 'title', 'itunesId'],
+ include: {
+ model: Database.libraryItemModel,
+ attributes: ['id', 'libraryId'],
+ where: {
+ libraryId: req.library.id
+ }
+ }
+ })
+
+ res.json({
+ podcasts: podcasts.map((p) => {
+ return {
+ title: p.title,
+ itunesId: p.itunesId,
+ libraryItemId: p.libraryItem.id,
+ libraryId: p.libraryItem.libraryId
+ }
+ })
+ })
+ }
+
/**
*
* @param {RequestWithUser} req
@@ -1192,6 +1337,17 @@ class LibraryController {
return res.status(404).send('Library not found')
}
req.library = library
+
+ // Ensure pagination query params are positive integers
+ for (const queryKey of ['limit', 'page']) {
+ if (req.query[queryKey] !== undefined) {
+ req.query[queryKey] = !isNaN(req.query[queryKey]) ? Number(req.query[queryKey]) : 0
+ if (!Number.isInteger(req.query[queryKey]) || req.query[queryKey] < 0) {
+ return res.status(400).send(`Invalid request. ${queryKey} must be a positive integer`)
+ }
+ }
+ }
+
next()
}
}
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index fe8539bc32..64069ac574 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -115,6 +115,16 @@ class LibraryItemController {
res.sendStatus(200)
}
+ static handleDownloadError(error, res) {
+ if (!res.headersSent) {
+ if (error.code === 'ENOENT') {
+ return res.status(404).send('File not found')
+ } else {
+ return res.status(500).send('Download failed')
+ }
+ }
+ }
+
/**
* GET: /api/items/:id/download
* Download library item. Zip file if multiple files.
@@ -122,7 +132,7 @@ class LibraryItemController {
* @param {RequestWithUser} req
* @param {Response} res
*/
- download(req, res) {
+ async download(req, res) {
if (!req.user.canDownload) {
Logger.warn(`User "${req.user.username}" attempted to download without permission`)
return res.sendStatus(403)
@@ -130,21 +140,26 @@ class LibraryItemController {
const libraryItemPath = req.libraryItem.path
const itemTitle = req.libraryItem.media.metadata.title
- // If library item is a single file in root dir then no need to zip
- if (req.libraryItem.isFile) {
- // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
- const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryItemPath))
- if (audioMimeType) {
- res.setHeader('Content-Type', audioMimeType)
+ Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
+
+ try {
+ // If library item is a single file in root dir then no need to zip
+ if (req.libraryItem.isFile) {
+ // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
+ const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryItemPath))
+ if (audioMimeType) {
+ res.setHeader('Content-Type', audioMimeType)
+ }
+ await new Promise((resolve, reject) => res.download(libraryItemPath, req.libraryItem.relPath, (error) => (error ? reject(error) : resolve())))
+ } else {
+ const filename = `${itemTitle}.zip`
+ await zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
}
- Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
- res.download(libraryItemPath, req.libraryItem.relPath)
- return
+ Logger.info(`[LibraryItemController] Downloaded item "${itemTitle}" at "${libraryItemPath}"`)
+ } catch (error) {
+ Logger.error(`[LibraryItemController] Download failed for item "${itemTitle}" at "${libraryItemPath}"`, error)
+ LibraryItemController.handleDownloadError(error, res)
}
-
- Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
- const filename = `${itemTitle}.zip`
- zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
}
/**
@@ -327,44 +342,25 @@ class LibraryItemController {
query: { width, height, format, raw }
} = req
- const libraryItem = await Database.libraryItemModel.findByPk(req.params.id, {
- attributes: ['id', 'mediaType', 'mediaId', 'libraryId'],
- include: [
- {
- model: Database.bookModel,
- attributes: ['id', 'coverPath', 'tags', 'explicit']
- },
- {
- model: Database.podcastModel,
- attributes: ['id', 'coverPath', 'tags', 'explicit']
- }
- ]
- })
- if (!libraryItem) {
- Logger.warn(`[LibraryItemController] getCover: Library item "${req.params.id}" does not exist`)
- return res.sendStatus(404)
- }
-
- // Check if user can access this library item
- if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
- return res.sendStatus(403)
- }
+ if (req.query.ts) res.set('Cache-Control', 'private, max-age=86400')
- // Check if library item media has a cover path
- if (!libraryItem.media.coverPath || !(await fs.pathExists(libraryItem.media.coverPath))) {
- return res.sendStatus(404)
+ const libraryItemId = req.params.id
+ if (!libraryItemId) {
+ return res.sendStatus(400)
}
- if (req.query.ts) res.set('Cache-Control', 'private, max-age=86400')
-
if (raw) {
+ const coverPath = await Database.libraryItemModel.getCoverPath(libraryItemId)
+ if (!coverPath || !(await fs.pathExists(coverPath))) {
+ return res.sendStatus(404)
+ }
// any value
if (global.XAccel) {
- const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath)
+ const encodedURI = encodeUriPath(global.XAccel + coverPath)
Logger.debug(`Use X-Accel to serve static file ${encodedURI}`)
return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
}
- return res.sendFile(libraryItem.media.coverPath)
+ return res.sendFile(coverPath)
}
const options = {
@@ -372,7 +368,7 @@ class LibraryItemController {
height: height ? parseInt(height) : null,
width: width ? parseInt(width) : null
}
- return CacheManager.handleCoverCache(res, libraryItem.id, libraryItem.media.coverPath, options)
+ return CacheManager.handleCoverCache(res, libraryItemId, options)
}
/**
@@ -845,7 +841,13 @@ class LibraryItemController {
res.setHeader('Content-Type', audioMimeType)
}
- res.download(libraryFile.metadata.path, libraryFile.metadata.filename)
+ try {
+ await new Promise((resolve, reject) => res.download(libraryFile.metadata.path, libraryFile.metadata.filename, (error) => (error ? reject(error) : resolve())))
+ Logger.info(`[LibraryItemController] Downloaded file "${libraryFile.metadata.path}"`)
+ } catch (error) {
+ Logger.error(`[LibraryItemController] Failed to download file "${libraryFile.metadata.path}"`, error)
+ LibraryItemController.handleDownloadError(error, res)
+ }
}
/**
@@ -883,7 +885,13 @@ class LibraryItemController {
return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
}
- res.sendFile(ebookFilePath)
+ try {
+ await new Promise((resolve, reject) => res.sendFile(ebookFilePath, (error) => (error ? reject(error) : resolve())))
+ Logger.info(`[LibraryItemController] Downloaded ebook file "${ebookFilePath}"`)
+ } catch (error) {
+ Logger.error(`[LibraryItemController] Failed to download ebook file "${ebookFilePath}"`, error)
+ LibraryItemController.handleDownloadError(error, res)
+ }
}
/**
diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js
index c7abbc2327..cc67b320d7 100644
--- a/server/controllers/MeController.js
+++ b/server/controllers/MeController.js
@@ -394,6 +394,58 @@ class MeController {
res.json(req.user.toOldJSONForBrowser())
}
+ /**
+ * POST: /api/me/ereader-devices
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
+ async updateUserEReaderDevices(req, res) {
+ if (!req.body.ereaderDevices || !Array.isArray(req.body.ereaderDevices)) {
+ return res.status(400).send('Invalid payload. ereaderDevices array required')
+ }
+
+ const userEReaderDevices = req.body.ereaderDevices
+ for (const device of userEReaderDevices) {
+ if (!device.name || !device.email) {
+ return res.status(400).send('Invalid payload. ereaderDevices array items must have name and email')
+ } else if (device.availabilityOption !== 'specificUsers' || device.users?.length !== 1 || device.users[0] !== req.user.id) {
+ return res.status(400).send('Invalid payload. ereaderDevices array items must have availabilityOption "specificUsers" and only the current user')
+ }
+ }
+
+ const otherDevices = Database.emailSettings.ereaderDevices.filter((device) => {
+ return !Database.emailSettings.checkUserCanAccessDevice(device, req.user) || device.users?.length !== 1
+ })
+
+ const ereaderDevices = otherDevices.concat(userEReaderDevices)
+
+ // Check for duplicate names
+ const nameSet = new Set()
+ const hasDupes = ereaderDevices.some((device) => {
+ if (nameSet.has(device.name)) {
+ return true // Duplicate found
+ }
+ nameSet.add(device.name)
+ return false
+ })
+
+ if (hasDupes) {
+ return res.status(400).send('Invalid payload. Duplicate "name" field found.')
+ }
+
+ const updated = Database.emailSettings.update({ ereaderDevices })
+ if (updated) {
+ await Database.updateSetting(Database.emailSettings)
+ SocketAuthority.clientEmitter(req.user.id, 'ereader-devices-updated', {
+ ereaderDevices: Database.emailSettings.ereaderDevices
+ })
+ }
+ res.json({
+ ereaderDevices: Database.emailSettings.getEReaderDevices(req.user)
+ })
+ }
+
/**
* GET: /api/me/stats/year/:year
*
diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js
index f3dd0c6d40..cf901bea03 100644
--- a/server/controllers/MiscController.js
+++ b/server/controllers/MiscController.js
@@ -5,6 +5,7 @@ const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
+const Watcher = require('../Watcher')
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
const patternValidation = require('../libs/nodeCron/pattern-validation')
@@ -557,10 +558,10 @@ class MiscController {
switch (type) {
case 'add':
- this.watcher.onFileAdded(libraryId, path)
+ Watcher.onFileAdded(libraryId, path)
break
case 'unlink':
- this.watcher.onFileRemoved(libraryId, path)
+ Watcher.onFileRemoved(libraryId, path)
break
case 'rename':
const oldPath = req.body.oldPath
@@ -568,7 +569,7 @@ class MiscController {
Logger.error(`[MiscController] Invalid request body for updateWatchedPath. oldPath is required for rename.`)
return res.sendStatus(400)
}
- this.watcher.onFileRename(libraryId, oldPath, path)
+ Watcher.onFileRename(libraryId, oldPath, path)
break
default:
Logger.error(`[MiscController] Invalid type for updateWatchedPath. type: "${type}"`)
diff --git a/server/controllers/NotificationController.js b/server/controllers/NotificationController.js
index 215afe0ab1..b3fd72400f 100644
--- a/server/controllers/NotificationController.js
+++ b/server/controllers/NotificationController.js
@@ -1,6 +1,7 @@
const { Request, Response, NextFunction } = require('express')
const Database = require('../Database')
const { version } = require('../../package.json')
+const NotificationManager = require('../managers/NotificationManager')
/**
* @typedef RequestUserObject
@@ -23,7 +24,7 @@ class NotificationController {
*/
get(req, res) {
res.json({
- data: this.notificationManager.getData(),
+ data: NotificationManager.getData(),
settings: Database.notificationSettings
})
}
@@ -52,7 +53,7 @@ class NotificationController {
* @param {Response} res
*/
getData(req, res) {
- res.json(this.notificationManager.getData())
+ res.json(NotificationManager.getData())
}
/**
@@ -64,7 +65,7 @@ class NotificationController {
* @param {Response} res
*/
async fireTestEvent(req, res) {
- await this.notificationManager.triggerNotification('onTest', { version: `v${version}` }, req.query.fail === '1')
+ await NotificationManager.triggerNotification('onTest', { version: `v${version}` }, req.query.fail === '1')
res.sendStatus(200)
}
@@ -121,7 +122,7 @@ class NotificationController {
async sendNotificationTest(req, res) {
if (!Database.notificationSettings.isUseable) return res.status(400).send('Apprise is not configured')
- const success = await this.notificationManager.sendTestNotification(req.notification)
+ const success = await NotificationManager.sendTestNotification(req.notification)
if (success) res.sendStatus(200)
else res.sendStatus(500)
}
diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js
index 54727b6bba..102521b711 100644
--- a/server/managers/BackupManager.js
+++ b/server/managers/BackupManager.js
@@ -15,13 +15,12 @@ const { getFileSize } = require('../utils/fileUtils')
const Backup = require('../objects/Backup')
const CacheManager = require('./CacheManager')
+const NotificationManager = require('./NotificationManager')
class BackupManager {
- constructor(notificationManager) {
+ constructor() {
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
- /** @type {import('./NotificationManager')} */
- this.notificationManager = notificationManager
this.scheduleTask = null
@@ -301,7 +300,7 @@ class BackupManager {
const sqliteBackupPath = await this.backupSqliteDb(newBackup).catch((error) => {
Logger.error(`[BackupManager] Failed to backup sqlite db`, error)
const errorMsg = error?.message || error || 'Unknown Error'
- this.notificationManager.onBackupFailed(errorMsg)
+ NotificationManager.onBackupFailed(errorMsg)
return false
})
@@ -313,7 +312,7 @@ class BackupManager {
const zipResult = await this.zipBackup(sqliteBackupPath, newBackup).catch((error) => {
Logger.error(`[BackupManager] Backup Failed ${error}`)
const errorMsg = error?.message || error || 'Unknown Error'
- this.notificationManager.onBackupFailed(errorMsg)
+ NotificationManager.onBackupFailed(errorMsg)
return false
})
@@ -344,7 +343,7 @@ class BackupManager {
}
// Notification for backup successfully completed
- this.notificationManager.onBackupCompleted(newBackup, this.backups.length, removeOldest)
+ NotificationManager.onBackupCompleted(newBackup, this.backups.length, removeOldest)
return true
}
diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js
index 823a4c0e7d..f16d47c0b1 100644
--- a/server/managers/BinaryManager.js
+++ b/server/managers/BinaryManager.js
@@ -76,18 +76,27 @@ class ZippedAssetDownloader {
async extractFiles(zipPath, filesToExtract, destDir) {
const zip = new StreamZip.async({ file: zipPath })
- for (const file of filesToExtract) {
- const outputPath = path.join(destDir, file.outputFileName)
- await zip.extract(file.pathInsideZip, outputPath)
- Logger.debug(`[ZippedAssetDownloader] Extracted file ${file.pathInsideZip} to ${outputPath}`)
-
- // Set executable permission for Linux
- if (process.platform !== 'win32') {
- await fs.chmod(outputPath, 0o755)
+ try {
+ for (const file of filesToExtract) {
+ const outputPath = path.join(destDir, file.outputFileName)
+ if (!(await zip.entry(file.pathInsideZip))) {
+ Logger.error(`[ZippedAssetDownloader] File ${file.pathInsideZip} not found in zip file ${zipPath}`)
+ continue
+ }
+ await zip.extract(file.pathInsideZip, outputPath)
+ Logger.debug(`[ZippedAssetDownloader] Extracted file ${file.pathInsideZip} to ${outputPath}`)
+
+ // Set executable permission for Linux
+ if (process.platform !== 'win32') {
+ await fs.chmod(outputPath, 0o755)
+ }
}
+ } catch (error) {
+ Logger.error('[ZippedAssetDownloader] Error extracting files:', error)
+ throw error
+ } finally {
+ await zip.close()
}
-
- await zip.close()
}
async downloadAndExtractFiles(releaseTag, assetName, filesToExtract, destDir) {
@@ -99,7 +108,6 @@ class ZippedAssetDownloader {
await this.extractFiles(zipPath, filesToExtract, destDir)
} catch (error) {
Logger.error(`[ZippedAssetDownloader] Error downloading or extracting files: ${error.message}`)
- throw error
} finally {
if (zipPath) await fs.remove(zipPath)
}
@@ -164,14 +172,67 @@ class FFBinariesDownloader extends ZippedAssetDownloader {
}
}
+class NunicodeDownloader extends ZippedAssetDownloader {
+ constructor() {
+ super()
+ this.platformSuffix = this.getPlatformSuffix()
+ }
+
+ getPlatformSuffix() {
+ const platform = process.platform
+ const arch = process.arch
+
+ if (platform === 'win32' && arch === 'x64') {
+ return 'win-x64'
+ } else if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) {
+ return 'osx-arm64'
+ } else if (platform === 'linux' && arch === 'x64') {
+ return 'linux-x64'
+ } else if (platform === 'linux' && arch === 'arm64') {
+ return 'linux-arm64'
+ }
+
+ return null
+ }
+
+ async getAssetUrl(releaseTag, assetName) {
+ return `https://github.com/mikiher/nunicode-sqlite/releases/download/v${releaseTag}/${assetName}`
+ }
+
+ getAssetName(binaryName, releaseTag) {
+ if (!this.platformSuffix) {
+ throw new Error(`[NunicodeDownloader] Platform ${process.platform}-${process.arch} not supported`)
+ }
+ return `${binaryName}-${this.platformSuffix}.zip`
+ }
+
+ getAssetFileName(binaryName) {
+ if (process.platform === 'win32') {
+ return `${binaryName}.dll`
+ } else if (process.platform === 'darwin') {
+ return `${binaryName}.dylib`
+ } else if (process.platform === 'linux') {
+ return `${binaryName}.so`
+ }
+
+ throw new Error(`[NunicodeDownloader] Platform ${process.platform} not supported`)
+ }
+}
+
class Binary {
- constructor(name, type, envVariable, validVersions, source) {
+ constructor(name, type, envVariable, validVersions, source, required = true) {
+ if (!name) throw new Error('Binary name is required')
this.name = name
+ if (!type) throw new Error('Binary type is required')
this.type = type
+ if (!envVariable) throw new Error('Binary environment variable name is required')
this.envVariable = envVariable
+ if (!validVersions || !validVersions.length) throw new Error(`No valid versions specified for ${type} ${name}. At least one version is required.`)
this.validVersions = validVersions
+ if (!source || !(source instanceof ZippedAssetDownloader)) throw new Error('Binary source is required, and must be an instance of ZippedAssetDownloader')
this.source = source
this.fileName = this.getFileName()
+ this.required = required
this.exec = exec
}
@@ -205,37 +266,65 @@ class Binary {
}
}
- async isGood(binaryPath) {
- if (!binaryPath || !(await fs.pathExists(binaryPath))) return false
- if (!this.validVersions.length) return true
- if (this.type === 'library') return true
+ async isLibraryVersionValid(libraryPath) {
try {
- const { stdout } = await this.exec('"' + binaryPath + '"' + ' -version')
+ const versionFilePath = libraryPath + '.ver'
+ if (!(await fs.pathExists(versionFilePath))) return false
+ const version = (await fs.readFile(versionFilePath, 'utf8')).trim()
+ return this.validVersions.some((validVersion) => version.startsWith(validVersion))
+ } catch (err) {
+ Logger.error(`[Binary] Failed to check version of ${libraryPath}`, err)
+ return false
+ }
+ }
+
+ async isExecutableVersionValid(executablePath) {
+ try {
+ const { stdout } = await this.exec('"' + executablePath + '"' + ' -version')
const version = stdout.match(/version\s([\d\.]+)/)?.[1]
if (!version) return false
return this.validVersions.some((validVersion) => version.startsWith(validVersion))
} catch (err) {
- Logger.error(`[Binary] Failed to check version of ${binaryPath}`)
+ Logger.error(`[Binary] Failed to check version of ${executablePath}`, err)
+ return false
+ }
+ }
+
+ async isGood(binaryPath) {
+ try {
+ if (!binaryPath || !(await fs.pathExists(binaryPath))) return false
+ if (this.type === 'library') return await this.isLibraryVersionValid(binaryPath)
+ else if (this.type === 'executable') return await this.isExecutableVersionValid(binaryPath)
+ else return true
+ } catch (err) {
+ Logger.error(`[Binary] Failed to check ${this.type} ${this.name} at ${binaryPath}`, err)
return false
}
}
async download(destination) {
- await this.source.downloadBinary(this.name, this.validVersions[0], destination)
+ const version = this.validVersions[0]
+ try {
+ await this.source.downloadBinary(this.name, version, destination)
+ // if it's a library, write the version string to a file
+ if (this.type === 'library') {
+ const libraryPath = path.join(destination, this.fileName)
+ await fs.writeFile(libraryPath + '.ver', version)
+ }
+ } catch (err) {
+ Logger.error(`[Binary] Failed to download ${this.type} ${this.name} version ${version} to ${destination}`, err)
+ }
}
}
const ffbinaries = new FFBinariesDownloader()
-module.exports.ffbinaries = ffbinaries // for testing
-//const sqlean = new SQLeanDownloader()
-//module.exports.sqlean = sqlean // for testing
+const nunicode = new NunicodeDownloader()
class BinaryManager {
defaultRequiredBinaries = [
new Binary('ffmpeg', 'executable', 'FFMPEG_PATH', ['5.1'], ffbinaries), // ffmpeg executable
- new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries) // ffprobe executable
- // TODO: Temporarily disabled due to db corruption issues
- // new Binary('unicode', 'library', 'SQLEAN_UNICODE_PATH', ['0.24.2'], sqlean) // sqlean unicode extension
+ new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries), // ffprobe executable
+ new Binary('libnusqlite3', 'library', 'NUSQLITE3_PATH', ['1.2'], nunicode, false) // nunicode sqlite3 extension
]
constructor(requiredBinaries = this.defaultRequiredBinaries) {
@@ -249,7 +338,7 @@ class BinaryManager {
// Optional skip binaries check
if (process.env.SKIP_BINARIES_CHECK === '1') {
for (const binary of this.requiredBinaries) {
- if (!process.env[binary.envVariable]) {
+ if (!process.env[binary.envVariable] && binary.required) {
await Logger.fatal(`[BinaryManager] Environment variable ${binary.envVariable} must be set`)
process.exit(1)
}
@@ -265,21 +354,37 @@ class BinaryManager {
await this.removeOldBinaries(missingBinaries)
await this.install(missingBinaries)
const missingBinariesAfterInstall = await this.findRequiredBinaries()
- if (missingBinariesAfterInstall.length) {
- Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`)
+ const missingRequiredBinryNames = missingBinariesAfterInstall.filter((binary) => binary.required).map((binary) => binary.name)
+ if (missingRequiredBinryNames.length) {
+ Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingRequiredBinryNames.join(', ')}`)
process.exit(1)
}
this.initialized = true
}
+ /**
+ * Remove binary
+ *
+ * @param {string} destination
+ * @param {Binary} binary
+ */
async removeBinary(destination, binary) {
- const binaryPath = path.join(destination, binary.fileName)
- if (await fs.pathExists(binaryPath)) {
- Logger.debug(`[BinaryManager] Removing binary: ${binaryPath}`)
- await fs.remove(binaryPath)
+ try {
+ const binaryPath = path.join(destination, binary.fileName)
+ if (await fs.pathExists(binaryPath)) {
+ Logger.debug(`[BinaryManager] Removing binary: ${binaryPath}`)
+ await fs.remove(binaryPath)
+ }
+ } catch (err) {
+ Logger.error(`[BinaryManager] Error removing binary: ${binaryPath}`)
}
}
+ /**
+ * Remove old binaries
+ *
+ * @param {Binary[]} binaries
+ */
async removeOldBinaries(binaries) {
for (const binary of binaries) {
await this.removeBinary(this.mainInstallDir, binary)
@@ -290,26 +395,31 @@ class BinaryManager {
/**
* Find required binaries and return array of binary names that are missing
*
- * @returns {Promise}
+ * @returns {Promise} Array of missing binaries
*/
async findRequiredBinaries() {
const missingBinaries = []
for (const binary of this.requiredBinaries) {
const binaryPath = await binary.find(this.mainInstallDir, this.altInstallDir)
if (binaryPath) {
- Logger.info(`[BinaryManager] Found valid binary ${binary.name} at ${binaryPath}`)
+ Logger.info(`[BinaryManager] Found valid ${binary.type} ${binary.name} at ${binaryPath}`)
if (process.env[binary.envVariable] !== binaryPath) {
Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`)
process.env[binary.envVariable] = binaryPath
}
} else {
- Logger.info(`[BinaryManager] ${binary.name} not found or version too old`)
+ Logger.info(`[BinaryManager] ${binary.name} not found or not a valid version`)
missingBinaries.push(binary)
}
}
return missingBinaries
}
+ /**
+ * Install missing binaries
+ *
+ * @param {Binary[]} binaries
+ */
async install(binaries) {
if (!binaries.length) return
Logger.info(`[BinaryManager] Installing binaries: ${binaries.map((binary) => binary.name).join(', ')}`)
@@ -323,3 +433,5 @@ class BinaryManager {
module.exports = BinaryManager
module.exports.Binary = Binary // for testing
+module.exports.ffbinaries = ffbinaries // for testing
+module.exports.nunicode = nunicode // for testing
diff --git a/server/managers/CacheManager.js b/server/managers/CacheManager.js
index b4d2f270c2..f03756918a 100644
--- a/server/managers/CacheManager.js
+++ b/server/managers/CacheManager.js
@@ -4,6 +4,7 @@ const stream = require('stream')
const Logger = require('../Logger')
const { resizeImage } = require('../utils/ffmpegHelpers')
const { encodeUriPath } = require('../utils/fileUtils')
+const Database = require('../Database')
class CacheManager {
constructor() {
@@ -29,24 +30,24 @@ class CacheManager {
await fs.ensureDir(this.ItemCachePath)
}
- async handleCoverCache(res, libraryItemId, coverPath, options = {}) {
+ async handleCoverCache(res, libraryItemId, options = {}) {
const format = options.format || 'webp'
const width = options.width || 400
const height = options.height || null
res.type(`image/${format}`)
- const path = Path.join(this.CoverCachePath, `${libraryItemId}_${width}${height ? `x${height}` : ''}`) + '.' + format
+ const cachePath = Path.join(this.CoverCachePath, `${libraryItemId}_${width}${height ? `x${height}` : ''}`) + '.' + format
// Cache exists
- if (await fs.pathExists(path)) {
+ if (await fs.pathExists(cachePath)) {
if (global.XAccel) {
- const encodedURI = encodeUriPath(global.XAccel + path)
+ const encodedURI = encodeUriPath(global.XAccel + cachePath)
Logger.debug(`Use X-Accel to serve static file ${encodedURI}`)
return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
}
- const r = fs.createReadStream(path)
+ const r = fs.createReadStream(cachePath)
const ps = new stream.PassThrough()
stream.pipeline(r, ps, (err) => {
if (err) {
@@ -57,7 +58,13 @@ class CacheManager {
return ps.pipe(res)
}
- const writtenFile = await resizeImage(coverPath, path, width, height)
+ // Cached cover does not exist, generate it
+ const coverPath = await Database.libraryItemModel.getCoverPath(libraryItemId)
+ if (!coverPath || !(await fs.pathExists(coverPath))) {
+ return res.sendStatus(404)
+ }
+
+ const writtenFile = await resizeImage(coverPath, cachePath, width, height)
if (!writtenFile) return res.sendStatus(500)
if (global.XAccel) {
@@ -127,22 +134,22 @@ class CacheManager {
/**
*
* @param {import('express').Response} res
- * @param {import('../models/Author')} author
+ * @param {String} authorId
* @param {{ format?: string, width?: number, height?: number }} options
* @returns
*/
- async handleAuthorCache(res, author, options = {}) {
+ async handleAuthorCache(res, authorId, options = {}) {
const format = options.format || 'webp'
const width = options.width || 400
const height = options.height || null
res.type(`image/${format}`)
- var path = Path.join(this.ImageCachePath, `${author.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
+ var cachePath = Path.join(this.ImageCachePath, `${authorId}_${width}${height ? `x${height}` : ''}`) + '.' + format
// Cache exists
- if (await fs.pathExists(path)) {
- const r = fs.createReadStream(path)
+ if (await fs.pathExists(cachePath)) {
+ const r = fs.createReadStream(cachePath)
const ps = new stream.PassThrough()
stream.pipeline(r, ps, (err) => {
if (err) {
@@ -153,7 +160,12 @@ class CacheManager {
return ps.pipe(res)
}
- let writtenFile = await resizeImage(author.imagePath, path, width, height)
+ const author = await Database.authorModel.findByPk(authorId)
+ if (!author || !author.imagePath || !(await fs.pathExists(author.imagePath))) {
+ return res.sendStatus(404)
+ }
+
+ let writtenFile = await resizeImage(author.imagePath, cachePath, width, height)
if (!writtenFile) return res.sendStatus(500)
var readStream = fs.createReadStream(writtenFile)
diff --git a/server/managers/MigrationManager.js b/server/managers/MigrationManager.js
index 706e359cf1..003f8dfa11 100644
--- a/server/managers/MigrationManager.js
+++ b/server/managers/MigrationManager.js
@@ -38,6 +38,7 @@ 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)
this.serverVersion = this.extractVersionFromTag(serverVersion)
if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`)
@@ -129,7 +130,7 @@ class MigrationManager {
async initUmzug(umzugStorage = new SequelizeStorage({ sequelize: this.sequelize })) {
// This check is for dependency injection in tests
- const files = (await fs.readdir(this.migrationsDir)).map((file) => path.join(this.migrationsDir, file))
+ const files = (await fs.readdir(this.migrationsDir)).filter((file) => !file.startsWith('.')).map((file) => path.join(this.migrationsDir, file))
const parent = new Umzug({
migrations: {
@@ -190,7 +191,21 @@ class MigrationManager {
const queryInterface = this.sequelize.getQueryInterface()
let migrationsMetaTableExists = await queryInterface.tableExists(MigrationManager.MIGRATIONS_META_TABLE)
+ // If the table exists, check that the `version` and `maxVersion` rows exist
+ if (migrationsMetaTableExists) {
+ const [{ count }] = await this.sequelize.query("SELECT COUNT(*) as count FROM :migrationsMeta WHERE key IN ('version', 'maxVersion')", {
+ replacements: { migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
+ type: Sequelize.QueryTypes.SELECT
+ })
+ if (count < 2) {
+ Logger.warn(`[MigrationManager] migrationsMeta table exists but is missing 'version' or 'maxVersion' row. Dropping it...`)
+ await queryInterface.dropTable(MigrationManager.MIGRATIONS_META_TABLE)
+ migrationsMetaTableExists = false
+ }
+ }
+
if (this.isDatabaseNew && migrationsMetaTableExists) {
+ Logger.warn(`[MigrationManager] migrationsMeta table already exists. Dropping it...`)
// This can happen if database was initialized with force: true
await queryInterface.dropTable(MigrationManager.MIGRATIONS_META_TABLE)
migrationsMetaTableExists = false
@@ -222,8 +237,6 @@ class MigrationManager {
}
async copyMigrationsToConfigDir() {
- await fs.ensureDir(this.migrationsDir) // Ensure the target directory exists
-
if (!(await fs.pathExists(this.migrationsSourceDir))) return
const files = await fs.readdir(this.migrationsSourceDir)
diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js
index a59c128154..c48e878c2c 100644
--- a/server/managers/NotificationManager.js
+++ b/server/managers/NotificationManager.js
@@ -180,4 +180,4 @@ class NotificationManager {
})
}
}
-module.exports = NotificationManager
+module.exports = new NotificationManager()
diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js
index 4318841e1e..ce43fc8c41 100644
--- a/server/managers/PlaybackSessionManager.js
+++ b/server/managers/PlaybackSessionManager.js
@@ -119,6 +119,7 @@ class PlaybackSessionManager {
* @returns
*/
async syncLocalSession(user, sessionJson, deviceInfo) {
+ // TODO: Combine libraryItem query with library query
const libraryItem = await Database.libraryItemModel.getOldById(sessionJson.libraryItemId)
const episode = sessionJson.episodeId && libraryItem && libraryItem.isPodcast ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
if (!libraryItem || (libraryItem.isPodcast && !episode)) {
@@ -130,6 +131,16 @@ class PlaybackSessionManager {
}
}
+ const library = await Database.libraryModel.findByPk(libraryItem.libraryId)
+ if (!library) {
+ Logger.error(`[PlaybackSessionManager] syncLocalSession: Library not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`)
+ return {
+ id: sessionJson.id,
+ success: false,
+ error: 'Library not found'
+ }
+ }
+
sessionJson.userId = user.id
sessionJson.serverVersion = serverVersion
@@ -199,7 +210,9 @@ class PlaybackSessionManager {
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,
- ...session.mediaProgressObject
+ ...session.mediaProgressObject,
+ markAsFinishedPercentComplete: library.librarySettings.markAsFinishedPercentComplete,
+ markAsFinishedTimeRemaining: library.librarySettings.markAsFinishedTimeRemaining
})
result.progressSynced = !!updateResponse.mediaProgress
if (result.progressSynced) {
@@ -211,7 +224,9 @@ class PlaybackSessionManager {
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,
- ...session.mediaProgressObject
+ ...session.mediaProgressObject,
+ markAsFinishedPercentComplete: library.librarySettings.markAsFinishedPercentComplete,
+ markAsFinishedTimeRemaining: library.librarySettings.markAsFinishedTimeRemaining
})
result.progressSynced = !!updateResponse.mediaProgress
if (result.progressSynced) {
@@ -330,12 +345,19 @@ class PlaybackSessionManager {
* @returns
*/
async syncSession(user, session, syncData) {
+ // TODO: Combine libraryItem query with library query
const libraryItem = await Database.libraryItemModel.getOldById(session.libraryItemId)
if (!libraryItem) {
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
return null
}
+ const library = await Database.libraryModel.findByPk(libraryItem.libraryId)
+ if (!library) {
+ Logger.error(`[PlaybackSessionManager] syncSession Library not found "${libraryItem.libraryId}"`)
+ return null
+ }
+
session.currentTime = syncData.currentTime
session.addListeningTime(syncData.timeListened)
Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" (Device: ${session.deviceDescription}) | Total Time Listened: ${session.timeListening}`)
@@ -343,9 +365,12 @@ class PlaybackSessionManager {
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,
- duration: syncData.duration,
+ // duration no longer required (v2.15.1) but used if available
+ duration: syncData.duration || session.duration || 0,
currentTime: syncData.currentTime,
- progress: session.progress
+ progress: session.progress,
+ markAsFinishedTimeRemaining: library.librarySettings.markAsFinishedTimeRemaining,
+ markAsFinishedPercentComplete: library.librarySettings.markAsFinishedPercentComplete
})
if (updateResponse.mediaProgress) {
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index 4e6c3fb839..0a32e3cad1 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -1,6 +1,7 @@
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
+const Watcher = require('../Watcher')
const fs = require('../libs/fsExtra')
@@ -14,6 +15,7 @@ const ffmpegHelpers = require('../utils/ffmpegHelpers')
const TaskManager = require('./TaskManager')
const CoverManager = require('../managers/CoverManager')
+const NotificationManager = require('../managers/NotificationManager')
const LibraryFile = require('../objects/files/LibraryFile')
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
@@ -22,10 +24,7 @@ const AudioFile = require('../objects/files/AudioFile')
const LibraryItem = require('../objects/LibraryItem')
class PodcastManager {
- constructor(watcher, notificationManager) {
- this.watcher = watcher
- this.notificationManager = notificationManager
-
+ constructor() {
this.downloadQueue = []
this.currentDownload = null
@@ -47,6 +46,7 @@ class PodcastManager {
var itemDownloads = this.getEpisodeDownloadsInQueue(libraryItemId)
Logger.info(`[PodcastManager] Clearing downloads in queue for item "${libraryItemId}" (${itemDownloads.length})`)
this.downloadQueue = this.downloadQueue.filter((d) => d.libraryItemId !== libraryItemId)
+ SocketAuthority.emitter('episode_download_queue_cleared', libraryItemId)
}
}
@@ -64,7 +64,6 @@ class PodcastManager {
}
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
- SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails())
if (this.currentDownload) {
this.downloadQueue.push(podcastEpisodeDownload)
SocketAuthority.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient())
@@ -97,7 +96,8 @@ class PodcastManager {
}
// Ignores all added files to this dir
- this.watcher.addIgnoreDir(this.currentDownload.libraryItem.path)
+ Watcher.addIgnoreDir(this.currentDownload.libraryItem.path)
+ Watcher.ignoreFilePathsDownloading.add(this.currentDownload.targetPath)
// Make sure podcast library item folder exists
if (!(await fs.pathExists(this.currentDownload.libraryItem.path))) {
@@ -149,9 +149,10 @@ class PodcastManager {
TaskManager.taskFinished(task)
SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
- SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails())
- this.watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
+ Watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
+
+ Watcher.ignoreFilePathsDownloading.delete(this.currentDownload.targetPath)
this.currentDownload = null
if (this.downloadQueue.length) {
this.startPodcastEpisodeDownload(this.downloadQueue.shift())
@@ -203,7 +204,7 @@ class PodcastManager {
if (this.currentDownload.isAutoDownload) {
// Notifications only for auto downloaded episodes
- this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
+ NotificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
}
return true
diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md
index 2e3c295af1..8960ade2f7 100644
--- a/server/migrations/changelog.md
+++ b/server/migrations/changelog.md
@@ -2,6 +2,9 @@
Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time.
-| Server Version | Migration Script Name | Description |
-| -------------- | --------------------- | ----------- |
-| | | |
+| Server Version | Migration Script Name | Description |
+| -------------- | ---------------------------- | ------------------------------------------------------------------------------------ |
+| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library |
+| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 |
+| v2.15.2 | v2.15.2-index-creation | Creates author, series, and podcast episode indexes |
+| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model |
diff --git a/server/migrations/v2.15.0-series-column-unique.js b/server/migrations/v2.15.0-series-column-unique.js
new file mode 100644
index 0000000000..7f8526f97c
--- /dev/null
+++ b/server/migrations/v2.15.0-series-column-unique.js
@@ -0,0 +1,210 @@
+/**
+ * @typedef MigrationContext
+ * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
+ * @property {import('../Logger')} logger - a Logger object.
+ *
+ * @typedef MigrationOptions
+ * @property {MigrationContext} context - an object containing the migration context.
+ */
+
+/**
+ * This upward migration script cleans any duplicate series in the `Series` table and
+ * adds a unique index on the `name` and `libraryId` columns.
+ *
+ * @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 } }) {
+ // Upwards migration script
+ logger.info('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique ')
+
+ // Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
+ logger.info('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues')
+ await queryInterface.sequelize.query('REINDEX NOCASE;')
+
+ // Check if the unique index already exists
+ const seriesIndexes = await queryInterface.showIndex('Series')
+ if (seriesIndexes.some((index) => index.name === 'unique_series_name_per_library')) {
+ logger.info('[2.15.0 migration] Unique index on Series.name and Series.libraryId already exists')
+ logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
+ return
+ }
+
+ // The steps taken to deduplicate the series are as follows:
+ // 1. Find all duplicate series in the `Series` table.
+ // 2. Iterate over the duplicate series and find all book IDs that are associated with the duplicate series in `bookSeries` table.
+ // 2.a For each book ID, check if the ID occurs multiple times for the duplicate series.
+ // 2.b If so, keep only one of the rows that has this bookId and seriesId.
+ // 3. Update `bookSeries` table to point to the most recent series.
+ // 4. Delete the older series.
+
+ // Use the queryInterface to get the series table and find duplicates in the `name` and `libraryId` column
+ const [duplicates] = await queryInterface.sequelize.query(`
+ SELECT name, libraryId
+ FROM Series
+ GROUP BY name, libraryId
+ HAVING COUNT(name) > 1
+ `)
+
+ // Print out how many duplicates were found
+ logger.info(`[2.15.0 migration] Found ${duplicates.length} duplicate series`)
+
+ // Iterate over each duplicate series
+ for (const duplicate of duplicates) {
+ // Report the series name that is being deleted
+ logger.info(`[2.15.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`)
+
+ // Determine any duplicate book IDs in the `bookSeries` table for the same series
+ const [duplicateBookIds] = await queryInterface.sequelize.query(
+ `
+ SELECT bookId
+ FROM BookSeries
+ WHERE seriesId IN (
+ SELECT id
+ FROM Series
+ WHERE name = :name AND libraryId = :libraryId
+ )
+ GROUP BY bookId
+ HAVING COUNT(bookId) > 1
+ `,
+ {
+ replacements: {
+ name: duplicate.name,
+ libraryId: duplicate.libraryId
+ }
+ }
+ )
+
+ // Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId
+ for (const { bookId } of duplicateBookIds) {
+ logger.info(`[2.15.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
+ // Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last
+ const [duplicateBookSeries] = await queryInterface.sequelize.query(
+ `
+ SELECT id
+ FROM BookSeries
+ WHERE bookId = :bookId
+ AND seriesId IN (
+ SELECT id
+ FROM Series
+ WHERE name = :name AND libraryId = :libraryId
+ )
+ ORDER BY sequence NULLS LAST
+ `,
+ {
+ replacements: {
+ bookId,
+ name: duplicate.name,
+ libraryId: duplicate.libraryId
+ }
+ }
+ )
+
+ // remove the first element from the array
+ duplicateBookSeries.shift()
+
+ // Delete the remaining duplicate rows
+ if (duplicateBookSeries.length > 0) {
+ const [deletedBookSeries] = await queryInterface.sequelize.query(
+ `
+ DELETE FROM BookSeries
+ WHERE id IN (:ids)
+ `,
+ {
+ replacements: {
+ ids: duplicateBookSeries.map((row) => row.id)
+ }
+ }
+ )
+ }
+ logger.info(`[2.15.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
+ }
+
+ // Get all the most recent series which matches the `name` and `libraryId`
+ const [mostRecentSeries] = await queryInterface.sequelize.query(
+ `
+ SELECT id
+ FROM Series
+ WHERE name = :name AND libraryId = :libraryId
+ ORDER BY updatedAt DESC
+ LIMIT 1
+ `,
+ {
+ replacements: {
+ name: duplicate.name,
+ libraryId: duplicate.libraryId
+ },
+ type: queryInterface.sequelize.QueryTypes.SELECT
+ }
+ )
+
+ if (mostRecentSeries) {
+ // Update all BookSeries records for this series to point to the most recent series
+ const [seriesUpdated] = await queryInterface.sequelize.query(
+ `
+ UPDATE BookSeries
+ SET seriesId = :mostRecentSeriesId
+ WHERE seriesId IN (
+ SELECT id
+ FROM Series
+ WHERE name = :name AND libraryId = :libraryId
+ AND id != :mostRecentSeriesId
+ )
+ `,
+ {
+ replacements: {
+ name: duplicate.name,
+ libraryId: duplicate.libraryId,
+ mostRecentSeriesId: mostRecentSeries.id
+ }
+ }
+ )
+
+ // Delete the older series
+ const seriesDeleted = await queryInterface.sequelize.query(
+ `
+ DELETE FROM Series
+ WHERE name = :name AND libraryId = :libraryId
+ AND id != :mostRecentSeriesId
+ `,
+ {
+ replacements: {
+ name: duplicate.name,
+ libraryId: duplicate.libraryId,
+ mostRecentSeriesId: mostRecentSeries.id
+ }
+ }
+ )
+ }
+ }
+
+ logger.info(`[2.15.0 migration] Deduplication complete`)
+
+ // Create a unique index based on the name and library ID for the `Series` table
+ await queryInterface.addIndex('Series', ['name', 'libraryId'], {
+ unique: true,
+ name: 'unique_series_name_per_library'
+ })
+ logger.info('[2.15.0 migration] Added unique index on Series.name and Series.libraryId')
+
+ logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
+}
+
+/**
+ * This removes the unique index on the `Series` 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 } }) {
+ // Downward migration script
+ logger.info('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique ')
+
+ // Remove the unique index
+ await queryInterface.removeIndex('Series', 'unique_series_name_per_library')
+ logger.info('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId')
+
+ logger.info('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique ')
+}
+
+module.exports = { up, down }
diff --git a/server/migrations/v2.15.1-reindex-nocase.js b/server/migrations/v2.15.1-reindex-nocase.js
new file mode 100644
index 0000000000..2ec9487d26
--- /dev/null
+++ b/server/migrations/v2.15.1-reindex-nocase.js
@@ -0,0 +1,43 @@
+/**
+ * @typedef MigrationContext
+ * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
+ * @property {import('../Logger')} logger - a Logger object.
+ *
+ * @typedef MigrationOptions
+ * @property {MigrationContext} context - an object containing the migration context.
+ */
+
+/**
+ * This upward migration script fixes old database corruptions due to the a bad sqlite extension introduced in v2.12.0.
+ *
+ * @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 } }) {
+ // Upwards migration script
+ logger.info('[2.15.1 migration] UPGRADE BEGIN: 2.15.1-reindex-nocase ')
+
+ // Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
+ logger.info('[2.15.1 migration] Reindexing NOCASE indices to fix potential hidden corruption issues')
+ await queryInterface.sequelize.query('REINDEX NOCASE;')
+
+ logger.info('[2.15.1 migration] UPGRADE END: 2.15.1-reindex-nocase ')
+}
+
+/**
+ * This downward migration script is a no-op.
+ *
+ * @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 } }) {
+ // Downward migration script
+ logger.info('[2.15.1 migration] DOWNGRADE BEGIN: 2.15.1-reindex-nocase ')
+
+ // This migration is a no-op
+ logger.info('[2.15.1 migration] No action required for downgrade')
+
+ logger.info('[2.15.1 migration] DOWNGRADE END: 2.15.1-reindex-nocase ')
+}
+
+module.exports = { up, down }
diff --git a/server/migrations/v2.15.2-index-creation.js b/server/migrations/v2.15.2-index-creation.js
new file mode 100644
index 0000000000..f1302dd26b
--- /dev/null
+++ b/server/migrations/v2.15.2-index-creation.js
@@ -0,0 +1,93 @@
+/**
+ * @typedef MigrationContext
+ * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
+ * @property {import('../Logger')} logger - a Logger object.
+ *
+ * @typedef MigrationOptions
+ * @property {MigrationContext} context - an object containing the migration context.
+ */
+
+/**
+ * This upward migration script adds indexes to speed up queries on the `BookAuthor`, `BookSeries`, and `podcastEpisodes` tables.
+ *
+ * @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 } }) {
+ // Upwards migration script
+ logger.info('[2.15.2 migration] UPGRADE BEGIN: 2.15.2-index-creation')
+
+ // Create index for bookAuthors
+ logger.info('[2.15.2 migration] Creating index for bookAuthors')
+ const bookAuthorsIndexes = await queryInterface.showIndex('bookAuthors')
+ if (!bookAuthorsIndexes.some((index) => index.name === 'bookAuthor_authorId')) {
+ await queryInterface.addIndex('bookAuthors', ['authorId'], {
+ name: 'bookAuthor_authorId'
+ })
+ } else {
+ logger.info('[2.15.2 migration] Index bookAuthor_authorId already exists')
+ }
+
+ // Create index for bookSeries
+ logger.info('[2.15.2 migration] Creating index for bookSeries')
+ const bookSeriesIndexes = await queryInterface.showIndex('bookSeries')
+ if (!bookSeriesIndexes.some((index) => index.name === 'bookSeries_seriesId')) {
+ await queryInterface.addIndex('bookSeries', ['seriesId'], {
+ name: 'bookSeries_seriesId'
+ })
+ } else {
+ logger.info('[2.15.2 migration] Index bookSeries_seriesId already exists')
+ }
+
+ // Delete existing podcastEpisode index
+ logger.info('[2.15.2 migration] Deleting existing podcastEpisode index')
+ await queryInterface.removeIndex('podcastEpisodes', 'podcast_episodes_created_at')
+
+ // Create index for podcastEpisode and createdAt
+ logger.info('[2.15.2 migration] Creating index for podcastEpisode and createdAt')
+ const podcastEpisodesIndexes = await queryInterface.showIndex('podcastEpisodes')
+ if (!podcastEpisodesIndexes.some((index) => index.name === 'podcastEpisode_createdAt_podcastId')) {
+ await queryInterface.addIndex('podcastEpisodes', ['createdAt', 'podcastId'], {
+ name: 'podcastEpisode_createdAt_podcastId'
+ })
+ } else {
+ logger.info('[2.15.2 migration] Index podcastEpisode_createdAt_podcastId already exists')
+ }
+
+ // Completed migration
+ logger.info('[2.15.2 migration] UPGRADE END: 2.15.2-index-creation')
+}
+
+/**
+ * This downward migration script removes the newly created indexes and re-adds the old index on the `podcastEpisodes` 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 } }) {
+ // Downward migration script
+ logger.info('[2.15.2 migration] DOWNGRADE BEGIN: 2.15.2-index-creation')
+
+ // Remove index for bookAuthors
+ logger.info('[2.15.2 migration] Removing index for bookAuthors')
+ await queryInterface.removeIndex('bookAuthors', 'bookAuthor_authorId')
+
+ // Remove index for bookSeries
+ logger.info('[2.15.2 migration] Removing index for bookSeries')
+ await queryInterface.removeIndex('bookSeries', 'bookSeries_seriesId')
+
+ // Delete existing podcastEpisode index
+ logger.info('[2.15.2 migration] Deleting existing podcastEpisode index')
+ await queryInterface.removeIndex('podcastEpisodes', 'podcastEpisode_createdAt_podcastId')
+
+ // Create index for podcastEpisode and createdAt
+ logger.info('[2.15.2 migration] Creating original index for podcastEpisode createdAt')
+ await queryInterface.addIndex('podcastEpisodes', ['createdAt'], {
+ name: 'podcast_episodes_created_at'
+ })
+
+ // Finished migration
+ logger.info('[2.15.2 migration] DOWNGRADE END: 2.15.2-index-creation')
+}
+
+module.exports = { up, down }
diff --git a/server/migrations/v2.17.0-uuid-replacement.js b/server/migrations/v2.17.0-uuid-replacement.js
new file mode 100644
index 0000000000..4316cd7694
--- /dev/null
+++ b/server/migrations/v2.17.0-uuid-replacement.js
@@ -0,0 +1,102 @@
+/**
+ * @typedef MigrationContext
+ * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
+ * @property {import('../Logger')} logger - a Logger object.
+ *
+ * @typedef MigrationOptions
+ * @property {MigrationContext} context - an object containing the migration context.
+ */
+
+/**
+ * This upward migration script changes table columns with data type UUIDv4 to UUID to match associated models.
+ *
+ * @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 } }) {
+ // Upwards migration script
+ logger.info('[2.17.0 migration] UPGRADE BEGIN: 2.17.0-uuid-replacement')
+
+ logger.info('[2.17.0 migration] Changing libraryItems.mediaId column to UUID')
+ await queryInterface.changeColumn('libraryItems', 'mediaId', {
+ type: 'UUID'
+ })
+
+ logger.info('[2.17.0 migration] Changing feeds.entityId column to UUID')
+ await queryInterface.changeColumn('feeds', 'entityId', {
+ type: 'UUID'
+ })
+
+ if (await queryInterface.tableExists('mediaItemShares')) {
+ logger.info('[2.17.0 migration] Changing mediaItemShares.mediaItemId column to UUID')
+ await queryInterface.changeColumn('mediaItemShares', 'mediaItemId', {
+ type: 'UUID'
+ })
+ } else {
+ logger.info('[2.17.0 migration] mediaItemShares table does not exist, skipping column change')
+ }
+
+ logger.info('[2.17.0 migration] Changing playbackSessions.mediaItemId column to UUID')
+ await queryInterface.changeColumn('playbackSessions', 'mediaItemId', {
+ type: 'UUID'
+ })
+
+ logger.info('[2.17.0 migration] Changing playlistMediaItems.mediaItemId column to UUID')
+ await queryInterface.changeColumn('playlistMediaItems', 'mediaItemId', {
+ type: 'UUID'
+ })
+
+ logger.info('[2.17.0 migration] Changing mediaProgresses.mediaItemId column to UUID')
+ await queryInterface.changeColumn('mediaProgresses', 'mediaItemId', {
+ type: 'UUID'
+ })
+
+ // Completed migration
+ logger.info('[2.17.0 migration] UPGRADE END: 2.17.0-uuid-replacement')
+}
+
+/**
+ * This downward migration script changes table columns data type back to UUIDv4.
+ *
+ * @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 } }) {
+ // Downward migration script
+ logger.info('[2.17.0 migration] DOWNGRADE BEGIN: 2.17.0-uuid-replacement')
+
+ logger.info('[2.17.0 migration] Changing libraryItems.mediaId column to UUIDV4')
+ await queryInterface.changeColumn('libraryItems', 'mediaId', {
+ type: 'UUIDV4'
+ })
+
+ logger.info('[2.17.0 migration] Changing feeds.entityId column to UUIDV4')
+ await queryInterface.changeColumn('feeds', 'entityId', {
+ type: 'UUIDV4'
+ })
+
+ logger.info('[2.17.0 migration] Changing mediaItemShares.mediaItemId column to UUIDV4')
+ await queryInterface.changeColumn('mediaItemShares', 'mediaItemId', {
+ type: 'UUIDV4'
+ })
+
+ logger.info('[2.17.0 migration] Changing playbackSessions.mediaItemId column to UUIDV4')
+ await queryInterface.changeColumn('playbackSessions', 'mediaItemId', {
+ type: 'UUIDV4'
+ })
+
+ logger.info('[2.17.0 migration] Changing playlistMediaItems.mediaItemId column to UUIDV4')
+ await queryInterface.changeColumn('playlistMediaItems', 'mediaItemId', {
+ type: 'UUIDV4'
+ })
+
+ logger.info('[2.17.0 migration] Changing mediaProgresses.mediaItemId column to UUIDV4')
+ await queryInterface.changeColumn('mediaProgresses', 'mediaItemId', {
+ type: 'UUIDV4'
+ })
+
+ // Completed migration
+ logger.info('[2.17.0 migration] DOWNGRADE END: 2.17.0-uuid-replacement')
+}
+
+module.exports = { up, down }
diff --git a/server/models/Author.js b/server/models/Author.js
index 40e7f75a47..f3bbba5740 100644
--- a/server/models/Author.js
+++ b/server/models/Author.js
@@ -1,6 +1,5 @@
const { DataTypes, Model, where, fn, col } = require('sequelize')
const parseNameString = require('../utils/parsers/parseNameString')
-const { asciiOnlyToLowerCase } = require('../utils/index')
class Author extends Model {
constructor(values, options) {
@@ -56,7 +55,7 @@ class Author extends Model {
static async getByNameAndLibrary(authorName, libraryId) {
return this.findOne({
where: [
- where(fn('lower', col('name')), asciiOnlyToLowerCase(authorName)),
+ where(fn('lower', col('name')), authorName.toLowerCase()),
{
libraryId
}
diff --git a/server/models/BookAuthor.js b/server/models/BookAuthor.js
index 8e09367161..d7d65728e2 100644
--- a/server/models/BookAuthor.js
+++ b/server/models/BookAuthor.js
@@ -54,7 +54,13 @@ class BookAuthor extends Model {
sequelize,
modelName: 'bookAuthor',
timestamps: true,
- updatedAt: false
+ updatedAt: false,
+ indexes: [
+ {
+ name: 'bookAuthor_authorId',
+ fields: ['authorId']
+ }
+ ]
}
)
diff --git a/server/models/BookSeries.js b/server/models/BookSeries.js
index fad547181c..31eccb9fa3 100644
--- a/server/models/BookSeries.js
+++ b/server/models/BookSeries.js
@@ -43,7 +43,13 @@ class BookSeries extends Model {
sequelize,
modelName: 'bookSeries',
timestamps: true,
- updatedAt: false
+ updatedAt: false,
+ indexes: [
+ {
+ name: 'bookSeries_seriesId',
+ fields: ['seriesId']
+ }
+ ]
}
)
diff --git a/server/models/Feed.js b/server/models/Feed.js
index 72321da926..4f51e66d9e 100644
--- a/server/models/Feed.js
+++ b/server/models/Feed.js
@@ -274,7 +274,7 @@ class Feed extends Model {
},
slug: DataTypes.STRING,
entityType: DataTypes.STRING,
- entityId: DataTypes.UUIDV4,
+ entityId: DataTypes.UUID,
entityUpdatedAt: DataTypes.DATE,
serverAddress: DataTypes.STRING,
feedURL: DataTypes.STRING,
diff --git a/server/models/Library.js b/server/models/Library.js
index 90dd2512ee..708880aad3 100644
--- a/server/models/Library.js
+++ b/server/models/Library.js
@@ -12,6 +12,8 @@ const Logger = require('../Logger')
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
* @property {boolean} onlyShowLaterBooksInContinueSeries Skip showing books that are earlier than the max sequence read
* @property {string[]} metadataPrecedence
+ * @property {number} markAsFinishedTimeRemaining Time remaining in seconds to mark as finished. (defaults to 10s)
+ * @property {number} markAsFinishedPercentComplete Percent complete to mark as finished (0-100). If this is set it will be used over markAsFinishedTimeRemaining.
*/
class Library extends Model {
@@ -57,7 +59,9 @@ class Library extends Model {
coverAspectRatio: 1, // Square
disableWatcher: false,
autoScanCronExpression: null,
- podcastSearchRegion: 'us'
+ podcastSearchRegion: 'us',
+ markAsFinishedPercentComplete: null,
+ markAsFinishedTimeRemaining: 10
}
} else {
return {
@@ -70,7 +74,9 @@ class Library extends Model {
epubsAllowScriptedContent: false,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
- metadataPrecedence: this.defaultMetadataPrecedence
+ metadataPrecedence: this.defaultMetadataPrecedence,
+ markAsFinishedPercentComplete: null,
+ markAsFinishedTimeRemaining: 10
}
}
}
@@ -101,19 +107,6 @@ class Library extends Model {
})
}
- /**
- * Destroy library by id
- * @param {string} libraryId
- * @returns
- */
- static removeById(libraryId) {
- return this.destroy({
- where: {
- id: libraryId
- }
- })
- }
-
/**
* Get all library ids
* @returns {Promise} array of library ids
@@ -196,6 +189,13 @@ class Library extends Model {
return this.extraData?.lastScanMetadataPrecedence || []
}
+ /**
+ * @returns {LibrarySettingsObject}
+ */
+ get librarySettings() {
+ return this.settings || Library.getDefaultLibrarySettingsForMediaType(this.mediaType)
+ }
+
/**
* TODO: Update to use new model
*/
diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js
index dd07747a91..10395c49c2 100644
--- a/server/models/LibraryItem.js
+++ b/server/models/LibraryItem.js
@@ -237,35 +237,7 @@ class LibraryItem extends Model {
* @returns {Promise} true if updates were made
*/
static async fullUpdateFromOld(oldLibraryItem) {
- const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
- include: [
- {
- model: this.sequelize.models.book,
- include: [
- {
- model: this.sequelize.models.author,
- through: {
- attributes: []
- }
- },
- {
- model: this.sequelize.models.series,
- through: {
- attributes: ['id', 'sequence']
- }
- }
- ]
- },
- {
- model: this.sequelize.models.podcast,
- include: [
- {
- model: this.sequelize.models.podcastEpisode
- }
- ]
- }
- ]
- })
+ const libraryItemExpanded = await this.getExpandedById(oldLibraryItem.id)
if (!libraryItemExpanded) return false
let hasUpdates = false
@@ -507,7 +479,7 @@ class LibraryItem extends Model {
{
model: this.sequelize.models.series,
through: {
- attributes: ['sequence']
+ attributes: ['id', 'sequence']
}
}
],
@@ -863,6 +835,33 @@ class LibraryItem extends Model {
return this.getOldLibraryItem(libraryItem)
}
+ /**
+ *
+ * @param {string} libraryItemId
+ * @returns {Promise}
+ */
+ static async getCoverPath(libraryItemId) {
+ const libraryItem = await this.findByPk(libraryItemId, {
+ attributes: ['id', 'mediaType', 'mediaId', 'libraryId'],
+ include: [
+ {
+ model: this.sequelize.models.book,
+ attributes: ['id', 'coverPath']
+ },
+ {
+ model: this.sequelize.models.podcast,
+ attributes: ['id', 'coverPath']
+ }
+ ]
+ })
+ if (!libraryItem) {
+ Logger.warn(`[LibraryItem] getCoverPath: Library item "${libraryItemId}" does not exist`)
+ return null
+ }
+
+ return libraryItem.media.coverPath
+ }
+
/**
*
* @param {import('sequelize').FindOptions} options
@@ -1032,7 +1031,7 @@ class LibraryItem extends Model {
ino: DataTypes.STRING,
path: DataTypes.STRING,
relPath: DataTypes.STRING,
- mediaId: DataTypes.UUIDV4,
+ mediaId: DataTypes.UUID,
mediaType: DataTypes.STRING,
isFile: DataTypes.BOOLEAN,
isMissing: DataTypes.BOOLEAN,
diff --git a/server/models/MediaItemShare.js b/server/models/MediaItemShare.js
index ffdc3ddd1f..38b8dbbf4b 100644
--- a/server/models/MediaItemShare.js
+++ b/server/models/MediaItemShare.js
@@ -109,7 +109,7 @@ class MediaItemShare extends Model {
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
- mediaItemId: DataTypes.UUIDV4,
+ mediaItemId: DataTypes.UUID,
mediaItemType: DataTypes.STRING,
slug: DataTypes.STRING,
pash: DataTypes.STRING,
diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js
index 196353d8eb..80204ef5cc 100644
--- a/server/models/MediaProgress.js
+++ b/server/models/MediaProgress.js
@@ -1,4 +1,6 @@
const { DataTypes, Model } = require('sequelize')
+const Logger = require('../Logger')
+const { isNullOrNaN } = require('../utils')
class MediaProgress extends Model {
constructor(values, options) {
@@ -91,7 +93,7 @@ class MediaProgress extends Model {
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
- mediaItemId: DataTypes.UUIDV4,
+ mediaItemId: DataTypes.UUID,
mediaItemType: DataTypes.STRING,
duration: DataTypes.FLOAT,
currentTime: DataTypes.FLOAT,
@@ -183,10 +185,16 @@ class MediaProgress extends Model {
}
}
+ get progress() {
+ // Value between 0 and 1
+ if (!this.duration) return 0
+ return Math.max(0, Math.min(this.currentTime / this.duration, 1))
+ }
+
/**
* Apply update to media progress
*
- * @param {Object} progress
+ * @param {import('./User').ProgressUpdatePayload} progressPayload
* @returns {Promise}
*/
applyProgressUpdate(progressPayload) {
@@ -219,13 +227,32 @@ class MediaProgress extends Model {
}
const timeRemaining = this.duration - this.currentTime
- // Set to finished if time remaining is less than 5 seconds
- if (!this.isFinished && this.duration && timeRemaining < 5) {
+
+ // Check if progress is far enough to mark as finished
+ // - If markAsFinishedPercentComplete is provided, use that otherwise use markAsFinishedTimeRemaining (default 10 seconds)
+ let shouldMarkAsFinished = false
+ if (this.duration) {
+ if (!isNullOrNaN(progressPayload.markAsFinishedPercentComplete) && progressPayload.markAsFinishedPercentComplete > 0) {
+ const markAsFinishedPercentComplete = Number(progressPayload.markAsFinishedPercentComplete) / 100
+ shouldMarkAsFinished = markAsFinishedPercentComplete < this.progress
+ if (shouldMarkAsFinished) {
+ Logger.debug(`[MediaProgress] Marking media progress as finished because progress (${this.progress}) is greater than ${markAsFinishedPercentComplete}`)
+ }
+ } else {
+ const markAsFinishedTimeRemaining = isNullOrNaN(progressPayload.markAsFinishedTimeRemaining) ? 10 : Number(progressPayload.markAsFinishedTimeRemaining)
+ shouldMarkAsFinished = timeRemaining < markAsFinishedTimeRemaining
+ if (shouldMarkAsFinished) {
+ Logger.debug(`[MediaProgress] Marking media progress as finished because time remaining (${timeRemaining}) is less than ${markAsFinishedTimeRemaining} seconds`)
+ }
+ }
+ }
+
+ if (!this.isFinished && shouldMarkAsFinished) {
this.isFinished = true
this.finishedAt = this.finishedAt || Date.now()
this.extraData.progress = 1
this.changed('extraData', true)
- } else if (this.isFinished && this.changed('currentTime') && this.currentTime < this.duration) {
+ } else if (this.isFinished && this.changed('currentTime') && !shouldMarkAsFinished) {
this.isFinished = false
this.finishedAt = null
}
diff --git a/server/models/PlaybackSession.js b/server/models/PlaybackSession.js
index c7c6323af6..196fbda6ce 100644
--- a/server/models/PlaybackSession.js
+++ b/server/models/PlaybackSession.js
@@ -179,7 +179,7 @@ class PlaybackSession extends Model {
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
- mediaItemId: DataTypes.UUIDV4,
+ mediaItemId: DataTypes.UUID,
mediaItemType: DataTypes.STRING,
displayTitle: DataTypes.STRING,
displayAuthor: DataTypes.STRING,
diff --git a/server/models/PlaylistMediaItem.js b/server/models/PlaylistMediaItem.js
index 25e7b8c553..1c53bea115 100644
--- a/server/models/PlaylistMediaItem.js
+++ b/server/models/PlaylistMediaItem.js
@@ -45,7 +45,7 @@ class PlaylistMediaItem extends Model {
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
- mediaItemId: DataTypes.UUIDV4,
+ mediaItemId: DataTypes.UUID,
mediaItemType: DataTypes.STRING,
order: DataTypes.INTEGER
},
diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js
index 1707fbd5f1..1f99361ab7 100644
--- a/server/models/PodcastEpisode.js
+++ b/server/models/PodcastEpisode.js
@@ -157,7 +157,8 @@ class PodcastEpisode extends Model {
modelName: 'podcastEpisode',
indexes: [
{
- fields: ['createdAt']
+ name: 'podcastEpisode_createdAt_podcastId',
+ fields: ['createdAt', 'podcastId']
}
]
}
diff --git a/server/models/Series.js b/server/models/Series.js
index dc8d110fd6..731908e9ca 100644
--- a/server/models/Series.js
+++ b/server/models/Series.js
@@ -1,7 +1,6 @@
const { DataTypes, Model, where, fn, col } = require('sequelize')
const { getTitlePrefixAtEnd } = require('../utils/index')
-const { asciiOnlyToLowerCase } = require('../utils/index')
class Series extends Model {
constructor(values, options) {
@@ -42,7 +41,7 @@ class Series extends Model {
static async getByNameAndLibrary(seriesName, libraryId) {
return this.findOne({
where: [
- where(fn('lower', col('name')), asciiOnlyToLowerCase(seriesName)),
+ where(fn('lower', col('name')), seriesName.toLowerCase()),
{
libraryId
}
@@ -84,6 +83,12 @@ class Series extends Model {
// collate: 'NOCASE'
// }]
// },
+ {
+ // unique constraint on name and libraryId
+ fields: ['name', 'libraryId'],
+ unique: true,
+ name: 'unique_series_name_per_library'
+ },
{
fields: ['libraryId']
}
diff --git a/server/models/User.js b/server/models/User.js
index 4333db88e6..b2a4fd2bcc 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -3,6 +3,53 @@ const sequelize = require('sequelize')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const { isNullOrNaN } = require('../utils')
+const { LRUCache } = require('lru-cache')
+
+class UserCache {
+ constructor() {
+ this.cache = new LRUCache({ max: 100 })
+ }
+
+ getById(id) {
+ const user = this.cache.get(id)
+ return user
+ }
+
+ getByEmail(email) {
+ const user = this.cache.find((u) => u.email === email)
+ return user
+ }
+
+ getByUsername(username) {
+ const user = this.cache.find((u) => u.username === username)
+ return user
+ }
+
+ getByOldId(oldUserId) {
+ const user = this.cache.find((u) => u.extraData?.oldUserId === oldUserId)
+ return user
+ }
+
+ getByOpenIDSub(sub) {
+ const user = this.cache.find((u) => u.extraData?.authOpenIDSub === sub)
+ return user
+ }
+
+ set(user) {
+ user.fromCache = true
+ this.cache.set(user.id, user)
+ }
+
+ delete(userId) {
+ this.cache.delete(userId)
+ }
+
+ maybeInvalidate(user) {
+ if (!user.fromCache) this.delete(user.id)
+ }
+}
+
+const userCache = new UserCache()
const { DataTypes, Model } = sequelize
@@ -14,6 +61,23 @@ const { DataTypes, Model } = sequelize
* @property {number} createdAt
*/
+/**
+ * @typedef ProgressUpdatePayload
+ * @property {string} libraryItemId
+ * @property {string} [episodeId]
+ * @property {number} [duration]
+ * @property {number} [progress]
+ * @property {number} [currentTime]
+ * @property {boolean} [isFinished]
+ * @property {boolean} [hideFromContinueListening]
+ * @property {string} [ebookLocation]
+ * @property {number} [ebookProgress]
+ * @property {string} [finishedAt]
+ * @property {number} [lastUpdate]
+ * @property {number} [markAsFinishedTimeRemaining]
+ * @property {number} [markAsFinishedPercentComplete]
+ */
+
class User extends Model {
constructor(values, options) {
super(values, options)
@@ -65,6 +129,7 @@ class User extends Model {
canAccessExplicitContent: 'accessExplicitContent',
canAccessAllLibraries: 'accessAllLibraries',
canAccessAllTags: 'accessAllTags',
+ canCreateEReader: 'createEreader',
tagsAreDenylist: 'selectedTagsNotAccessible',
// Direct mapping for array-based permissions
allowedLibraries: 'librariesAccessible',
@@ -105,6 +170,7 @@ class User extends Model {
update: type === 'root' || type === 'admin',
delete: type === 'root',
upload: type === 'root' || type === 'admin',
+ createEreader: type === 'root' || type === 'admin',
accessAllLibraries: true,
accessAllTags: true,
accessExplicitContent: type === 'root' || type === 'admin',
@@ -187,7 +253,11 @@ class User extends Model {
*/
static async getUserByUsername(username) {
if (!username) return null
- return this.findOne({
+
+ const cachedUser = userCache.getByUsername(username)
+ if (cachedUser) return cachedUser
+
+ const user = await this.findOne({
where: {
username: {
[sequelize.Op.like]: username
@@ -195,6 +265,10 @@ class User extends Model {
},
include: this.sequelize.models.mediaProgress
})
+
+ if (user) userCache.set(user)
+
+ return user
}
/**
@@ -204,7 +278,11 @@ class User extends Model {
*/
static async getUserByEmail(email) {
if (!email) return null
- return this.findOne({
+
+ const cachedUser = userCache.getByEmail(email)
+ if (cachedUser) return cachedUser
+
+ const user = await this.findOne({
where: {
email: {
[sequelize.Op.like]: email
@@ -212,6 +290,10 @@ class User extends Model {
},
include: this.sequelize.models.mediaProgress
})
+
+ if (user) userCache.set(user)
+
+ return user
}
/**
@@ -221,9 +303,17 @@ class User extends Model {
*/
static async getUserById(userId) {
if (!userId) return null
- return this.findByPk(userId, {
+
+ const cachedUser = userCache.getById(userId)
+ if (cachedUser) return cachedUser
+
+ const user = await this.findByPk(userId, {
include: this.sequelize.models.mediaProgress
})
+
+ if (user) userCache.set(user)
+
+ return user
}
/**
@@ -235,12 +325,19 @@ class User extends Model {
*/
static async getUserByIdOrOldId(userId) {
if (!userId) return null
- return this.findOne({
+ const cachedUser = userCache.getById(userId) || userCache.getByOldId(userId)
+ if (cachedUser) return cachedUser
+
+ const user = await this.findOne({
where: {
[sequelize.Op.or]: [{ id: userId }, { 'extraData.oldUserId': userId }]
},
include: this.sequelize.models.mediaProgress
})
+
+ if (user) userCache.set(user)
+
+ return user
}
/**
@@ -250,10 +347,18 @@ class User extends Model {
*/
static async getUserByOpenIDSub(sub) {
if (!sub) return null
- return this.findOne({
+
+ const cachedUser = userCache.getByOpenIDSub(sub)
+ if (cachedUser) return cachedUser
+
+ const user = await this.findOne({
where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub),
include: this.sequelize.models.mediaProgress
})
+
+ if (user) userCache.set(user)
+
+ return user
}
/**
@@ -506,7 +611,7 @@ class User extends Model {
*/
getOldMediaProgress(libraryItemId, episodeId = null) {
const mediaProgress = this.mediaProgresses?.find((mp) => {
- if (episodeId && mp.mediaItemId === episodeId) return true
+ if (episodeId && mp.mediaItemId !== episodeId) return false
return mp.extraData?.libraryItemId === libraryItemId
})
return mediaProgress?.getOldMediaProgress() || null
@@ -515,19 +620,6 @@ class User extends Model {
/**
* TODO: Uses old model and should account for the different between ebook/audiobook progress
*
- * @typedef ProgressUpdatePayload
- * @property {string} libraryItemId
- * @property {string} [episodeId]
- * @property {number} [duration]
- * @property {number} [progress]
- * @property {number} [currentTime]
- * @property {boolean} [isFinished]
- * @property {boolean} [hideFromContinueListening]
- * @property {string} [ebookLocation]
- * @property {number} [ebookProgress]
- * @property {string} [finishedAt]
- * @property {number} [lastUpdate]
- *
* @param {ProgressUpdatePayload} progressPayload
* @returns {Promise<{ mediaProgress: import('./MediaProgress'), error: [string], statusCode: [number] }>}
*/
@@ -617,6 +709,7 @@ class User extends Model {
mediaProgress = await this.sequelize.models.mediaProgress.create(newMediaProgressPayload)
this.mediaProgresses.push(mediaProgress)
}
+ userCache.maybeInvalidate(this)
return {
mediaProgress
}
@@ -798,6 +891,21 @@ class User extends Model {
return hasUpdates
}
+
+ async update(values, options) {
+ userCache.maybeInvalidate(this)
+ return await super.update(values, options)
+ }
+
+ async save(options) {
+ userCache.maybeInvalidate(this)
+ return await super.save(options)
+ }
+
+ async destroy(options) {
+ userCache.delete(this.id)
+ await super.destroy(options)
+ }
}
module.exports = User
diff --git a/server/objects/PodcastEpisodeDownload.js b/server/objects/PodcastEpisodeDownload.js
index 2dfdc52e77..86a8580164 100644
--- a/server/objects/PodcastEpisodeDownload.js
+++ b/server/objects/PodcastEpisodeDownload.js
@@ -1,6 +1,6 @@
const Path = require('path')
-const uuidv4 = require("uuid").v4
-const { sanitizeFilename } = require('../utils/fileUtils')
+const uuidv4 = require('uuid').v4
+const { sanitizeFilename, filePathToPOSIX } = require('../utils/fileUtils')
const globals = require('../utils/globals')
class PodcastEpisodeDownload {
@@ -60,7 +60,7 @@ class PodcastEpisodeDownload {
return sanitizeFilename(filename)
}
get targetPath() {
- return Path.join(this.libraryItem.path, this.targetFilename)
+ return filePathToPOSIX(Path.join(this.libraryItem.path, this.targetFilename))
}
get targetRelPath() {
return this.targetFilename
@@ -74,7 +74,8 @@ class PodcastEpisodeDownload {
this.podcastEpisode = podcastEpisode
const url = podcastEpisode.enclosure.url
- if (decodeURIComponent(url) !== url) { // Already encoded
+ if (decodeURIComponent(url) !== url) {
+ // Already encoded
this.url = url
} else {
this.url = encodeURI(url)
diff --git a/server/objects/metadata/AudioMetaTags.js b/server/objects/metadata/AudioMetaTags.js
index 404c74837e..78c9d92ad8 100644
--- a/server/objects/metadata/AudioMetaTags.js
+++ b/server/objects/metadata/AudioMetaTags.js
@@ -9,6 +9,7 @@ class AudioMetaTags {
this.tagTitleSort = null
this.tagSeries = null
this.tagSeriesPart = null
+ this.tagGrouping = null
this.tagTrack = null
this.tagDisc = null
this.tagSubtitle = null
@@ -116,6 +117,7 @@ class AudioMetaTags {
this.tagTitleSort = metadata.tagTitleSort || null
this.tagSeries = metadata.tagSeries || null
this.tagSeriesPart = metadata.tagSeriesPart || null
+ this.tagGrouping = metadata.tagGrouping || null
this.tagTrack = metadata.tagTrack || null
this.tagDisc = metadata.tagDisc || null
this.tagSubtitle = metadata.tagSubtitle || null
@@ -156,6 +158,7 @@ class AudioMetaTags {
this.tagTitleSort = payload.file_tag_titlesort || null
this.tagSeries = payload.file_tag_series || null
this.tagSeriesPart = payload.file_tag_seriespart || null
+ this.tagGrouping = payload.file_tag_grouping || null
this.tagTrack = payload.file_tag_track || null
this.tagDisc = payload.file_tag_disc || null
this.tagSubtitle = payload.file_tag_subtitle || null
@@ -196,6 +199,7 @@ class AudioMetaTags {
tagTitleSort: payload.file_tag_titlesort || null,
tagSeries: payload.file_tag_series || null,
tagSeriesPart: payload.file_tag_seriespart || null,
+ tagGrouping: payload.file_tag_grouping || null,
tagTrack: payload.file_tag_track || null,
tagDisc: payload.file_tag_disc || null,
tagSubtitle: payload.file_tag_subtitle || null,
diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js
index 6d3dae4326..c6192f116c 100644
--- a/server/objects/metadata/BookMetadata.js
+++ b/server/objects/metadata/BookMetadata.js
@@ -29,7 +29,12 @@ class BookMetadata {
this.subtitle = metadata.subtitle
this.authors = metadata.authors?.map ? metadata.authors.map((a) => ({ ...a })) : []
this.narrators = metadata.narrators ? [...metadata.narrators].filter((n) => n) : []
- this.series = metadata.series?.map ? metadata.series.map((s) => ({ ...s })) : []
+ this.series = metadata.series?.map
+ ? metadata.series.map((s) => ({
+ ...s,
+ name: s.name || 'No Title'
+ }))
+ : []
this.genres = metadata.genres ? [...metadata.genres] : []
this.publishedYear = metadata.publishedYear || null
this.publishedDate = metadata.publishedDate || null
diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js
index 60762ede63..4f11a2a365 100644
--- a/server/providers/Audnexus.js
+++ b/server/providers/Audnexus.js
@@ -129,7 +129,7 @@ class Audnexus {
return null
}
- const author = await this.authorRequest(closestMatch.asin)
+ const author = await this.authorRequest(closestMatch.asin, region)
if (!author) {
return null
}
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index f44fedb475..7f21c3ac51 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -45,8 +45,6 @@ class ApiRouter {
this.abMergeManager = Server.abMergeManager
/** @type {import('../managers/BackupManager')} */
this.backupManager = Server.backupManager
- /** @type {import('../Watcher')} */
- this.watcher = Server.watcher
/** @type {import('../managers/PodcastManager')} */
this.podcastManager = Server.podcastManager
/** @type {import('../managers/AudioMetadataManager')} */
@@ -55,8 +53,6 @@ class ApiRouter {
this.rssFeedManager = Server.rssFeedManager
/** @type {import('../managers/CronManager')} */
this.cronManager = Server.cronManager
- /** @type {import('../managers/NotificationManager')} */
- this.notificationManager = Server.notificationManager
/** @type {import('../managers/EmailManager')} */
this.emailManager = Server.emailManager
this.apiCacheManager = Server.apiCacheManager
@@ -98,6 +94,7 @@ class ApiRouter {
this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this))
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
this.router.post('/libraries/:id/remove-metadata', LibraryController.middleware.bind(this), LibraryController.removeAllMetadataFiles.bind(this))
+ this.router.get('/libraries/:id/podcast-titles', LibraryController.middleware.bind(this), LibraryController.getPodcastTitles.bind(this))
//
// Item Routes
@@ -191,6 +188,7 @@ class ApiRouter {
this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))
this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this))
this.router.get('/me/stats/year/:year', MeController.getStatsForYear.bind(this))
+ this.router.post('/me/ereader-devices', MeController.updateUserEReaderDevices.bind(this))
//
// Backup Routes
@@ -216,7 +214,7 @@ class ApiRouter {
this.router.patch('/authors/:id', AuthorController.middleware.bind(this), AuthorController.update.bind(this))
this.router.delete('/authors/:id', AuthorController.middleware.bind(this), AuthorController.delete.bind(this))
this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this))
- this.router.get('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.getImage.bind(this))
+ this.router.get('/authors/:id/image', AuthorController.getImage.bind(this))
this.router.post('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.uploadImage.bind(this))
this.router.delete('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.deleteImage.bind(this))
diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js
index 2a70e6a071..3c364c1066 100644
--- a/server/scanner/AudioFileScanner.js
+++ b/server/scanner/AudioFileScanner.js
@@ -4,6 +4,7 @@ const prober = require('../utils/prober')
const { LogLevel } = require('../utils/constants')
const { parseOverdriveMediaMarkersAsChapters } = require('../utils/parsers/parseOverdriveMediaMarkers')
const parseNameString = require('../utils/parsers/parseNameString')
+const parseSeriesString = require('../utils/parsers/parseSeriesString')
const LibraryItem = require('../models/LibraryItem')
const AudioFile = require('../objects/files/AudioFile')
@@ -256,6 +257,7 @@ class AudioFileScanner {
},
{
tag: 'tagSeries',
+ altTag: 'tagGrouping',
key: 'series'
},
{
@@ -276,8 +278,10 @@ class AudioFileScanner {
const audioFileMetaTags = firstScannedFile.metaTags
MetadataMapArray.forEach((mapping) => {
let value = audioFileMetaTags[mapping.tag]
+ let isAltTag = false
if (!value && mapping.altTag) {
value = audioFileMetaTags[mapping.altTag]
+ isAltTag = true
}
if (value && typeof value === 'string') {
@@ -290,12 +294,28 @@ class AudioFileScanner {
} else if (mapping.key === 'genres') {
bookMetadata.genres = this.parseGenresString(value)
} else if (mapping.key === 'series') {
- bookMetadata.series = [
- {
- name: value,
- sequence: audioFileMetaTags.tagSeriesPart || null
+ // If series was embedded in the grouping tag, then parse it with semicolon separator and sequence in the same string
+ // e.g. "Test Series; Series Name #1; Other Series #2"
+ if (isAltTag) {
+ const series = value
+ .split(';')
+ .map((seriesWithPart) => {
+ seriesWithPart = seriesWithPart.trim()
+ return parseSeriesString.parse(seriesWithPart)
+ })
+ .filter(Boolean)
+ if (series.length) {
+ bookMetadata.series = series
}
- ]
+ } else {
+ // Original embed used "series" and "series-part" tags
+ bookMetadata.series = [
+ {
+ name: value,
+ sequence: audioFileMetaTags.tagSeriesPart || null
+ }
+ ]
+ }
} else {
bookMetadata[mapping.key] = value
}
diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js
index 279fcf64da..f0737dac05 100644
--- a/server/scanner/BookScanner.js
+++ b/server/scanner/BookScanner.js
@@ -590,6 +590,10 @@ class BookScanner {
Database.addPublisherToFilterData(libraryItemData.libraryId, libraryItem.book.publisher)
Database.addLanguageToFilterData(libraryItemData.libraryId, libraryItem.book.language)
+ const publishedYear = libraryItem.book.publishedYear
+ const decade = publishedYear ? `${Math.floor(publishedYear / 10) * 10}` : null
+ Database.addPublishedDecadeToFilterData(libraryItemData.libraryId, decade)
+
// Load for emitting to client
libraryItem.media = await libraryItem.getMedia({
include: [
diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js
index 38608e479f..5edfc2e2b6 100644
--- a/server/scanner/LibraryItemScanner.js
+++ b/server/scanner/LibraryItemScanner.js
@@ -4,7 +4,9 @@ const { LogLevel, ScanResult } = require('../utils/constants')
const fileUtils = require('../utils/fileUtils')
const scanUtils = require('../utils/scandir')
const libraryFilters = require('../utils/queries/libraryFilters')
+const Logger = require('../Logger')
const Database = require('../Database')
+const Watcher = require('../Watcher')
const LibraryScan = require('./LibraryScan')
const LibraryItemScanData = require('./LibraryItemScanData')
const BookScanner = require('./BookScanner')
@@ -128,6 +130,13 @@ class LibraryItemScanner {
const libraryFiles = []
for (let i = 0; i < fileItems.length; i++) {
const fileItem = fileItems[i]
+
+ if (Watcher.checkShouldIgnoreFilePath(fileItem.fullpath)) {
+ // Skip file if it's pending
+ Logger.info(`[LibraryItemScanner] Skipping watcher pending file "${fileItem.fullpath}" during scan of library item path "${libraryItemPath}"`)
+ continue
+ }
+
const newLibraryFile = new LibraryFile()
// fileItem.path is the relative path
await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path)
diff --git a/server/utils/constants.js b/server/utils/constants.js
index cbfe65f207..dd52e2e1b2 100644
--- a/server/utils/constants.js
+++ b/server/utils/constants.js
@@ -49,5 +49,7 @@ module.exports.AudioMimeType = {
WEBMA: 'audio/webm',
MKA: 'audio/x-matroska',
AWB: 'audio/amr-wb',
- CAF: 'audio/x-caf'
+ CAF: 'audio/x-caf',
+ MPEG: 'audio/mpeg',
+ MPG: 'audio/mpeg'
}
diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js
index 3fa9f63cd6..c70242252c 100644
--- a/server/utils/ffmpegHelpers.js
+++ b/server/utils/ffmpegHelpers.js
@@ -55,7 +55,7 @@ async function extractCoverArt(filepath, outputpath) {
return new Promise((resolve) => {
/** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
var ffmpeg = Ffmpeg(filepath)
- ffmpeg.addOption(['-map 0:v', '-frames:v 1'])
+ ffmpeg.addOption(['-map 0:v:0', '-frames:v 1'])
ffmpeg.output(outputpath)
ffmpeg.on('start', (cmd) => {
@@ -380,9 +380,8 @@ function getFFMetadataObject(libraryItem, audioFilesLength) {
copyright: metadata.publisher,
publisher: metadata.publisher, // mp3 only
TRACKTOTAL: `${audioFilesLength}`, // mp3 only
- grouping: metadata.series?.map((s) => s.name + (s.sequence ? ` #${s.sequence}` : '')).join(', ')
+ grouping: metadata.series?.map((s) => s.name + (s.sequence ? ` #${s.sequence}` : '')).join('; ')
}
-
Object.keys(ffmetadata).forEach((key) => {
if (!ffmetadata[key]) {
delete ffmetadata[key]
diff --git a/server/utils/generators/abmetadataGenerator.js b/server/utils/generators/abmetadataGenerator.js
index e0b78d2e04..01f8532889 100644
--- a/server/utils/generators/abmetadataGenerator.js
+++ b/server/utils/generators/abmetadataGenerator.js
@@ -1,4 +1,5 @@
const Logger = require('../../Logger')
+const parseSeriesString = require('../parsers/parseSeriesString')
function parseJsonMetadataText(text) {
try {
@@ -19,39 +20,25 @@ 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 => {
- let sequence = null
- let name = series
- // Series sequence match any characters after " #" other than whitespace and another #
- // e.g. "Name #1a" is valid. "Name #1#a" or "Name #1 a" is not valid.
- const matchResults = series.match(/ #([^#\s]+)$/) // Pull out sequence #
- if (matchResults && matchResults.length && matchResults.length > 1) {
- sequence = matchResults[1] // Group 1
- name = series.replace(matchResults[0], '')
- }
- return {
- name,
- sequence
- }
- })
+ abmetadataData.series = [...new Set(abmetadataData.series.map((t) => t?.trim()).filter((t) => t))]
+ abmetadataData.series = abmetadataData.series.map((series) => parseSeriesString.parse(series))
}
// clean tags & remove dupes
if (abmetadataData.tags?.length) {
- abmetadataData.tags = [...new Set(abmetadataData.tags.map(t => t?.trim()).filter(t => t))]
+ abmetadataData.tags = [...new Set(abmetadataData.tags.map((t) => t?.trim()).filter((t) => t))]
}
if (abmetadataData.chapters?.length) {
abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title)
}
// clean remove dupes
if (abmetadataData.authors?.length) {
- abmetadataData.authors = [...new Set(abmetadataData.authors.map(t => t?.trim()).filter(t => t))]
+ abmetadataData.authors = [...new Set(abmetadataData.authors.map((t) => t?.trim()).filter((t) => t))]
}
if (abmetadataData.narrators?.length) {
- abmetadataData.narrators = [...new Set(abmetadataData.narrators.map(t => t?.trim()).filter(t => t))]
+ 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))]
+ abmetadataData.genres = [...new Set(abmetadataData.genres.map((t) => t?.trim()).filter((t) => t))]
}
return abmetadataData
} catch (error) {
diff --git a/server/utils/globals.js b/server/utils/globals.js
index 877cf07a08..5a5bd9513e 100644
--- a/server/utils/globals.js
+++ b/server/utils/globals.js
@@ -1,6 +1,6 @@
const globals = {
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
- SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'],
+ SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpg', 'mpeg'],
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
TextFileTypes: ['txt', 'nfo'],
MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
diff --git a/server/utils/index.js b/server/utils/index.js
index 2d52bcd084..fa7ae92ed2 100644
--- a/server/utils/index.js
+++ b/server/utils/index.js
@@ -194,29 +194,6 @@ module.exports.getTitlePrefixAtEnd = (title) => {
return prefix ? `${sort}, ${prefix}` : title
}
-/**
- * to lower case for only ascii characters
- * used to handle sqlite that doesnt support unicode lower
- * @see https://github.com/advplyr/audiobookshelf/issues/2187
- *
- * @param {string} str
- * @returns {string}
- */
-module.exports.asciiOnlyToLowerCase = (str) => {
- if (!str) return ''
-
- let temp = ''
- for (let chars of str) {
- let value = chars.charCodeAt()
- if (value >= 65 && value <= 90) {
- temp += String.fromCharCode(value + 32)
- } else {
- temp += chars
- }
- }
- return temp
-}
-
/**
* Escape string used in RegExp
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
diff --git a/server/utils/notifications.js b/server/utils/notifications.js
index 96e8ddf8cd..7a3e119839 100644
--- a/server/utils/notifications.js
+++ b/server/utils/notifications.js
@@ -7,6 +7,7 @@ module.exports.notificationData = {
requiresLibrary: true,
libraryMediaType: 'podcast',
description: 'Triggered when a podcast episode is auto-downloaded',
+ descriptionKey: 'NotificationOnEpisodeDownloadedDescription',
variables: ['libraryItemId', 'libraryId', 'podcastTitle', 'podcastAuthor', 'podcastDescription', 'podcastGenres', 'episodeTitle', 'episodeSubtitle', 'episodeDescription', 'libraryName', 'episodeId', 'mediaTags'],
defaults: {
title: 'New {{podcastTitle}} Episode!',
@@ -31,6 +32,7 @@ module.exports.notificationData = {
name: 'onBackupCompleted',
requiresLibrary: false,
description: 'Triggered when a backup is completed',
+ descriptionKey: 'NotificationOnBackupCompletedDescription',
variables: ['completionTime', 'backupPath', 'backupSize', 'backupCount', 'removedOldest'],
defaults: {
title: 'Backup Completed',
@@ -48,6 +50,7 @@ module.exports.notificationData = {
name: 'onBackupFailed',
requiresLibrary: false,
description: 'Triggered when a backup fails',
+ descriptionKey: 'NotificationOnBackupFailedDescription',
variables: ['errorMsg'],
defaults: {
title: 'Backup Failed',
@@ -61,6 +64,7 @@ module.exports.notificationData = {
name: 'onTest',
requiresLibrary: false,
description: 'Event for testing the notification system',
+ descriptionKey: 'NotificationOnTestDescription',
variables: ['version'],
defaults: {
title: 'Test Notification on Abs {{version}}',
diff --git a/server/utils/parsers/parseNameString.js b/server/utils/parsers/parseNameString.js
index 741beb0911..4b16b496f1 100644
--- a/server/utils/parsers/parseNameString.js
+++ b/server/utils/parsers/parseNameString.js
@@ -52,6 +52,13 @@ module.exports.parse = (nameString) => {
}
if (splitNames.length) splitNames = splitNames.map((a) => a.trim())
+ // If names are in Chinese,Japanese and Korean languages, return as is.
+ if (/[\u4e00-\u9fff\u3040-\u30ff\u31f0-\u31ff]/.test(splitNames[0])) {
+ return {
+ names: splitNames
+ }
+ }
+
var names = []
// 1 name FIRST LAST
diff --git a/server/utils/parsers/parseSeriesString.js b/server/utils/parsers/parseSeriesString.js
new file mode 100644
index 0000000000..ed5f00e3f3
--- /dev/null
+++ b/server/utils/parsers/parseSeriesString.js
@@ -0,0 +1,27 @@
+/**
+ * Parse a series string into a name and sequence
+ *
+ * @example
+ * Name #1a => { name: 'Name', sequence: '1a' }
+ * Name #1 => { name: 'Name', sequence: '1' }
+ *
+ * @param {string} seriesString
+ * @returns {{name: string, sequence: string}|null}
+ */
+module.exports.parse = (seriesString) => {
+ if (!seriesString || typeof seriesString !== 'string') return null
+
+ let sequence = null
+ let name = seriesString
+ // Series sequence match any characters after " #" other than whitespace and another #
+ // e.g. "Name #1a" is valid. "Name #1#a" or "Name #1 a" is not valid.
+ const matchResults = seriesString.match(/ #([^#\s]+)$/) // Pull out sequence #
+ if (matchResults && matchResults.length && matchResults.length > 1) {
+ sequence = matchResults[1] // Group 1
+ name = seriesString.replace(matchResults[0], '')
+ }
+ return {
+ name,
+ sequence
+ }
+}
diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js
index 92679903ba..ac96c8d049 100644
--- a/server/utils/podcastUtils.js
+++ b/server/utils/podcastUtils.js
@@ -228,6 +228,13 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal
module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`)
+ let userAgent = 'audiobookshelf (+https://audiobookshelf.org; like iTMS)'
+ // Workaround for CBC RSS feeds rejecting our user agent string
+ // See: https://github.com/advplyr/audiobookshelf/issues/3322
+ if (feedUrl.startsWith('https://www.cbc.ca')) {
+ userAgent = 'audiobookshelf (+https://audiobookshelf.org; like iTMS) - CBC'
+ }
+
return axios({
url: feedUrl,
method: 'GET',
@@ -235,7 +242,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
responseType: 'arraybuffer',
headers: {
Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8',
- 'User-Agent': 'audiobookshelf (+https://audiobookshelf.org; like iTMS)'
+ 'User-Agent': userAgent
},
httpAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(feedUrl),
httpsAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(feedUrl)
diff --git a/server/utils/prober.js b/server/utils/prober.js
index 9b4d34e91b..b54b981d24 100644
--- a/server/utils/prober.js
+++ b/server/utils/prober.js
@@ -189,6 +189,7 @@ function parseTags(format, verbose) {
file_tag_genre: tryGrabTags(format, 'genre', 'tcon', 'tco'),
file_tag_series: tryGrabTags(format, 'series', 'show', 'mvnm'),
file_tag_seriespart: tryGrabTags(format, 'series-part', 'episode_id', 'mvin', 'part'),
+ file_tag_grouping: tryGrabTags(format, 'grouping'),
file_tag_isbn: tryGrabTags(format, 'isbn'), // custom
file_tag_language: tryGrabTags(format, 'language', 'lang'),
file_tag_asin: tryGrabTags(format, 'asin', 'audible_asin'), // custom
diff --git a/server/utils/queries/authorFilters.js b/server/utils/queries/authorFilters.js
index 675915350f..3d6bc7bd65 100644
--- a/server/utils/queries/authorFilters.js
+++ b/server/utils/queries/authorFilters.js
@@ -54,13 +54,13 @@ module.exports = {
* Search authors
*
* @param {string} libraryId
- * @param {string} query
+ * @param {Database.TextQuery} query
* @param {number} limit
* @param {number} offset
* @returns {Promise} oldAuthor with numBooks
*/
async search(libraryId, query, limit, offset) {
- const matchAuthor = Database.matchExpression('name', query)
+ const matchAuthor = query.matchExpression('name')
const authors = await Database.authorModel.findAll({
where: {
[Sequelize.Op.and]: [Sequelize.literal(matchAuthor), { libraryId }]
diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js
index 5c7b7dc0a9..bdddde7595 100644
--- a/server/utils/queries/libraryFilters.js
+++ b/server/utils/queries/libraryFilters.js
@@ -26,7 +26,7 @@ module.exports = {
let filterValue = null
let filterGroup = null
if (filterBy) {
- const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks']
+ const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'publishedDecades', 'missing', 'languages', 'tracks', 'ebooks']
const group = searchGroups.find((_group) => filterBy.startsWith(_group + '.'))
filterGroup = group || filterBy
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
@@ -458,10 +458,66 @@ module.exports = {
narrators: new Set(),
languages: new Set(),
publishers: new Set(),
+ publishedDecades: new Set(),
+ bookCount: 0, // How many books returned from database query
+ authorCount: 0, // How many authors returned from database query
+ seriesCount: 0, // How many series returned from database query
+ podcastCount: 0, // How many podcasts returned from database query
numIssues: 0
}
+ const lastLoadedAt = cachedFilterData ? cachedFilterData.loadedAt : 0
+
if (mediaType === 'podcast') {
+ // Check how many podcasts are in library to determine if we need to load all of the data
+ // This is done to handle the edge case of podcasts having been deleted and not having
+ // an updatedAt timestamp to trigger a reload of the filter data
+ const podcastCountFromDatabase = await Database.podcastModel.count({
+ include: {
+ model: Database.libraryItemModel,
+ attributes: [],
+ where: {
+ libraryId: libraryId
+ }
+ }
+ })
+
+ // To reduce the cold-start load time, first check if any podcasts
+ // have an "updatedAt" timestamp since the last time the filter
+ // data was loaded. If so, we can skip loading all of the data.
+ // Because many items could change, just check the count of items instead
+ // of actually loading the data twice
+ const changedPodcasts = await Database.podcastModel.count({
+ include: {
+ model: Database.libraryItemModel,
+ attributes: [],
+ where: {
+ libraryId: libraryId,
+ updatedAt: {
+ [Sequelize.Op.gt]: new Date(lastLoadedAt)
+ }
+ }
+ },
+ where: {
+ updatedAt: {
+ [Sequelize.Op.gt]: new Date(lastLoadedAt)
+ }
+ },
+ limit: 1
+ })
+
+ if (changedPodcasts === 0) {
+ // If nothing has changed, check if the number of podcasts in
+ // library is still the same as prior check before updating cache creation time
+
+ if (podcastCountFromDatabase === Database.libraryFilterData[libraryId]?.podcastCount) {
+ Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
+ Database.libraryFilterData[libraryId].loadedAt = Date.now()
+ return cachedFilterData
+ }
+ }
+
+ // Something has changed in the podcasts table, so reload all of the filter data for library
const podcasts = await Database.podcastModel.findAll({
include: {
model: Database.libraryItemModel,
@@ -483,7 +539,93 @@ module.exports = {
data.languages.add(podcast.language)
}
}
+
+ // Set podcast count for later comparison
+ data.podcastCount = podcastCountFromDatabase
} else {
+ const bookCountFromDatabase = await Database.bookModel.count({
+ include: {
+ model: Database.libraryItemModel,
+ attributes: [],
+ where: {
+ libraryId: libraryId
+ }
+ }
+ })
+
+ const seriesCountFromDatabase = await Database.seriesModel.count({
+ where: {
+ libraryId: libraryId
+ }
+ })
+
+ const authorCountFromDatabase = await Database.authorModel.count({
+ where: {
+ libraryId: libraryId
+ }
+ })
+
+ // To reduce the cold-start load time, first check if any library items, series,
+ // or authors have an "updatedAt" timestamp since the last time the filter
+ // data was loaded. If so, we can skip loading all of the data.
+ // Because many items could change, just check the count of items instead
+ // of actually loading the data twice
+
+ const changedBooks = await Database.bookModel.count({
+ include: {
+ model: Database.libraryItemModel,
+ attributes: [],
+ where: {
+ libraryId: libraryId,
+ updatedAt: {
+ [Sequelize.Op.gt]: new Date(lastLoadedAt)
+ }
+ }
+ },
+ where: {
+ updatedAt: {
+ [Sequelize.Op.gt]: new Date(lastLoadedAt)
+ }
+ },
+ limit: 1
+ })
+
+ const changedSeries = await Database.seriesModel.count({
+ where: {
+ libraryId: libraryId,
+ updatedAt: {
+ [Sequelize.Op.gt]: new Date(lastLoadedAt)
+ }
+ },
+ limit: 1
+ })
+
+ const changedAuthors = await Database.authorModel.count({
+ where: {
+ libraryId: libraryId,
+ updatedAt: {
+ [Sequelize.Op.gt]: new Date(lastLoadedAt)
+ }
+ },
+ limit: 1
+ })
+
+ if (changedBooks + changedSeries + changedAuthors === 0) {
+ // If nothing has changed, check if the number of authors, series, and books
+ // matches the prior check before updating cache creation time
+ if (bookCountFromDatabase === Database.libraryFilterData[libraryId]?.bookCount && seriesCountFromDatabase === Database.libraryFilterData[libraryId]?.seriesCount && authorCountFromDatabase === Database.libraryFilterData[libraryId].authorCount) {
+ Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
+ Database.libraryFilterData[libraryId].loadedAt = Date.now()
+ return cachedFilterData
+ }
+ }
+
+ // Store the counts for later comparison
+ data.bookCount = bookCountFromDatabase
+ data.seriesCount = seriesCountFromDatabase
+ data.authorCount = authorCountFromDatabase
+
+ // Something has changed in one of the tables, so reload all of the filter data for library
const books = await Database.bookModel.findAll({
include: {
model: Database.libraryItemModel,
@@ -492,7 +634,7 @@ module.exports = {
libraryId: libraryId
}
},
- attributes: ['tags', 'genres', 'publisher', 'narrators', 'language']
+ attributes: ['tags', 'genres', 'publisher', 'publishedYear', 'narrators', 'language']
})
for (const book of books) {
if (book.libraryItem.isMissing || book.libraryItem.isInvalid) data.numIssues++
@@ -506,6 +648,11 @@ module.exports = {
book.narrators.forEach((narrator) => data.narrators.add(narrator))
}
if (book.publisher) data.publishers.add(book.publisher)
+ // Check if published year exists and is valid
+ if (book.publishedYear && !isNaN(book.publishedYear) && book.publishedYear > 0 && book.publishedYear < 3000) {
+ const decade = (Math.floor(book.publishedYear / 10) * 10).toString()
+ data.publishedDecades.add(decade)
+ }
if (book.language) data.languages.add(book.language)
}
@@ -515,7 +662,7 @@ module.exports = {
},
attributes: ['id', 'name']
})
- series.forEach((s) => data.series.push({ id: s.id, name: s.name }))
+ series.forEach((s) => data.series.push({ id: s.id, name: s.name || 'No Title' }))
const authors = await Database.authorModel.findAll({
where: {
@@ -532,6 +679,7 @@ module.exports = {
data.series = naturalSort(data.series).asc((se) => se.name)
data.narrators = naturalSort([...data.narrators]).asc()
data.publishers = naturalSort([...data.publishers]).asc()
+ data.publishedDecades = naturalSort([...data.publishedDecades]).asc()
data.languages = naturalSort([...data.languages]).asc()
data.loadedAt = Date.now()
Database.libraryFilterData[libraryId] = data
diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js
index ae1ccc03bc..b2784f5ddd 100644
--- a/server/utils/queries/libraryItemsBookFilters.js
+++ b/server/utils/queries/libraryItemsBookFilters.js
@@ -219,7 +219,7 @@ module.exports = {
mediaWhere[key] = {
[Sequelize.Op.or]: [null, '']
}
- } else if (['genres', 'tags', 'narrators'].includes(value)) {
+ } else if (['genres', 'tags', 'narrators', 'chapters'].includes(value)) {
mediaWhere[value] = {
[Sequelize.Op.or]: [null, Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col(value)), 0)]
}
@@ -228,6 +228,12 @@ module.exports = {
} else if (value === 'series') {
mediaWhere['$series.id$'] = null
}
+ } else if (group === 'publishedDecades') {
+ const startYear = parseInt(value)
+ const endYear = parseInt(value, 10) + 9
+ mediaWhere = Sequelize.where(Sequelize.literal('CAST(`book`.`publishedYear` AS INTEGER)'), {
+ [Sequelize.Op.between]: [startYear, endYear]
+ })
}
return { mediaWhere, replacements }
@@ -253,7 +259,7 @@ module.exports = {
} else if (sortBy === 'media.duration') {
return [['duration', dir]]
} else if (sortBy === 'media.metadata.publishedYear') {
- return [['publishedYear', dir]]
+ return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]]
} else if (sortBy === 'media.metadata.authorNameLF') {
return [[Sequelize.literal('author_name COLLATE NOCASE'), dir]]
} else if (sortBy === 'media.metadata.authorName') {
@@ -499,7 +505,6 @@ module.exports = {
}
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
-
let bookWhere = Array.isArray(mediaWhere) ? mediaWhere : [mediaWhere]
// User permissions
@@ -975,10 +980,10 @@ module.exports = {
async search(user, library, query, limit, offset) {
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
- const normalizedQuery = query
+ const textSearchQuery = await Database.createTextSearchQuery(query)
- const matchTitle = Database.matchExpression('title', normalizedQuery)
- const matchSubtitle = Database.matchExpression('subtitle', normalizedQuery)
+ const matchTitle = textSearchQuery.matchExpression('title')
+ const matchSubtitle = textSearchQuery.matchExpression('subtitle')
// Search title, subtitle, asin, isbn
const books = await Database.bookModel.findAll({
@@ -1041,7 +1046,7 @@ module.exports = {
})
}
- const matchJsonValue = Database.matchExpression('json_each.value', normalizedQuery)
+ const matchJsonValue = textSearchQuery.matchExpression('json_each.value')
// Search narrators
const narratorMatches = []
@@ -1095,7 +1100,7 @@ module.exports = {
}
// Search series
- const matchName = Database.matchExpression('name', normalizedQuery)
+ const matchName = textSearchQuery.matchExpression('name')
const allSeries = await Database.seriesModel.findAll({
where: {
[Sequelize.Op.and]: [
@@ -1136,7 +1141,7 @@ module.exports = {
}
// Search authors
- const authorMatches = await authorFilters.search(library.id, normalizedQuery, limit, offset)
+ const authorMatches = await authorFilters.search(library.id, textSearchQuery, limit, offset)
return {
book: itemMatches,
diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js
index 50163edfbd..2f259efc07 100644
--- a/server/utils/queries/libraryItemsPodcastFilters.js
+++ b/server/utils/queries/libraryItemsPodcastFilters.js
@@ -315,9 +315,10 @@ module.exports = {
async search(user, library, query, limit, offset) {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
- const normalizedQuery = query
- const matchTitle = Database.matchExpression('title', normalizedQuery)
- const matchAuthor = Database.matchExpression('author', normalizedQuery)
+ const textSearchQuery = await Database.createTextSearchQuery(query)
+
+ const matchTitle = textSearchQuery.matchExpression('title')
+ const matchAuthor = textSearchQuery.matchExpression('author')
// Search title, author, itunesId, itunesArtistId
const podcasts = await Database.podcastModel.findAll({
@@ -366,7 +367,7 @@ module.exports = {
})
}
- const matchJsonValue = Database.matchExpression('json_each.value', normalizedQuery)
+ const matchJsonValue = textSearchQuery.matchExpression('json_each.value')
// Search tags
const tagMatches = []
diff --git a/server/utils/queries/seriesFilters.js b/server/utils/queries/seriesFilters.js
index 06ca254793..c293f1dfb0 100644
--- a/server/utils/queries/seriesFilters.js
+++ b/server/utils/queries/seriesFilters.js
@@ -73,15 +73,19 @@ module.exports = {
userPermissionBookWhere.replacements.filterValue = filterValue
} else if (filterGroup === 'progress') {
if (filterValue === 'not-finished') {
- attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
+ attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id AND mp.userId = :userId WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
+ userPermissionBookWhere.replacements.userId = user.id
} else if (filterValue === 'finished') {
- const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
+ const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id AND mp.userId = :userId WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${progQuery})`), 0))
+ userPermissionBookWhere.replacements.userId = user.id
} else if (filterValue === 'not-started') {
- const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished = 1 OR mp.currentTime > 0)'
+ const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id AND mp.userId = :userId WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished = 1 OR mp.currentTime > 0)'
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${progQuery})`), 0))
+ userPermissionBookWhere.replacements.userId = user.id
} else if (filterValue === 'in-progress') {
- attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.currentTime > 0 OR mp.ebookProgress > 0) AND mp.isFinished = 0'
+ attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id AND mp.userId = :userId WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.currentTime > 0 OR mp.ebookProgress > 0) AND mp.isFinished = 0'
+ userPermissionBookWhere.replacements.userId = user.id
}
}
diff --git a/test/server/managers/BinaryManager.test.js b/test/server/managers/BinaryManager.test.js
index 365fdff9e7..5ec38c4b93 100644
--- a/test/server/managers/BinaryManager.test.js
+++ b/test/server/managers/BinaryManager.test.js
@@ -85,6 +85,25 @@ describe('BinaryManager', () => {
expect(exitStub.calledOnce).to.be.true
expect(exitStub.calledWith(1)).to.be.true
})
+
+ it('should not exit if binaries are not found but not required', async () => {
+ const ffmpegBinary = new Binary('ffmpeg', 'executable', 'FFMPEG_PATH', ['5.1'], ffbinaries)
+ const ffprobeBinary = new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries, false)
+ const requiredBinaries = [ffmpegBinary]
+ const missingBinaries = [ffprobeBinary]
+ const missingBinariesAfterInstall = [ffprobeBinary]
+ findStub.onFirstCall().resolves(missingBinaries)
+ findStub.onSecondCall().resolves(missingBinariesAfterInstall)
+ binaryManager.requiredBinaries = requiredBinaries
+
+ await binaryManager.init()
+
+ expect(findStub.calledTwice).to.be.true
+ expect(installStub.calledOnce).to.be.true
+ expect(removeOldBinariesStub.calledOnce).to.be.true
+ expect(errorStub.called).to.be.false
+ expect(exitStub.called).to.be.false
+ })
})
describe('findRequiredBinaries', () => {
@@ -296,6 +315,7 @@ describe('Binary', () => {
describe('isGood', () => {
let binary
let fsPathExistsStub
+ let fsReadFileStub
let execStub
const binaryPath = '/path/to/binary'
@@ -305,11 +325,13 @@ describe('Binary', () => {
beforeEach(() => {
binary = new Binary('ffmpeg', 'executable', 'FFMPEG_PATH', goodVersions, ffbinaries)
fsPathExistsStub = sinon.stub(fs, 'pathExists')
+ fsReadFileStub = sinon.stub(fs, 'readFile')
execStub = sinon.stub(binary, 'exec')
})
afterEach(() => {
fsPathExistsStub.restore()
+ fsReadFileStub.restore()
execStub.restore()
})
@@ -388,6 +410,53 @@ describe('Binary', () => {
expect(execStub.calledOnce).to.be.true
expect(execStub.calledWith(execCommand)).to.be.true
})
+
+ it('should check library version file', async () => {
+ const binary = new Binary('libavcodec', 'library', 'FFMPEG_PATH', ['5.1'], ffbinaries)
+ fsReadFileStub.resolves('5.1.2 ')
+ fsPathExistsStub.onFirstCall().resolves(true)
+ fsPathExistsStub.onSecondCall().resolves(true)
+
+ const result = await binary.isGood(binaryPath)
+
+ expect(result).to.be.true
+ expect(fsPathExistsStub.calledTwice).to.be.true
+ expect(fsPathExistsStub.firstCall.args[0]).to.be.equal(binaryPath)
+ expect(fsPathExistsStub.secondCall.args[0]).to.be.equal(binaryPath + '.ver')
+ expect(fsReadFileStub.calledOnce).to.be.true
+ expect(fsReadFileStub.calledWith(binaryPath + '.ver'), 'utf8').to.be.true
+ })
+
+ it('should return false if library version file does not exist', async () => {
+ const binary = new Binary('libavcodec', 'library', 'FFMPEG_PATH', ['5.1'], ffbinaries)
+ fsReadFileStub.resolves('5.1.2 ')
+ fsPathExistsStub.onFirstCall().resolves(true)
+ fsPathExistsStub.onSecondCall().resolves(false)
+
+ const result = await binary.isGood(binaryPath)
+
+ expect(result).to.be.false
+ expect(fsPathExistsStub.calledTwice).to.be.true
+ expect(fsPathExistsStub.firstCall.args[0]).to.be.equal(binaryPath)
+ expect(fsPathExistsStub.secondCall.args[0]).to.be.equal(binaryPath + '.ver')
+ expect(fsReadFileStub.called).to.be.false
+ })
+
+ it('should return false if library version does not match a valid version', async () => {
+ const binary = new Binary('libavcodec', 'library', 'FFMPEG_PATH', ['5.1'], ffbinaries)
+ fsReadFileStub.resolves('5.2.1 ')
+ fsPathExistsStub.onFirstCall().resolves(true)
+ fsPathExistsStub.onSecondCall().resolves(true)
+
+ const result = await binary.isGood(binaryPath)
+
+ expect(result).to.be.false
+ expect(fsPathExistsStub.calledTwice).to.be.true
+ expect(fsPathExistsStub.firstCall.args[0]).to.be.equal(binaryPath)
+ expect(fsPathExistsStub.secondCall.args[0]).to.be.equal(binaryPath + '.ver')
+ expect(fsReadFileStub.calledOnce).to.be.true
+ expect(fsReadFileStub.calledWith(binaryPath + '.ver'), 'utf8').to.be.true
+ })
})
describe('getFileName', () => {
@@ -452,4 +521,43 @@ describe('Binary', () => {
expect(result).to.equal('ffmpeg')
})
})
+
+ describe('download', () => {
+ let binary
+ let downloadBinaryStub
+ let fsWriteFileStub
+
+ beforeEach(() => {
+ binary = new Binary('ffmpeg', 'executable', 'FFMPEG_PATH', ['5.1'], ffbinaries)
+ downloadBinaryStub = sinon.stub(binary.source, 'downloadBinary')
+ fsWriteFileStub = sinon.stub(fs, 'writeFile')
+ })
+
+ afterEach(() => {
+ downloadBinaryStub.restore()
+ fsWriteFileStub.restore()
+ })
+
+ it('should call downloadBinary with the correct parameters', async () => {
+ const destination = '/path/to/destination'
+
+ await binary.download(destination)
+
+ expect(downloadBinaryStub.calledOnce).to.be.true
+ expect(downloadBinaryStub.calledWith('ffmpeg', '5.1', destination)).to.be.true
+ })
+
+ it('should write a version file for libraries', async () => {
+ const binary = new Binary('libavcodec', 'library', 'FFMPEG_PATH', ['5.1'], ffbinaries)
+ const destination = '/path/to/destination'
+ const versionFilePath = path.join(destination, binary.fileName) + '.ver'
+
+ await binary.download(destination)
+
+ expect(downloadBinaryStub.calledOnce).to.be.true
+ expect(downloadBinaryStub.calledWith('libavcodec', '5.1', destination)).to.be.true
+ expect(fsWriteFileStub.calledOnce).to.be.true
+ expect(fsWriteFileStub.calledWith(versionFilePath, '5.1')).to.be.true
+ })
+ })
})
diff --git a/test/server/managers/MigrationManager.test.js b/test/server/managers/MigrationManager.test.js
index ae94cd75cc..af2e9da8f0 100644
--- a/test/server/managers/MigrationManager.test.js
+++ b/test/server/managers/MigrationManager.test.js
@@ -63,6 +63,8 @@ describe('MigrationManager', () => {
await migrationManager.init(serverVersion)
// Assert
+ expect(fsEnsureDirStub.calledOnce).to.be.true
+ expect(fsEnsureDirStub.calledWith(migrationManager.migrationsDir)).to.be.true
expect(migrationManager.serverVersion).to.equal(serverVersion)
expect(migrationManager.sequelize).to.equal(sequelizeStub)
expect(migrationManager.migrationsDir).to.equal(path.join(__dirname, 'migrations'))
@@ -353,8 +355,6 @@ describe('MigrationManager', () => {
await migrationManager.copyMigrationsToConfigDir()
// Assert
- expect(fsEnsureDirStub.calledOnce).to.be.true
- expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true
expect(readdirStub.calledOnce).to.be.true
expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true
expect(fsCopyStub.calledTwice).to.be.true
@@ -382,8 +382,6 @@ describe('MigrationManager', () => {
} catch (error) {}
// Assert
- expect(fsEnsureDirStub.calledOnce).to.be.true
- expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true
expect(readdirStub.calledOnce).to.be.true
expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true
expect(fsCopyStub.calledTwice).to.be.true
diff --git a/test/server/migrations/v2.15.0-series-column-unique.test.js b/test/server/migrations/v2.15.0-series-column-unique.test.js
new file mode 100644
index 0000000000..a9ad0fab13
--- /dev/null
+++ b/test/server/migrations/v2.15.0-series-column-unique.test.js
@@ -0,0 +1,342 @@
+const { expect } = require('chai')
+const sinon = require('sinon')
+const { up, down } = require('../../../server/migrations/v2.15.0-series-column-unique')
+const { Sequelize } = require('sequelize')
+const Logger = require('../../../server/Logger')
+const { query } = require('express')
+const { logger } = require('sequelize/lib/utils/logger')
+const e = require('express')
+
+describe('migration-v2.15.0-series-column-unique', () => {
+ let sequelize
+ let queryInterface
+ let loggerInfoStub
+ let series1Id
+ let series2Id
+ let series3Id
+ let series1Id_dup
+ let series3Id_dup
+ let series1Id_dup2
+ let book1Id
+ let book2Id
+ let book3Id
+ let book4Id
+ let book5Id
+ let book6Id
+ let library1Id
+ let library2Id
+ let bookSeries1Id
+ let bookSeries2Id
+ let bookSeries3Id
+ let bookSeries1Id_dup
+ let bookSeries3Id_dup
+ let bookSeries1Id_dup2
+
+ beforeEach(() => {
+ sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
+ queryInterface = sequelize.getQueryInterface()
+ loggerInfoStub = sinon.stub(Logger, 'info')
+ })
+
+ afterEach(() => {
+ sinon.restore()
+ })
+
+ describe('up', () => {
+ beforeEach(async () => {
+ await queryInterface.createTable('Series', {
+ id: { type: Sequelize.UUID, primaryKey: true },
+ name: { type: Sequelize.STRING, allowNull: false },
+ libraryId: { type: Sequelize.UUID, allowNull: false },
+ createdAt: { type: Sequelize.DATE, allowNull: false },
+ updatedAt: { type: Sequelize.DATE, allowNull: false }
+ })
+ // Create a table for BookSeries, with a unique constraint of bookId and seriesId
+ await queryInterface.createTable(
+ 'BookSeries',
+ {
+ id: { type: Sequelize.UUID, primaryKey: true },
+ sequence: { type: Sequelize.STRING, allowNull: true },
+ bookId: { type: Sequelize.UUID, allowNull: false },
+ seriesId: { type: Sequelize.UUID, allowNull: false }
+ },
+ { uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } }
+ )
+ // Set UUIDs for the tests
+ series1Id = 'fc086255-3fd2-4a95-8a28-840d9206501b'
+ series2Id = '70f46ac2-ee48-4b3c-9822-933cc15c29bd'
+ series3Id = '01cac008-142b-4e15-b0ff-cf7cc2c5b64e'
+ series1Id_dup = 'ad0b3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ series3Id_dup = '4b3b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ series1Id_dup2 = '0123456a-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ book1Id = '4a38b6e5-0ae4-4de4-b119-4e33891bd63f'
+ book2Id = '8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f'
+ book3Id = 'ec9bbaaf-1e55-457f-b59c-bd2bd955a404'
+ book4Id = '876f3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ book5Id = '4e5b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ book6Id = 'abcda123-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ library1Id = '3a5a1c7c-a914-472e-88b0-b871ceae63e7'
+ library2Id = 'fd6c324a-4f3a-4bb0-99d6-7a330e765e7e'
+ bookSeries1Id = 'eca24687-2241-4ffa-a9b3-02a0ba03c763'
+ bookSeries2Id = '56f56105-813b-4395-9689-fd04198e7d5d'
+ bookSeries3Id = '404a1761-c710-4d86-9d78-68d9a9c0fb6b'
+ bookSeries1Id_dup = '8bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ bookSeries3Id_dup = '89656a3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ bookSeries1Id_dup2 = '9bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
+ })
+ afterEach(async () => {
+ await queryInterface.dropTable('Series')
+ await queryInterface.dropTable('BookSeries')
+ })
+ it('upgrade with no duplicate series', async () => {
+ // Add some entries to the Series table using the UUID for the ids
+ await queryInterface.bulkInsert('Series', [
+ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
+ ])
+ // Add some entries to the BookSeries table
+ await queryInterface.bulkInsert('BookSeries', [
+ { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id },
+ { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id },
+ { id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id }
+ ])
+
+ await up({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(6)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ // Validate rows in tables
+ const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(series).to.have.length(3)
+ expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
+ expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id })
+ expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id })
+ const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(bookSeries).to.have.length(3)
+ expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book2Id, seriesId: series2Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id })
+ })
+ it('upgrade with duplicate series and no sequence', async () => {
+ // Add some entries to the Series table using the UUID for the ids
+ await queryInterface.bulkInsert('Series', [
+ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
+ ])
+ // Add some entries to the BookSeries table
+ await queryInterface.bulkInsert('BookSeries', [
+ { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id },
+ { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id },
+ { id: bookSeries3Id, bookId: book3Id, seriesId: series3Id },
+ { id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id_dup },
+ { id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id_dup },
+ { id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id_dup2 }
+ ])
+
+ await up({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(8)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 2 duplicate series'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
+ expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ // Validate rows
+ const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(series).to.have.length(3)
+ expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
+ expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id })
+ expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id })
+ const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(bookSeries).to.have.length(6)
+ expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id })
+ })
+ it('upgrade with same series name in different libraries', async () => {
+ // Add some entries to the Series table using the UUID for the ids
+ await queryInterface.bulkInsert('Series', [
+ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series2Id, name: 'Series 1', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() }
+ ])
+ // Add some entries to the BookSeries table
+ await queryInterface.bulkInsert('BookSeries', [
+ { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id },
+ { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }
+ ])
+
+ await up({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(6)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ // Validate rows
+ const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(series).to.have.length(2)
+ expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
+ expect(series).to.deep.include({ id: series2Id, name: 'Series 1', libraryId: library2Id })
+ const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(bookSeries).to.have.length(2)
+ expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id })
+ expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id })
+ })
+ it('upgrade with one book in two of the same series, both sequence are null', async () => {
+ // Create two different series with the same name in the same library
+ await queryInterface.bulkInsert('Series', [
+ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
+ ])
+ // Create a book that is in both series
+ await queryInterface.bulkInsert('BookSeries', [
+ { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id },
+ { id: bookSeries2Id, bookId: book1Id, seriesId: series2Id }
+ ])
+
+ await up({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(9)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
+ expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ // validate rows
+ const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(series).to.have.length(1)
+ expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
+ const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(bookSeries).to.have.length(1)
+ // Keep BookSeries 2 because it was edited last from cleaning up duplicate books
+ expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book1Id, seriesId: series1Id })
+ })
+ it('upgrade with one book in two of the same series, one sequence is null', async () => {
+ // Create two different series with the same name in the same library
+ await queryInterface.bulkInsert('Series', [
+ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
+ ])
+ // Create a book that is in both series
+ await queryInterface.bulkInsert('BookSeries', [
+ { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id },
+ { id: bookSeries2Id, bookId: book1Id, seriesId: series2Id }
+ ])
+
+ await up({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(9)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
+ expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ // validate rows
+ const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(series).to.have.length(1)
+ expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
+ const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(bookSeries).to.have.length(1)
+ expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id })
+ })
+ it('upgrade with one book in two of the same series, both sequence are not null', async () => {
+ // Create two different series with the same name in the same library
+ await queryInterface.bulkInsert('Series', [
+ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
+ { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
+ ])
+ // Create a book that is in both series
+ await queryInterface.bulkInsert('BookSeries', [
+ { id: bookSeries1Id, sequence: '3', bookId: book1Id, seriesId: series1Id },
+ { id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series2Id }
+ ])
+
+ await up({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(9)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
+ expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
+ expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ // validate rows
+ const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(series).to.have.length(1)
+ expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
+ const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
+ expect(bookSeries).to.have.length(1)
+ // Keep BookSeries 2 because it is the lower sequence number
+ expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series1Id })
+ })
+ })
+
+ describe('down', () => {
+ beforeEach(async () => {
+ await queryInterface.createTable('Series', {
+ id: { type: Sequelize.UUID, primaryKey: true },
+ name: { type: Sequelize.STRING, allowNull: false },
+ libraryId: { type: Sequelize.UUID, allowNull: false },
+ createdAt: { type: Sequelize.DATE, allowNull: false },
+ updatedAt: { type: Sequelize.DATE, allowNull: false }
+ })
+ // Create a table for BookSeries, with a unique constraint of bookId and seriesId
+ await queryInterface.createTable(
+ 'BookSeries',
+ {
+ id: { type: Sequelize.UUID, primaryKey: true },
+ bookId: { type: Sequelize.UUID, allowNull: false },
+ seriesId: { type: Sequelize.UUID, allowNull: false }
+ },
+ { uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } }
+ )
+ })
+ it('should not have unique constraint on series name and libraryId', async () => {
+ await up({ context: { queryInterface, logger: Logger } })
+ await down({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(9)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
+ expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true
+ expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique '))).to.be.true
+ // Ensure index does not exist
+ const indexes = await queryInterface.showIndex('Series')
+ expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' })
+ })
+ })
+})