diff --git a/client/pages/login.vue b/client/pages/login.vue
index d12600c99d..a853def452 100644
--- a/client/pages/login.vue
+++ b/client/pages/login.vue
@@ -132,11 +132,11 @@ export default {
methods: {
async submitServerSetup() {
if (!this.newRoot.username || !this.newRoot.username.trim()) {
- this.$toast.error('Must enter a root username')
+ this.$toast.error(this.$strings.ToastUserRootRequireName)
return
}
if (this.newRoot.password !== this.confirmPassword) {
- this.$toast.error('Password mismatch')
+ this.$toast.error(this.$strings.ToastUserPasswordMismatch)
return
}
if (!this.newRoot.password) {
diff --git a/client/players/LocalVideoPlayer.js b/client/players/LocalVideoPlayer.js
deleted file mode 100644
index 128f070b01..0000000000
--- a/client/players/LocalVideoPlayer.js
+++ /dev/null
@@ -1,260 +0,0 @@
-import Hls from 'hls.js'
-import EventEmitter from 'events'
-
-export default class LocalVideoPlayer extends EventEmitter {
- constructor(ctx) {
- super()
-
- this.ctx = ctx
- this.player = null
-
- this.libraryItem = null
- this.videoTrack = null
- this.isHlsTranscode = null
- this.hlsInstance = null
- this.usingNativeplayer = false
- this.startTime = 0
- this.playWhenReady = false
- this.defaultPlaybackRate = 1
-
- this.playableMimeTypes = []
-
- this.initialize()
- }
-
- initialize() {
- if (document.getElementById('video-player')) {
- document.getElementById('video-player').remove()
- }
- var videoEl = document.createElement('video')
- videoEl.id = 'video-player'
- // videoEl.style.display = 'none'
- videoEl.className = 'absolute bg-black z-50'
- videoEl.style.height = '216px'
- videoEl.style.width = '384px'
- videoEl.style.bottom = '80px'
- videoEl.style.left = '16px'
- document.body.appendChild(videoEl)
- this.player = videoEl
-
- this.player.addEventListener('play', this.evtPlay.bind(this))
- this.player.addEventListener('pause', this.evtPause.bind(this))
- this.player.addEventListener('progress', this.evtProgress.bind(this))
- this.player.addEventListener('ended', this.evtEnded.bind(this))
- this.player.addEventListener('error', this.evtError.bind(this))
- this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
- this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
-
- var mimeTypes = ['video/mp4']
- var mimeTypeCanPlayMap = {}
- mimeTypes.forEach((mt) => {
- var canPlay = this.player.canPlayType(mt)
- mimeTypeCanPlayMap[mt] = canPlay
- if (canPlay) this.playableMimeTypes.push(mt)
- })
- console.log(`[LocalVideoPlayer] Supported mime types`, mimeTypeCanPlayMap, this.playableMimeTypes)
- }
-
- evtPlay() {
- this.emit('stateChange', 'PLAYING')
- }
- evtPause() {
- this.emit('stateChange', 'PAUSED')
- }
- evtProgress() {
- var lastBufferTime = this.getLastBufferedTime()
- this.emit('buffertimeUpdate', lastBufferTime)
- }
- evtEnded() {
- console.log(`[LocalVideoPlayer] Ended`)
- this.emit('finished')
- }
- evtError(error) {
- console.error('Player error', error)
- this.emit('error', error)
- }
- evtLoadedMetadata(data) {
- if (!this.isHlsTranscode) {
- this.player.currentTime = this.startTime
- }
-
- this.emit('stateChange', 'LOADED')
- if (this.playWhenReady) {
- this.playWhenReady = false
- this.play()
- }
- }
- evtTimeupdate() {
- if (this.player.paused) {
- this.emit('timeupdate', this.getCurrentTime())
- }
- }
-
- destroy() {
- this.destroyHlsInstance()
- if (this.player) {
- this.player.remove()
- }
- }
-
- set(libraryItem, videoTrack, isHlsTranscode, startTime, playWhenReady = false) {
- this.libraryItem = libraryItem
- this.videoTrack = videoTrack
- this.isHlsTranscode = isHlsTranscode
- this.playWhenReady = playWhenReady
- this.startTime = startTime
-
- if (this.hlsInstance) {
- this.destroyHlsInstance()
- }
-
- if (this.isHlsTranscode) {
- this.setHlsStream()
- } else {
- this.setDirectPlay()
- }
- }
-
- setHlsStream() {
- // iOS does not support Media Elements but allows for HLS in the native video player
- if (!Hls.isSupported()) {
- console.warn('HLS is not supported - fallback to using video element')
- this.usingNativeplayer = true
- this.player.src = this.videoTrack.relativeContentUrl
- this.player.currentTime = this.startTime
- return
- }
-
- var hlsOptions = {
- startPosition: this.startTime || -1
- // No longer needed because token is put in a query string
- // xhrSetup: (xhr) => {
- // xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
- // }
- }
- this.hlsInstance = new Hls(hlsOptions)
-
- this.hlsInstance.attachMedia(this.player)
- this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
- this.hlsInstance.loadSource(this.videoTrack.relativeContentUrl)
-
- this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
- console.log('[HLS] Manifest Parsed')
- })
-
- this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
- console.error('[HLS] Error', data.type, data.details, data)
- if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
- console.error('[HLS] BUFFER STALLED ERROR')
- }
- })
- this.hlsInstance.on(Hls.Events.DESTROYING, () => {
- console.log('[HLS] Destroying HLS Instance')
- })
- })
- }
-
- setDirectPlay() {
- this.player.src = this.videoTrack.relativeContentUrl
- console.log(`[LocalVideoPlayer] Loading track src ${this.videoTrack.relativeContentUrl}`)
- this.player.load()
- }
-
- destroyHlsInstance() {
- if (!this.hlsInstance) return
- if (this.hlsInstance.destroy) {
- var temp = this.hlsInstance
- temp.destroy()
- }
- this.hlsInstance = null
- }
-
- async resetStream(startTime) {
- this.destroyHlsInstance()
- await new Promise((resolve) => setTimeout(resolve, 1000))
- this.set(this.libraryItem, this.videoTrack, this.isHlsTranscode, startTime, true)
- }
-
- playPause() {
- if (!this.player) return
- if (this.player.paused) this.play()
- else this.pause()
- }
-
- play() {
- if (this.player) this.player.play()
- }
-
- pause() {
- if (this.player) this.player.pause()
- }
-
- getCurrentTime() {
- return this.player ? this.player.currentTime : 0
- }
-
- getDuration() {
- return this.videoTrack.duration
- }
-
- setPlaybackRate(playbackRate) {
- if (!this.player) return
- this.defaultPlaybackRate = playbackRate
- this.player.playbackRate = playbackRate
- }
-
- seek(time) {
- if (!this.player) return
- this.player.currentTime = Math.max(0, time)
- }
-
- setVolume(volume) {
- if (!this.player) return
- this.player.volume = volume
- }
-
- // Utils
- isValidDuration(duration) {
- if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
- return true
- }
- return false
- }
-
- getBufferedRanges() {
- if (!this.player) return []
- const ranges = []
- const seekable = this.player.buffered || []
-
- let offset = 0
-
- for (let i = 0, length = seekable.length; i < length; i++) {
- let start = seekable.start(i)
- let end = seekable.end(i)
- if (!this.isValidDuration(start)) {
- start = 0
- }
- if (!this.isValidDuration(end)) {
- end = 0
- continue
- }
-
- ranges.push({
- start: start + offset,
- end: end + offset
- })
- }
- return ranges
- }
-
- getLastBufferedTime() {
- var bufferedRanges = this.getBufferedRanges()
- if (!bufferedRanges.length) return 0
-
- var buff = bufferedRanges.find((buff) => buff.start < this.player.currentTime && buff.end > this.player.currentTime)
- if (buff) return buff.end
-
- 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 42d76bd033..a327f831f2 100644
--- a/client/players/PlayerHandler.js
+++ b/client/players/PlayerHandler.js
@@ -1,8 +1,6 @@
import LocalAudioPlayer from './LocalAudioPlayer'
-import LocalVideoPlayer from './LocalVideoPlayer'
import CastPlayer from './CastPlayer'
import AudioTrack from './AudioTrack'
-import VideoTrack from './VideoTrack'
export default class PlayerHandler {
constructor(ctx) {
@@ -16,8 +14,6 @@ export default class PlayerHandler {
this.player = null
this.playerState = 'IDLE'
this.isHlsTranscode = false
- this.isVideo = false
- this.isMusic = false
this.currentSessionId = null
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
this.startTime = 0
@@ -65,12 +61,10 @@ export default class PlayerHandler {
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
this.libraryItem = libraryItem
- this.isVideo = libraryItem.mediaType === 'video'
- this.isMusic = libraryItem.mediaType === 'music'
this.episodeId = episodeId
this.playWhenReady = playWhenReady
- this.initialPlaybackRate = this.isMusic ? 1 : playbackRate
+ this.initialPlaybackRate = playbackRate
this.startTimeOverride = startTimeOverride == null || isNaN(startTimeOverride) ? undefined : Number(startTimeOverride)
@@ -97,7 +91,7 @@ export default class PlayerHandler {
this.playWhenReady = playWhenReady
this.prepare()
}
- } else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer) && !(this.player instanceof LocalVideoPlayer)) {
+ } else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer)) {
console.log('[PlayerHandler] Switching to local player')
this.stopPlayInterval()
@@ -107,11 +101,7 @@ export default class PlayerHandler {
this.player.destroy()
}
- if (this.isVideo) {
- this.player = new LocalVideoPlayer(this.ctx)
- } else {
- this.player = new LocalAudioPlayer(this.ctx)
- }
+ this.player = new LocalAudioPlayer(this.ctx)
this.setPlayerListeners()
@@ -203,7 +193,7 @@ export default class PlayerHandler {
supportedMimeTypes: this.player.playableMimeTypes,
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
forceTranscode,
- forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast
+ forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
}
const path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play`
@@ -218,7 +208,6 @@ export default class PlayerHandler {
if (!this.player) this.switchPlayer() // Must set player first for open sessions
this.libraryItem = session.libraryItem
- this.isVideo = session.libraryItem.mediaType === 'video'
this.playWhenReady = false
this.initialPlaybackRate = playbackRate
this.startTimeOverride = undefined
@@ -237,28 +226,16 @@ export default class PlayerHandler {
console.log('[PlayerHandler] Preparing Session', session)
- if (session.videoTrack) {
- var videoTrack = new VideoTrack(session.videoTrack, this.userToken)
-
- this.ctx.playerLoading = true
- this.isHlsTranscode = true
- if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
- this.isHlsTranscode = false
- }
-
- this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady)
- } else {
- var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
-
- this.ctx.playerLoading = true
- this.isHlsTranscode = true
- if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
- this.isHlsTranscode = false
- }
+ var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
- this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
+ this.ctx.playerLoading = true
+ this.isHlsTranscode = true
+ if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
+ this.isHlsTranscode = false
}
+ this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
+
// browser media session api
this.ctx.setMediaSession()
}
@@ -333,8 +310,6 @@ export default class PlayerHandler {
}
sendProgressSync(currentTime) {
- if (this.isMusic) return
-
const diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
if (diffSinceLastSync < 1) return
diff --git a/client/players/VideoTrack.js b/client/players/VideoTrack.js
deleted file mode 100644
index 92bec5eb53..0000000000
--- a/client/players/VideoTrack.js
+++ /dev/null
@@ -1,32 +0,0 @@
-export default class VideoTrack {
- constructor(track, userToken) {
- this.index = track.index || 0
- this.startOffset = track.startOffset || 0 // Total time of all previous tracks
- this.duration = track.duration || 0
- this.title = track.title || ''
- this.contentUrl = track.contentUrl || null
- this.mimeType = track.mimeType
- this.metadata = track.metadata || {}
-
- this.userToken = userToken
- }
-
- get fullContentUrl() {
- 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 `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
- }
-
- 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}`
- }
-}
\ No newline at end of file
diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js
index f2784f1441..2eb6b123c8 100644
--- a/client/plugins/i18n.js
+++ b/client/plugins/i18n.js
@@ -25,6 +25,7 @@ const languageCodeMap = {
pl: { label: 'Polski', dateFnsLocale: 'pl' },
'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' },
ru: { label: 'Русский', dateFnsLocale: 'ru' },
+ sl: { label: 'Slovenščina', dateFnsLocale: 'sl' },
sv: { label: 'Svenska', dateFnsLocale: 'sv' },
uk: { label: 'Українська', dateFnsLocale: 'uk' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
diff --git a/client/plugins/utils.js b/client/plugins/utils.js
index ffcd33ad26..160ff9439c 100644
--- a/client/plugins/utils.js
+++ b/client/plugins/utils.js
@@ -11,7 +11,7 @@ Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
if (isNaN(bytes) || bytes == 0) {
return '0 Bytes'
}
- const k = 1024
+ const k = 1000
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
diff --git a/client/strings/bg.json b/client/strings/bg.json
index 3bcdea4825..3c934d8933 100644
--- a/client/strings/bg.json
+++ b/client/strings/bg.json
@@ -44,7 +44,6 @@
"ButtonMatchAllAuthors": "Съвпадение на Всички Автори",
"ButtonMatchBooks": "Съвпадение на Книги",
"ButtonNevermind": "Няма значение",
- "ButtonNext": "Next",
"ButtonNextChapter": "Следваща Глава",
"ButtonOk": "Добре",
"ButtonOpenFeed": "Отвори Feed",
@@ -53,7 +52,6 @@
"ButtonPlay": "Пусни",
"ButtonPlaying": "Пуска се",
"ButtonPlaylists": "Плейлисти",
- "ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Предишна Глава",
"ButtonPurgeAllCache": "Изчисти Всички Кешове",
"ButtonPurgeItemsCache": "Изчисти Кеша на Елементи",
@@ -202,7 +200,6 @@
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
"LabelAddToPlaylist": "Добави в Плейлист",
"LabelAddToPlaylistBatch": "Добави {0} Елемент в Плейлист",
- "LabelAdded": "Добавени",
"LabelAddedAt": "Добавени На",
"LabelAdminUsersOnly": "Само за Администратори",
"LabelAll": "Всички",
@@ -233,7 +230,6 @@
"LabelBitrate": "Битрейт",
"LabelBooks": "Книги",
"LabelButtonText": "Текст на Бутон",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Промени Парола",
"LabelChannels": "Канали",
"LabelChapterTitle": "Заглавие на Глава",
@@ -253,7 +249,6 @@
"LabelCover": "Корица",
"LabelCoverImageURL": "URL на Корица",
"LabelCreatedAt": "Създадено на",
- "LabelCronExpression": "Cron Expression",
"LabelCurrent": "Текущо",
"LabelCurrently": "Текущо:",
"LabelCustomCronExpression": "Потребителски Cron Expression:",
@@ -278,7 +273,6 @@
"LabelEbook": "Електронна книга",
"LabelEbooks": "Електронни книги",
"LabelEdit": "Редакция",
- "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "От Адрес",
"LabelEmailSettingsRejectUnauthorized": "Отхвърли неавторизирани сертификати",
"LabelEmailSettingsRejectUnauthorizedHelp": "Спирането на валидацията на SSL сертификате може да изложи връзката ви на рискове, като man-in-the-middle атака. Спираите тази опция само ако знете имоликацийте от това и се доверявате на mail сървъра към който се свързвате.",
@@ -293,9 +287,6 @@
"LabelEpisodeType": "Тип на Епизод",
"LabelExample": "Пример",
"LabelExplicit": "Експлицитно",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
- "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Взимане на Метаданни",
"LabelFile": "Файл",
"LabelFileBirthtime": "Дата на създаване на файла",
@@ -351,7 +342,6 @@
"LabelLess": "По-малко",
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
"LabelLibrary": "Библиотека",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Елемент на Библиотека",
"LabelLibraryName": "Име на Библиотека",
"LabelLimit": "Лимит",
@@ -403,9 +393,6 @@
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
"LabelNumberOfBooks": "Брой на Книги",
"LabelNumberOfEpisodes": "# Епизоди",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Отвори RSS Feed",
"LabelOverwrite": "Презапиши",
"LabelPassword": "Парола",
@@ -420,7 +407,6 @@
"LabelPersonalYearReview": "Преглед на годината Ви ({0})",
"LabelPhotoPathURL": "Път/URL на Снимка",
"LabelPlayMethod": "Метод на Пускане",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Плейлисти",
"LabelPodcast": "Подкаст",
"LabelPodcastSearchRegion": "Регион за Търсене на Подкасти",
@@ -441,7 +427,6 @@
"LabelRSSFeedOpen": "RSS Feed Оптворен",
"LabelRSSFeedPreventIndexing": "Предотврати индексиране",
"LabelRSSFeedSlug": "RSS Feed слъг",
- "LabelRSSFeedURL": "RSS Feed URL",
"LabelRead": "Прочети",
"LabelReadAgain": "Прочети Отново",
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
@@ -491,7 +476,6 @@
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
- "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": "Извлечи подзаглавия",
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.
Подзаглавията трябва да бъдат разделени с \" - \"
например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"",
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
@@ -508,7 +492,6 @@
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
"LabelSettingsTimeFormat": "Формат на Време",
"LabelShowAll": "Покажи Всички",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Размер",
"LabelSleepTimer": "Таймер за Сън",
"LabelSlug": "Слъг",
@@ -588,20 +571,16 @@
"LabelViewQueue": "Виж Опашка",
"LabelVolume": "Сила на Звука",
"LabelWeekdaysToRun": "Делници за изпълнение",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
"LabelYourBookmarks": "Вашите Отметки",
"LabelYourPlaylists": "Вашите Плейлисти",
"LabelYourProgress": "Вашият Прогрес",
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на
Apprise API или на друго АПИ което да обработва тези заявки.
The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от
http://192.168.1.1:8337
трябва да сложитев
http://192.168.1.1:8337/notify
.",
- "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in
/metadata/items
&
/metadata/authors
. Backups
do not include any files stored in your library folders.",
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
@@ -621,8 +600,6 @@
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
- "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": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове.
Искате ли да продължите?",
"MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?",
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
@@ -644,7 +621,6 @@
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
"MessageEmbedFinished": "Вграждането завърши!",
"MessageEpisodesQueuedForDownload": "{0} епизод(и) в опашка за изтегляне",
- "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-a ще бъде {0}",
"MessageFetching": "Взимане...",
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
@@ -656,7 +632,6 @@
"MessageListeningSessionsInTheLastYear": "{0} слушателски сесии през последната година",
"MessageLoading": "Зареждане...",
"MessageLoadingFolders": "Зареждане на Папки...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B Провалено!",
"MessageM4BFinished": "M4B Завършено!",
"MessageMapChapterTitles": "Съпостави заглавията на главите със съществуващите глави на аудиокнигата без да променяш времената",
@@ -692,7 +667,6 @@
"MessageNoSeries": "Няма Серии",
"MessageNoTags": "Няма Тагове",
"MessageNoTasksRunning": "Няма вършещи се задачи",
- "MessageNoUpdateNecessary": "Не е необходимо обновяване",
"MessageNoUpdatesWereNecessary": "Не бяха необходими обновления",
"MessageNoUserPlaylists": "Няма плейлисти на потребителя",
"MessageNotYetImplemented": "Още не е изпълнено",
@@ -739,7 +713,6 @@
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
"ToastAccountUpdateFailed": "Неуспешно обновяване на акаунта",
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
- "ToastAuthorImageRemoveFailed": "Неуспешно премахване на авторска снимка",
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
"ToastAuthorUpdateFailed": "Неуспешно обновяване на автора",
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
@@ -752,32 +725,21 @@
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
"ToastBackupUploadFailed": "Неуспешно качване на архив",
"ToastBackupUploadSuccess": "Архивът е качен",
- "ToastBatchUpdateFailed": "Batch update failed",
- "ToastBatchUpdateSuccess": "Batch update success",
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
"ToastBookmarkCreateSuccess": "Отметката е създадена",
- "ToastBookmarkRemoveFailed": "Неуспешно премахване на отметка",
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
"ToastBookmarkUpdateFailed": "Неуспешно обновяване на отметка",
"ToastBookmarkUpdateSuccess": "Отметката е обновена",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Главите имат грешки",
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
- "ToastCollectionItemsRemoveFailed": "Неуспешно премахване на елемент(и) от колекция",
"ToastCollectionItemsRemoveSuccess": "Елемент(и) премахнати от колекция",
- "ToastCollectionRemoveFailed": "Неуспешно премахване на колекция",
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
"ToastCollectionUpdateFailed": "Неуспешно обновяване на колекция",
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Неуспешно обновяване на корица на елемент",
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
"ToastItemDetailsUpdateFailed": "Неуспешно обновяване на детайли на елемент",
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
- "ToastItemDetailsUpdateUnneeded": "Не са необходими обновления на детайлите на елемента",
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като завършено",
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като незавършено",
@@ -792,7 +754,6 @@
"ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена",
"ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист",
"ToastPlaylistCreateSuccess": "Плейлистът е създаден",
- "ToastPlaylistRemoveFailed": "Неуспешно премахване на плейлист",
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
"ToastPlaylistUpdateFailed": "Неуспешно обновяване на плейлист",
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
@@ -806,16 +767,11 @@
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
"ToastSeriesUpdateSuccess": "Серията е обновена",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
"ToastSessionDeleteSuccess": "Сесията е изтрита",
"ToastSocketConnected": "Свързан сокет",
"ToastSocketDisconnected": "Сокетът е прекъснат",
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
"ToastUserDeleteSuccess": "Потребителят е изтрит"
}
diff --git a/client/strings/bn.json b/client/strings/bn.json
index 09db5382fe..d5856c1148 100644
--- a/client/strings/bn.json
+++ b/client/strings/bn.json
@@ -9,7 +9,7 @@
"ButtonApply": "প্রয়োগ করুন",
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
"ButtonAuthors": "লেখক",
- "ButtonBack": "Back",
+ "ButtonBack": "পেছনে যান",
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
"ButtonCancel": "বাতিল করুন",
"ButtonCancelEncode": "এনকোড বাতিল করুন",
@@ -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,6 +50,7 @@
"ButtonNevermind": "কিছু মনে করবেন না",
"ButtonNext": "পরবর্তী",
"ButtonNextChapter": "পরবর্তী অধ্যায়",
+ "ButtonNextItemInQueue": "সারিতে পরের আইটেম",
"ButtonOk": "ঠিক আছে",
"ButtonOpenFeed": "ফিড খুলুন",
"ButtonOpenManager": "ম্যানেজার খুলুন",
@@ -55,15 +60,17 @@
"ButtonPlaylists": "প্লেলিস্ট",
"ButtonPrevious": "পূর্ববর্তী",
"ButtonPreviousChapter": "আগের অধ্যায়",
+ "ButtonProbeAudioFile": "প্রোব অডিও ফাইল",
"ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন",
"ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন",
"ButtonQueueAddItem": "সারিতে যোগ করুন",
"ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন",
+ "ButtonQuickEmbedMetadata": "মেটাডেটা দ্রুত এম্বেড করুন",
"ButtonQuickMatch": "দ্রুত ম্যাচ",
"ButtonReScan": "পুনরায় স্ক্যান",
"ButtonRead": "পড়ুন",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
+ "ButtonReadLess": "সংক্ষিপ্ত",
+ "ButtonReadMore": "বিস্তারিত পড়ুন",
"ButtonRefresh": "রিফ্রেশ",
"ButtonRemove": "মুছে ফেলুন",
"ButtonRemoveAll": "সব মুছে ফেলুন",
@@ -88,8 +95,10 @@
"ButtonShow": "দেখান",
"ButtonStartM4BEncode": "M4B এনকোড শুরু করুন",
"ButtonStartMetadataEmbed": "মেটাডেটা এম্বেড শুরু করুন",
+ "ButtonStats": "পরিসংখ্যান",
"ButtonSubmit": "জমা দিন",
"ButtonTest": "পরীক্ষা",
+ "ButtonUnlinkOpenId": "ওপেন আইডি লিঙ্কমুক্ত করুন",
"ButtonUpload": "আপলোড",
"ButtonUploadBackup": "আপলোড ব্যাকআপ",
"ButtonUploadCover": "কভার আপলোড করুন",
@@ -102,9 +111,10 @@
"ErrorUploadFetchMetadataNoResults": "মেটাডেটা আনা যায়নি - শিরোনাম এবং/অথবা লেখক আপডেট করার চেষ্টা করুন",
"ErrorUploadLacksTitle": "একটি শিরোনাম থাকতে হবে",
"HeaderAccount": "অ্যাকাউন্ট",
+ "HeaderAddCustomMetadataProvider": "কাস্টম মেটাডেটা সরবরাহকারী যোগ করুন",
"HeaderAdvanced": "অ্যাডভান্সড",
"HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন",
- "HeaderAudioTracks": "অডিও ট্র্যাকস",
+ "HeaderAudioTracks": "অডিও ট্র্যাকসগুলো",
"HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস",
"HeaderAuthentication": "প্রমাণীকরণ",
"HeaderBackups": "ব্যাকআপ",
@@ -115,7 +125,7 @@
"HeaderCollectionItems": "সংগ্রহ আইটেম",
"HeaderCover": "কভার",
"HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
+ "HeaderCustomMessageOnLogin": "লগইন এ কাস্টম বার্তা",
"HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী",
"HeaderDetails": "বিস্তারিত",
"HeaderDownloadQueue": "ডাউনলোড সারি",
@@ -147,6 +157,8 @@
"HeaderMetadataToEmbed": "এম্বেড করার জন্য মেটাডেটা",
"HeaderNewAccount": "নতুন অ্যাকাউন্ট",
"HeaderNewLibrary": "নতুন লাইব্রেরি",
+ "HeaderNotificationCreate": "বিজ্ঞপ্তি তৈরি করুন",
+ "HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন",
"HeaderNotifications": "বিজ্ঞপ্তি",
"HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ",
"HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন",
@@ -154,6 +166,7 @@
"HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ",
"HeaderPermissions": "অনুমতি",
"HeaderPlayerQueue": "প্লেয়ার সারি",
+ "HeaderPlayerSettings": "প্লেয়ার সেটিংস",
"HeaderPlaylist": "প্লেলিস্ট",
"HeaderPlaylistItems": "প্লেলিস্ট আইটেম",
"HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট",
@@ -190,9 +203,9 @@
"HeaderYearReview": "বাৎসরিক পর্যালোচনা {0}",
"HeaderYourStats": "আপনার পরিসংখ্যান",
"LabelAbridged": "সংক্ষিপ্ত",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
+ "LabelAbridgedChecked": "সংক্ষিপ্ত (চেক)",
+ "LabelAbridgedUnchecked": "অসংক্ষেপিত (চেক করা হয়নি)",
+ "LabelAccessibleBy": "দ্বারা প্রবেশযোগ্য",
"LabelAccountType": "অ্যাকাউন্টের প্রকার",
"LabelAccountTypeAdmin": "প্রশাসন",
"LabelAccountTypeGuest": "অতিথি",
@@ -202,8 +215,8 @@
"LabelAddToCollectionBatch": "সংগ্রহে {0}টি বই যোগ করুন",
"LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন",
"LabelAddToPlaylistBatch": "প্লেলিস্টে {0}টি আইটেম যোগ করুন",
- "LabelAdded": "যোগ করা হয়েছে",
"LabelAddedAt": "এতে যোগ করা হয়েছে",
+ "LabelAddedDate": "যোগ করা হয়েছে {0}",
"LabelAdminUsersOnly": "শুধু অ্যাডমিন ব্যবহারকারী",
"LabelAll": "সব",
"LabelAllUsers": "সমস্ত ব্যবহারকারী",
@@ -226,14 +239,14 @@
"LabelBackupLocation": "ব্যাকআপ অবস্থান",
"LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন",
"LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত",
- "LabelBackupsMaxBackupSize": "সর্বোচ্চ ব্যাকআপ আকার (GB-তে)",
+ "LabelBackupsMaxBackupSize": "সর্বোচ্চ ব্যাকআপ আকার (GB-তে) (অসীমের জন্য 0)",
"LabelBackupsMaxBackupSizeHelp": "ভুল কনফিগারেশনের বিরুদ্ধে সুরক্ষা হিসেবে ব্যাকআপগুলি ব্যর্থ হবে যদি তারা কনফিগার করা আকার অতিক্রম করে।",
"LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন",
"LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।",
"LabelBitrate": "বিটরেট",
"LabelBooks": "বইগুলো",
"LabelButtonText": "ঘর পাঠ্য",
- "LabelByAuthor": "by {0}",
+ "LabelByAuthor": "দ্বারা {0}",
"LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
"LabelChannels": "চ্যানেল",
"LabelChapterTitle": "অধ্যায়ের শিরোনাম",
@@ -243,6 +256,7 @@
"LabelClosePlayer": "প্লেয়ার বন্ধ করুন",
"LabelCodec": "কোডেক",
"LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন",
+ "LabelCollapseSubSeries": "উপ-সিরিজ সঙ্কুচিত করুন",
"LabelCollection": "সংগ্রহ",
"LabelCollections": "সংগ্রহ",
"LabelComplete": "সম্পূর্ণ",
@@ -258,6 +272,7 @@
"LabelCurrently": "বর্তমানে:",
"LabelCustomCronExpression": "কাস্টম Cron এক্সপ্রেশন:",
"LabelDatetime": "তারিখ সময়",
+ "LabelDays": "দিনগুলো",
"LabelDeleteFromFileSystemCheckbox": "ফাইল সিস্টেম থেকে মুছে ফেলুন (শুধু ডাটাবেস থেকে সরাতে টিক চিহ্ন মুক্ত করুন)",
"LabelDescription": "বিবরণ",
"LabelDeselectAll": "সমস্ত অনির্বাচিত করুন",
@@ -271,35 +286,42 @@
"LabelDownload": "ডাউনলোড করুন",
"LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন",
"LabelDuration": "সময়কাল",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
+ "LabelDurationComparisonExactMatch": "(সঠিক মিল)",
+ "LabelDurationComparisonLonger": "({0} দীর্ঘ)",
+ "LabelDurationComparisonShorter": "({0} ছোট)",
"LabelDurationFound": "সময়কাল পাওয়া গেছে:",
"LabelEbook": "ই-বই",
"LabelEbooks": "ই-বইগুলো",
"LabelEdit": "সম্পাদনা করুন",
"LabelEmail": "ইমেইল",
"LabelEmailSettingsFromAddress": "ঠিকানা থেকে",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to।",
+ "LabelEmailSettingsRejectUnauthorized": "অননুমোদিত সার্টিফিকেট প্রত্যাখ্যান করুন",
+ "LabelEmailSettingsRejectUnauthorizedHelp": "SSL প্রমাণপত্রের বৈধতা নিষ্ক্রিয় করা আপনার সংযোগকে নিরাপত্তা ঝুঁকিতে ফেলতে পারে, যেমন ম্যান-ইন-দ্য-মিডল আক্রমণ। শুধুমাত্র এই বিকল্পটি নিষ্ক্রিয় করুন যদি আপনি এর প্রভাবগুলি বুঝতে পারেন এবং আপনি যে মেইল সার্ভারের সাথে সংযোগ করছেন তাকে বিশ্বাস করেন।",
"LabelEmailSettingsSecure": "নিরাপদ",
"LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)",
"LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা",
"LabelEmbeddedCover": "এম্বেডেড কভার",
"LabelEnable": "সক্ষম করুন",
"LabelEnd": "সমাপ্ত",
+ "LabelEndOfChapter": "অধ্যায়ের সমাপ্তি",
"LabelEpisode": "পর্ব",
"LabelEpisodeTitle": "পর্বের শিরোনাম",
"LabelEpisodeType": "পর্বের ধরন",
+ "LabelEpisodes": "পর্বগুলো",
"LabelExample": "উদাহরণ",
+ "LabelExpandSeries": "সিরিজ প্রসারিত করুন",
+ "LabelExpandSubSeries": "সাব সিরিজ প্রসারিত করুন",
"LabelExplicit": "বিশদ",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
+ "LabelExplicitChecked": "সুস্পষ্ট (পরীক্ষিত)",
+ "LabelExplicitUnchecked": "অস্পষ্ট (অপরিক্ষীত)",
+ "LabelExportOPML": "OPML এক্সপোর্ট করুন",
"LabelFeedURL": "ফিড ইউআরএল",
"LabelFetchingMetadata": "মেটাডেটা আনা হচ্ছে",
"LabelFile": "ফাইল",
"LabelFileBirthtime": "ফাইল জন্মের সময়",
+ "LabelFileBornDate": "জন্ম {0}",
"LabelFileModified": "ফাইল পরিবর্তিত",
+ "LabelFileModifiedDate": "পরিবর্তিত {0}",
"LabelFilename": "ফাইলের নাম",
"LabelFilterByUser": "ব্যবহারকারী দ্বারা ফিল্টারকৃত",
"LabelFindEpisodes": "পর্বগুলো খুঁজুন",
@@ -307,8 +329,8 @@
"LabelFolder": "ফোল্ডার",
"LabelFolders": "ফোল্ডারগুলো",
"LabelFontBold": "বোল্ড",
- "LabelFontBoldness": "Font Boldness",
- "LabelFontFamily": "ফন্ট পরিবার",
+ "LabelFontBoldness": "হরফ বোল্ডনেস",
+ "LabelFontFamily": "হরফ পরিবার",
"LabelFontItalic": "ইটালিক",
"LabelFontScale": "ফন্ট স্কেল",
"LabelFontStrikethrough": "অবচ্ছেদন রেখা",
@@ -318,9 +340,11 @@
"LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন",
"LabelHasEbook": "ই-বই আছে",
"LabelHasSupplementaryEbook": "পরিপূরক ই-বই আছে",
+ "LabelHideSubtitles": "সাবটাইটেল লুকান",
"LabelHighestPriority": "সর্বোচ্চ অগ্রাধিকার",
"LabelHost": "নিমন্ত্রণকর্তা",
"LabelHour": "ঘন্টা",
+ "LabelHours": "ঘন্টা",
"LabelIcon": "আইকন",
"LabelImageURLFromTheWeb": "ওয়েব থেকে ছবির ইউআরএল",
"LabelInProgress": "প্রগতিতে আছে",
@@ -337,9 +361,11 @@
"LabelIntervalEveryHour": "প্রতি ঘন্টা",
"LabelInvert": "উল্টানো",
"LabelItem": "আইটেম",
+ "LabelJumpBackwardAmount": "পিছন দিকে ঝাঁপের পরিমাণ",
+ "LabelJumpForwardAmount": "সামনের দিকে ঝাঁপের পরিমাণ",
"LabelLanguage": "ভাষা",
"LabelLanguageDefaultServer": "সার্ভারের ডিফল্ট ভাষা",
- "LabelLanguages": "Languages",
+ "LabelLanguages": "ভাষাসমূহ",
"LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে",
"LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে",
"LabelLastSeen": "শেষ দেখা",
@@ -351,7 +377,7 @@
"LabelLess": "কম",
"LabelLibrariesAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য লাইব্রেরি",
"LabelLibrary": "লাইব্রেরি",
- "LabelLibraryFilterSublistEmpty": "No {0}",
+ "LabelLibraryFilterSublistEmpty": "না {0}",
"LabelLibraryItem": "লাইব্রেরি আইটেম",
"LabelLibraryName": "লাইব্রেরির নাম",
"LabelLimit": "সীমা",
@@ -371,6 +397,7 @@
"LabelMetadataOrderOfPrecedenceDescription": "উচ্চ অগ্রাধিকারের মেটাডেটার উৎসগুলো নিম্ন অগ্রাধিকারের মেটাডেটা উৎসগুলোকে ওভাররাইড করবে",
"LabelMetadataProvider": "মেটাডেটা প্রদানকারী",
"LabelMinute": "মিনিট",
+ "LabelMinutes": "মিনিটস",
"LabelMissing": "নিখোঁজ",
"LabelMissingEbook": "কোনও ই-বই নেই",
"LabelMissingSupplementaryEbook": "কোনও সম্পূরক ই-বই নেই",
@@ -387,7 +414,7 @@
"LabelNewestEpisodes": "নতুনতম পর্ব",
"LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ",
"LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
+ "LabelNoCustomMetadataProviders": "কোনো কাস্টম মেটাডেটা প্রদানকারী নেই",
"LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি",
"LabelNotFinished": "সমাপ্ত হয়নি",
"LabelNotStarted": "শুরু হয়নি",
@@ -410,6 +437,7 @@
"LabelOverwrite": "পুনঃলিখিত",
"LabelPassword": "পাসওয়ার্ড",
"LabelPath": "পথ",
+ "LabelPermanent": "স্থায়ী",
"LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে",
@@ -420,7 +448,7 @@
"LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})",
"LabelPhotoPathURL": "ছবি পথ/ইউআরএল",
"LabelPlayMethod": "প্লে পদ্ধতি",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
+ "LabelPlayerChapterNumberMarker": "{1} এর মধ্যে {0}",
"LabelPlaylists": "প্লেলিস্ট",
"LabelPodcast": "পডকাস্ট",
"LabelPodcastSearchRegion": "পডকাস্ট অনুসন্ধান অঞ্চল",
@@ -432,16 +460,20 @@
"LabelPrimaryEbook": "প্রাথমিক ই-বই",
"LabelProgress": "প্রগতি",
"LabelProvider": "প্রদানকারী",
+ "LabelProviderAuthorizationValue": "অনুমোদন শিরোনামের মান",
"LabelPubDate": "প্রকাশের তারিখ",
"LabelPublishYear": "প্রকাশের বছর",
+ "LabelPublishedDate": "প্রকাশিত {0}",
"LabelPublisher": "প্রকাশক",
- "LabelPublishers": "Publishers",
+ "LabelPublishers": "প্রকাশকরা",
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
"LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম",
"LabelRSSFeedOpen": "আরএসএস ফিড খুলুন",
"LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন",
"LabelRSSFeedSlug": "আরএসএস ফিড স্লাগ",
"LabelRSSFeedURL": "আরএসএস ফিড ইউআরএল",
+ "LabelRandomly": "এলোমেলোভাবে",
+ "LabelReAddSeriesToContinueListening": "শোনা চালিয়ে যেতে সিরিজ পুনরায় যোগ করুন",
"LabelRead": "পড়ুন",
"LabelReadAgain": "আবার পড়ুন",
"LabelReadEbookWithoutProgress": "প্রগতি না রেখে ই-বই পড়ুন",
@@ -457,7 +489,7 @@
"LabelSearchTitle": "অনুসন্ধান শিরোনাম",
"LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN",
"LabelSeason": "সেশন",
- "LabelSelectAll": "Select all",
+ "LabelSelectAll": "সব নির্বাচন করুন",
"LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন",
"LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন",
"LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন",
@@ -480,8 +512,8 @@
"LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files।",
+ "LabelSettingsEpubsAllowScriptedContent": "ইপাবে স্ক্রিপ্ট করা বিষয়বস্তুর অনুমতি দিন",
+ "LabelSettingsEpubsAllowScriptedContentHelp": "ইপাব ফাইলগুলিকে স্ক্রিপ্ট চালানোর অনুমতি দিন। আপনি ইপাব ফাইলগুলির উৎসকে বিশ্বাস না করলে এই সেটিংটি নিষ্ক্রিয় রাখার সুপারিশ করা হলো।",
"LabelSettingsExperimentalFeatures": "পরীক্ষামূলক বৈশিষ্ট্য",
"LabelSettingsExperimentalFeaturesHelp": "ফিচারের বৈশিষ্ট্য যা আপনার প্রতিক্রিয়া ব্যবহার করতে পারে এবং পরীক্ষায় সহায়তা করতে পারে। গিটহাব আলোচনা খুলতে ক্লিক করুন।",
"LabelSettingsFindCovers": "কভার খুঁজুন",
@@ -491,7 +523,7 @@
"LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান",
- "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের হোম পেজ শেল্ফ প্রথম বইটি দেখায় যেটি সিরিজে শুরু হয়নি যেটিতে অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করা হলে তা শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চালিয়ে যাবে।",
+ "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের নীড় পেজ শেল্ফ দেখায় যে সিরিজে শুরু হয়নি এমন প্রথম বই যার অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করলে শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চলতে থাকবে।",
"LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন",
"LabelSettingsParseSubtitlesHelp": "অডিওবুক ফোল্ডারের নাম থেকে সাবটাইটেল বের করুন৷
সাবটাইটেল অবশ্যই \" - \"
অর্থাৎ \"বুকের শিরোনাম - এখানে একটি সাবটাইটেল\" এর সাবটাইটেল আছে \"এখানে একটি সাবটাইটেল\"",
"LabelSettingsPreferMatchedMetadata": "মিলিত মেটাডেটা পছন্দ করুন",
@@ -507,8 +539,12 @@
"LabelSettingsStoreMetadataWithItem": "আইটেমের সাথে মেটাডেটা সংরক্ষণ করুন",
"LabelSettingsStoreMetadataWithItemHelp": "ডিফল্টরূপে মেটাডেটা ফাইলগুলি /মেটাডাটা/আইটেমগুলি -এ সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে মেটাডেটা ফাইলগুলি আপনার লাইব্রেরি আইটেম ফোল্ডারে সংরক্ষণ করা হবে",
"LabelSettingsTimeFormat": "সময় বিন্যাস",
+ "LabelShare": "শেয়ার করুন",
+ "LabelShareOpen": "শেয়ার খোলা",
+ "LabelShareURL": "শেয়ার ইউআরএল",
"LabelShowAll": "সব দেখান",
- "LabelShowSeconds": "Show seconds",
+ "LabelShowSeconds": "সেকেন্ড দেখান",
+ "LabelShowSubtitles": "সহ-শিরোনাম দেখান",
"LabelSize": "আকার",
"LabelSleepTimer": "স্লিপ টাইমার",
"LabelSlug": "স্লাগ",
@@ -546,6 +582,10 @@
"LabelThemeDark": "অন্ধকার",
"LabelThemeLight": "আলো",
"LabelTimeBase": "সময় বেস",
+ "LabelTimeDurationXHours": "{0} ঘণ্টা",
+ "LabelTimeDurationXMinutes": "{0} মিনিট",
+ "LabelTimeDurationXSeconds": "{0} সেকেন্ড",
+ "LabelTimeInMinutes": "মিনিটে সময়",
"LabelTimeListened": "সময় শোনা হয়েছে",
"LabelTimeListenedToday": "আজ শোনার সময়",
"LabelTimeRemaining": "{0}টি অবশিষ্ট",
@@ -569,6 +609,7 @@
"LabelUnabridged": "অসংলগ্ন",
"LabelUndo": "পূর্বাবস্থা",
"LabelUnknown": "অজানা",
+ "LabelUnknownPublishDate": "প্রকাশের তারিখ অজানা",
"LabelUpdateCover": "কভার আপডেট করুন",
"LabelUpdateCoverHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান কভারগুলি ওভাররাইট করার অনুমতি দিন",
"LabelUpdateDetails": "বিশদ আপডেট করুন",
@@ -585,9 +626,12 @@
"LabelVersion": "সংস্করণ",
"LabelViewBookmarks": "বুকমার্ক দেখুন",
"LabelViewChapters": "অধ্যায় দেখুন",
+ "LabelViewPlayerSettings": "প্লেয়ার সেটিংস দেখুন",
"LabelViewQueue": "প্লেয়ার সারি দেখুন",
"LabelVolume": "ভলিউম",
"LabelWeekdaysToRun": "চলতে হবে সপ্তাহের দিন",
+ "LabelXBooks": "{0}টি বই",
+ "LabelXItems": "{0}টি আইটেম",
"LabelYearReviewHide": "পর্যালোচনার বছর লুকান",
"LabelYearReviewShow": "পর্যালোচনার বছর দেখুন",
"LabelYourAudiobookDuration": "আপনার অডিওবুকের সময়কাল",
@@ -595,13 +639,16 @@
"LabelYourPlaylists": "আপনার প্লেলিস্ট",
"LabelYourProgress": "আপনার অগ্রগতি",
"MessageAddToPlayerQueue": "প্লেয়ার সারিতে যোগ করুন",
- "MessageAppriseDescription": "এই বৈশিষ্ট্যটি ব্যবহার করার জন্য আপনাকে
Apprise API-এর একটি উদাহরণ থাকতে হবে a> চলমান বা একটি এপিআই যা সেই একই অনুরোধগুলি পরিচালনা করবে৷ বিজ্ঞপ্তি পাঠানোর জন্য Apprise API Url সম্পূর্ণ URL পাথ হওয়া উচিত, যেমন, যদি আপনার API উদাহরণ http://192.168 এ পরিবেশিত হয়৷ 1.1:8337
তারপর আপনি http://192.168.1.1:8337/notify
লিখবেন।",
+ "MessageAppriseDescription": "এই বৈশিষ্ট্যটি ব্যবহার করার জন্য আপনাকে Apprise API চালানোর একটি উদাহরণ বা একটি এপিআই পরিচালনা করতে হবে যে একই অনুরোধ পরিচালনা করবে।
অ্যাপ্রাইজ এপিআই ইউআরএলটি বিজ্ঞপ্তি পাঠানোর জন্য সম্পূর্ণ ইউআরএল পথ হওয়া উচিত, যেমন, যদি আপনার API ইনস্ট্যান্স
http://192.168.1.1:8337
এ পরিবেশিত হয় তাহলে আপনি
রাখবেন >http://192.168.1.1:8337/notify
।",
"MessageBackupsDescription": "ব্যাকআপের মধ্যে রয়েছে ব্যবহারকারী, ব্যবহারকারীর অগ্রগতি, লাইব্রেরি আইটেমের বিবরণ, সার্ভার সেটিংস এবং
/metadata/items
&
/metadata/authors
-এ সংরক্ষিত ছবি। ব্যাকআপগুলি
আপনার লাইব্রেরি ফোল্ডারে সঞ্চিত কোনো ফাইল >অন্তর্ভুক্ত করবেন না ।",
+ "MessageBackupsLocationEditNote": "দ্রষ্টব্য: ব্যাকআপ অবস্থান আপডেট করলে বিদ্যমান ব্যাকআপগুলি সরানো বা সংশোধন করা হবে না",
+ "MessageBackupsLocationNoEditNote": "দ্রষ্টব্য: ব্যাকআপ অবস্থান একটি পরিবেশ পরিবর্তনশীল মাধ্যমে স্থির করা হয়েছে এবং এখানে পরিবর্তন করা যাবে না।",
+ "MessageBackupsLocationPathEmpty": "ব্যাকআপ অবস্থানের পথ খালি থাকতে পারবে না",
"MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।",
"MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি",
"MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই",
"MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই",
- "MessageBookshelfNoResultsForQuery": "No results for query",
+ "MessageBookshelfNoResultsForQuery": "প্রশ্নের জন্য কোন ফলাফল নেই",
"MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই",
"MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে",
"MessageChapterErrorFirstNotZero": "প্রথম অধ্যায় 0 এ শুরু হতে হবে",
@@ -611,18 +658,24 @@
"MessageCheckingCron": "ক্রন পরীক্ষা করা হচ্ছে...",
"MessageConfirmCloseFeed": "আপনি কি নিশ্চিত যে আপনি এই ফিডটি বন্ধ করতে চান?",
"MessageConfirmDeleteBackup": "আপনি কি নিশ্চিত যে আপনি {0} এর ব্যাকআপ মুছে ফেলতে চান?",
+ "MessageConfirmDeleteDevice": "আপনি কি নিশ্চিতভাবে ই-রিডার ডিভাইস \"{0}\" মুছতে চান?",
"MessageConfirmDeleteFile": "এটি আপনার ফাইল সিস্টেম থেকে ফাইলটি মুছে দেবে। আপনি কি নিশ্চিত?",
"MessageConfirmDeleteLibrary": "আপনি কি নিশ্চিত যে আপনি স্থায়ীভাবে লাইব্রেরি \"{0}\" মুছে ফেলতে চান?",
"MessageConfirmDeleteLibraryItem": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে লাইব্রেরি আইটেমটি মুছে ফেলবে। আপনি কি নিশ্চিত?",
"MessageConfirmDeleteLibraryItems": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে {0}টি লাইব্রেরি আইটেম মুছে ফেলবে। আপনি কি নিশ্চিত?",
+ "MessageConfirmDeleteMetadataProvider": "আপনি কি নিশ্চিতভাবে কাস্টম মেটাডেটা প্রদানকারী \"{0}\" মুছতে চান?",
+ "MessageConfirmDeleteNotification": "আপনি কি নিশ্চিতভাবে এই বিজ্ঞপ্তিটি মুছতে চান?",
"MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?",
"MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?",
"MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
"MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
+ "MessageConfirmMarkItemFinished": "আপনি কি \"{0}\" কে সমাপ্ত হিসাবে চিহ্নিত করার বিষয়ে নিশ্চিত?",
+ "MessageConfirmMarkItemNotFinished": "আপনি কি \"{0}\" শেষ হয়নি বলে চিহ্নিত করার বিষয়ে নিশ্চিত?",
"MessageConfirmMarkSeriesFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
"MessageConfirmMarkSeriesNotFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
- "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?",
+ "MessageConfirmNotificationTestTrigger": "পরীক্ষার তথ্য দিয়ে এই বিজ্ঞপ্তিটি ট্রিগার করবেন?",
+ "MessageConfirmPurgeCache": "ক্যাশে পরিষ্কারক
/metadata/cache
-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে।
আপনি কি নিশ্চিত আপনি ক্যাশে ডিরেক্টরি সরাতে চান?",
+ "MessageConfirmPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কারক
/metadata/cache/items
-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে।
আপনি কি নিশ্চিত?",
"MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে।
আপনি কি চালিয়ে যেতে চান?",
"MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?",
"MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?",
@@ -639,12 +692,15 @@
"MessageConfirmRenameTag": "আপনি কি সব আইটেমের জন্য \"{0}\" ট্যাগের নাম পরিবর্তন করে \"{1}\" করার বিষয়ে নিশ্চিত?",
"MessageConfirmRenameTagMergeNote": "দ্রষ্টব্য: এই ট্যাগটি আগে থেকেই বিদ্যমান তাই সেগুলিকে একত্র করা হবে।",
"MessageConfirmRenameTagWarning": "সতর্কতা! একটি ভিন্ন কেসিং সহ একটি অনুরূপ ট্যাগ ইতিমধ্যেই বিদ্যমান \"{0}\"।",
+ "MessageConfirmResetProgress": "আপনি কি আপনার অগ্রগতি রিসেট করার বিষয়ে নিশ্চিত?",
"MessageConfirmSendEbookToDevice": "আপনি কি নিশ্চিত যে আপনি \"{2}\" ডিভাইসে {0} ইবুক \"{1}\" পাঠাতে চান?",
+ "MessageConfirmUnlinkOpenId": "আপনি কি এই ব্যবহারকারীকে ওপেনআইডি থেকে লিঙ্কমুক্ত করার বিষয়ে নিশ্চিত?",
"MessageDownloadingEpisode": "ডাউনলোডিং পর্ব",
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
+ "MessageEmbedFailed": "এম্বেড ব্যর্থ হয়েছে!",
"MessageEmbedFinished": "এম্বেড করা শেষ!",
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
- "MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below।",
+ "MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।",
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
"MessageFetching": "আনয় হচ্ছে...",
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
@@ -656,7 +712,7 @@
"MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন",
"MessageLoading": "লোড হচ্ছে...",
"MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
।",
+ "MessageLogsDescription": "লগগুলি JSON ফাইল হিসাবে
/metadata/logs
-এ সংরক্ষণ করা হয়। ক্র্যাশ লগগুলি
/metadata/logs/crash_logs.txt
-এ সংরক্ষণ করা হয়।",
"MessageM4BFailed": "M4B ব্যর্থ!",
"MessageM4BFinished": "M4B সমাপ্ত!",
"MessageMapChapterTitles": "টাইমস্ট্যাম্প সামঞ্জস্য না করে আপনার বিদ্যমান অডিওবুক অধ্যায়গুলিতে অধ্যায়ের শিরোনাম ম্যাপ করুন",
@@ -673,6 +729,7 @@
"MessageNoCollections": "কোন সংগ্রহ নেই",
"MessageNoCoversFound": "কোন কভার পাওয়া যায়নি",
"MessageNoDescription": "কোন বর্ণনা নেই",
+ "MessageNoDevices": "কোনো ডিভাইস নেই",
"MessageNoDownloadsInProgress": "বর্তমানে কোনো ডাউনলোড চলছে না",
"MessageNoDownloadsQueued": "কোনও ডাউনলোড সারি নেই",
"MessageNoEpisodeMatchesFound": "কোন পর্বের মিল পাওয়া যায়নি",
@@ -692,14 +749,15 @@
"MessageNoSeries": "কোন সিরিজ নেই",
"MessageNoTags": "কোন ট্যাগ নেই",
"MessageNoTasksRunning": "কোন টাস্ক চলছে না",
- "MessageNoUpdateNecessary": "কোন আপডেটের প্রয়োজন নেই",
"MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না",
"MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই",
"MessageNotYetImplemented": "এখনও বাস্তবায়িত হয়নি",
+ "MessageOpmlPreviewNote": "দ্রষ্টব্য: এটি পার্স করা OPML ফাইলের একটি পূর্বরূপ। প্রকৃত পডকাস্ট শিরোনাম RSS ফিড থেকে নেওয়া হবে।",
"MessageOr": "বা",
"MessagePauseChapter": "পজ অধ্যায় প্লেব্যাক",
"MessagePlayChapter": "অধ্যায়ের শুরুতে শুনুন",
"MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন",
+ "MessagePleaseWait": "অনুগ্রহ করে অপেক্ষা করুন..।",
"MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই",
"MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।",
"MessageRemoveChapter": "অধ্যায় সরান",
@@ -714,6 +772,9 @@
"MessageSelected": "{0}টি নির্বাচিত",
"MessageServerCouldNotBeReached": "সার্ভারে পৌঁছানো যায়নি",
"MessageSetChaptersFromTracksDescription": "প্রতিটি অডিও ফাইলকে অধ্যায় হিসেবে ব্যবহার করে অধ্যায় সেট করুন এবং অডিও ফাইলের নাম হিসেবে অধ্যায়ের শিরোনাম করুন",
+ "MessageShareExpirationWillBe": "মেয়াদ শেষ হবে
{0} ",
+ "MessageShareExpiresIn": "মেয়াদ শেষ হবে {0}",
+ "MessageShareURLWillBe": "শেয়ার করা ইউআরএল হবে
{0} ",
"MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?",
"MessageThinking": "চিন্তা করছি...",
"MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ",
@@ -737,51 +798,99 @@
"PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম",
"PlaceholderSearch": "অনুসন্ধান..",
"PlaceholderSearchEpisode": "অনুসন্ধান পর্ব..",
+ "StatsAuthorsAdded": "লেখক যোগ করা হয়েছে",
+ "StatsBooksAdded": "বই যোগ করা হয়েছে",
+ "StatsBooksAdditional": "কিছু সংযোজনের মধ্যে রয়েছে…",
+ "StatsBooksFinished": "বই সমাপ্ত",
+ "StatsBooksFinishedThisYear": "এ বছর শেষ হওয়া কিছু বই …",
+ "StatsBooksListenedTo": "বই শোনা হয়েছে",
+ "StatsCollectionGrewTo": "আপনার বইয়ের সংগ্রহ বেড়েছে…",
+ "StatsSessions": "অধিবেশনসমূহ",
+ "StatsSpentListening": "শুনে কাটিয়েছেন",
+ "StatsTopAuthor": "শীর্ষস্থানীয় লেখক",
+ "StatsTopAuthors": "শীর্ষস্থানীয় লেখকগণ",
+ "StatsTopGenre": "শীর্ষ ঘরানা",
+ "StatsTopGenres": "শীর্ষ ঘরানাগুলো",
+ "StatsTopMonth": "সেরা মাস",
+ "StatsTopNarrator": "শীর্ষ কথক",
+ "StatsTopNarrators": "শীর্ষ কথকগণ",
+ "StatsTotalDuration": "মোট সময়কাল…",
+ "StatsYearInReview": "বাৎসরিক পর্যালোচনা",
"ToastAccountUpdateFailed": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
"ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে",
- "ToastAuthorImageRemoveFailed": "ছবি সরাতে ব্যর্থ",
+ "ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে",
"ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে",
+ "ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি",
+ "ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে",
+ "ToastAuthorSearchNotFound": "লেখক পাওয়া যায়নি",
"ToastAuthorUpdateFailed": "লেখক আপডেট করতে ব্যর্থ",
"ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে",
"ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন",
"ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)",
+ "ToastBackupAppliedSuccess": "ব্যাকআপ প্রয়োগ করা হয়েছে",
"ToastBackupCreateFailed": "ব্যাকআপ তৈরি করতে ব্যর্থ",
"ToastBackupCreateSuccess": "ব্যাকআপ তৈরি করা হয়েছে",
"ToastBackupDeleteFailed": "ব্যাকআপ মুছে ফেলতে ব্যর্থ",
"ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে",
+ "ToastBackupInvalidMaxKeep": "রাখার জন্য অকার্যকর ব্যাকআপের সংখ্যা",
+ "ToastBackupInvalidMaxSize": "অকার্যকর সর্বোচ্চ ব্যাকআপ আকার",
+ "ToastBackupPathUpdateFailed": "ব্যাকআপ পথ আপডেট করতে ব্যর্থ হয়েছে",
"ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ",
"ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ",
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
+ "ToastBatchDeleteFailed": "ব্যাচ মুছে ফেলতে ব্যর্থ হয়েছে",
+ "ToastBatchDeleteSuccess": "ব্যাচ মুছে ফেলা সফল হয়েছে",
"ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে",
"ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য",
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
- "ToastBookmarkRemoveFailed": "বুকমার্ক সরাতে ব্যর্থ",
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
"ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ",
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
+ "ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
+ "ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
"ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে",
- "ToastCollectionItemsRemoveFailed": "সংগ্রহ থেকে আইটেম(গুলি) সরাতে ব্যর্থ",
+ "ToastChaptersRemoved": "অধ্যায়গুলো মুছে ফেলা হয়েছে",
+ "ToastCollectionItemsAddFailed": "আইটেম(গুলি) সংগ্রহে যোগ করা ব্যর্থ হয়েছে",
+ "ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে",
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
- "ToastCollectionRemoveFailed": "সংগ্রহ সরাতে ব্যর্থ",
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
"ToastCollectionUpdateFailed": "সংগ্রহ আপডেট করতে ব্যর্থ",
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
+ "ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে",
+ "ToastDeleteFileFailed": "ফাইল মুছে ফেলতে ব্যর্থ হয়েছে",
+ "ToastDeleteFileSuccess": "ফাইল মুছে ফেলা হয়েছে",
+ "ToastDeviceAddFailed": "ডিভাইস যোগ করতে ব্যর্থ হয়েছে",
+ "ToastDeviceNameAlreadyExists": "এই নামের ইরিডার ডিভাইস ইতিমধ্যেই বিদ্যমান",
+ "ToastDeviceTestEmailFailed": "পরীক্ষামূলক ইমেল পাঠাতে ব্যর্থ হয়েছে",
+ "ToastDeviceTestEmailSuccess": "পরীক্ষামূলক ইমেল পাঠানো হয়েছে",
+ "ToastDeviceUpdateFailed": "ডিভাইস আপডেট করতে ব্যর্থ হয়েছে",
+ "ToastEmailSettingsUpdateFailed": "ইমেল সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
+ "ToastEmailSettingsUpdateSuccess": "ইমেল সেটিংস আপডেট করা হয়েছে",
+ "ToastEncodeCancelFailed": "এনকোড বাতিল করতে ব্যর্থ হয়েছে",
+ "ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে",
+ "ToastEpisodeDownloadQueueClearFailed": "সারি সাফ করতে ব্যর্থ হয়েছে",
+ "ToastEpisodeDownloadQueueClearSuccess": "পর্ব ডাউনলোড সারি পরিষ্কার করা হয়েছে",
+ "ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
+ "ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
+ "ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
+ "ToastFailedToUpdateAccount": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
+ "ToastFailedToUpdateUser": "ব্যবহারকারী আপডেট করতে ব্যর্থ",
+ "ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
+ "ToastInvalidUrl": "অকার্যকর ইউআরএল",
"ToastItemCoverUpdateFailed": "আইটেম কভার আপডেট করতে ব্যর্থ হয়েছে",
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
+ "ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ",
+ "ToastItemDeletedSuccess": "মুছে ফেলা আইটেম",
"ToastItemDetailsUpdateFailed": "আইটেমের বিবরণ আপডেট করতে ব্যর্থ",
"ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে",
- "ToastItemDetailsUpdateUnneeded": "আইটেমের বিবরণের জন্য কোন আপডেটের প্রয়োজন নেই",
"ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত",
"ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত",
+ "ToastItemUpdateFailed": "আইটেম আপডেট করতে ব্যর্থ",
+ "ToastItemUpdateSuccess": "আইটেম আপডেট করা হয়েছে",
"ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ",
"ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে",
"ToastLibraryDeleteFailed": "লাইব্রেরি মুছে ফেলতে ব্যর্থ",
@@ -790,32 +899,78 @@
"ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে",
"ToastLibraryUpdateFailed": "লাইব্রেরি আপডেট করতে ব্যর্থ",
"ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে",
+ "ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক",
+ "ToastNameRequired": "নাম আবশ্যক",
+ "ToastNewUserCreatedFailed": "অ্যাকাউন্ট তৈরি করতে ব্যর্থ: \"{0}\"",
+ "ToastNewUserCreatedSuccess": "নতুন একাউন্ট তৈরি হয়েছে",
+ "ToastNewUserLibraryError": "অন্তত একটি লাইব্রেরি নির্বাচন করতে হবে",
+ "ToastNewUserPasswordError": "অন্তত একটি পাসওয়ার্ড থাকতে হবে, শুধুমাত্র রুট ব্যবহারকারীর একটি খালি পাসওয়ার্ড থাকতে পারে",
+ "ToastNewUserTagError": "অন্তত একটি ট্যাগ নির্বাচন করতে হবে",
+ "ToastNewUserUsernameError": "একটি ব্যবহারকারীর নাম লিখুন",
+ "ToastNoUpdatesNecessary": "কোন আপডেটের প্রয়োজন নেই",
+ "ToastNotificationCreateFailed": "বিজ্ঞপ্তি তৈরি করতে ব্যর্থ",
+ "ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ",
+ "ToastNotificationFailedMaximum": "সর্বাধিক ব্যর্থ প্রচেষ্টা >= 0 হতে হবে",
+ "ToastNotificationQueueMaximum": "সর্বাধিক বিজ্ঞপ্তি সারি >= 0 হতে হবে",
+ "ToastNotificationSettingsUpdateFailed": "বিজ্ঞপ্তি সেটিংস আপডেট করতে ব্যর্থ",
+ "ToastNotificationSettingsUpdateSuccess": "বিজ্ঞপ্তি সেটিংস আপডেট করা হয়েছে",
+ "ToastNotificationTestTriggerFailed": "পরীক্ষামূলক বিজ্ঞপ্তি ট্রিগার করতে ব্যর্থ হয়েছে",
+ "ToastNotificationTestTriggerSuccess": "পরীক্ষামুলক বিজ্ঞপ্তি ট্রিগার হয়েছে",
+ "ToastNotificationUpdateFailed": "বিজ্ঞপ্তি আপডেট করতে ব্যর্থ",
+ "ToastNotificationUpdateSuccess": "বিজ্ঞপ্তি আপডেট হয়েছে",
"ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ",
"ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে",
- "ToastPlaylistRemoveFailed": "প্লেলিস্ট সরাতে ব্যর্থ",
"ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে",
"ToastPlaylistUpdateFailed": "প্লেলিস্ট আপডেট করতে ব্যর্থ",
"ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে",
"ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে",
+ "ToastPodcastGetFeedFailed": "পডকাস্ট ফিড পেতে ব্যর্থ হয়েছে",
+ "ToastPodcastNoEpisodesInFeed": "আরএসএস ফিডে কোনো পর্ব পাওয়া যায়নি",
+ "ToastPodcastNoRssFeed": "পডকাস্টের কোন আরএসএস ফিড নেই",
+ "ToastProviderCreatedFailed": "প্রদানকারী যোগ করতে ব্যর্থ হয়েছে",
+ "ToastProviderCreatedSuccess": "নতুন প্রদানকারী যোগ করা হয়েছে",
+ "ToastProviderNameAndUrlRequired": "নাম এবং ইউআরএল আবশ্যক",
+ "ToastProviderRemoveSuccess": "প্রদানকারী সরানো হয়েছে",
"ToastRSSFeedCloseFailed": "RSS ফিড বন্ধ করতে ব্যর্থ",
"ToastRSSFeedCloseSuccess": "RSS ফিড বন্ধ",
+ "ToastRemoveFailed": "মুছে ফেলতে ব্যর্থ হয়েছে",
"ToastRemoveItemFromCollectionFailed": "সংগ্রহ থেকে আইটেম সরাতে ব্যর্থ",
"ToastRemoveItemFromCollectionSuccess": "সংগ্রহ থেকে আইটেম সরানো হয়েছে",
+ "ToastRemoveItemsWithIssuesFailed": "সমস্যাযুক্ত লাইব্রেরি আইটেমগুলি সরাতে ব্যর্থ হয়েছে",
+ "ToastRemoveItemsWithIssuesSuccess": "সমস্যাযুক্ত লাইব্রেরি আইটেম সরানো হয়েছে",
+ "ToastRenameFailed": "পুনঃনামকরণ ব্যর্থ হয়েছে",
+ "ToastRescanFailed": "{0} এর জন্য পুনরায় স্ক্যান করা ব্যর্থ হয়েছে",
+ "ToastRescanRemoved": "পুনরায় স্ক্যান সম্পূর্ণ,আইটেম সরানো হয়েছে",
+ "ToastRescanUpToDate": "পুনরায় স্ক্যান সম্পূর্ণ, আইটেম সাম্প্রতিক ছিল",
+ "ToastRescanUpdated": "পুনরায় স্ক্যান সম্পূর্ণ, আইটেম আপডেট করা হয়েছে",
+ "ToastScanFailed": "লাইব্রেরি আইটেম স্ক্যান করতে ব্যর্থ হয়েছে",
+ "ToastSelectAtLeastOneUser": "অন্তত একজন ব্যবহারকারী নির্বাচন করুন",
"ToastSendEbookToDeviceFailed": "ডিভাইসে ইবুক পাঠাতে ব্যর্থ",
"ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে",
"ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে",
"ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
+ "ToastServerSettingsUpdateFailed": "সার্ভার সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
+ "ToastServerSettingsUpdateSuccess": "সার্ভার সেটিংস আপডেট করা হয়েছে",
+ "ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে",
"ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ",
"ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে",
+ "ToastSlugMustChange": "স্লাগে অবৈধ অক্ষর রয়েছে",
+ "ToastSlugRequired": "স্লাগ আবশ্যক",
"ToastSocketConnected": "সকেট সংযুক্ত",
"ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন",
"ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
+ "ToastSortingPrefixesEmptyError": "কমপক্ষে ১ টি সাজানোর উপসর্গ থাকতে হবে",
+ "ToastSortingPrefixesUpdateFailed": "বাছাই উপসর্গ আপডেট করতে ব্যর্থ হয়েছে",
+ "ToastSortingPrefixesUpdateSuccess": "বাছাই করা উপসর্গ আপডেট করা হয়েছে ({0}টি আইটেম)",
+ "ToastTitleRequired": "শিরোনাম আবশ্যক",
+ "ToastUnknownError": "অজানা ত্রুটি",
+ "ToastUnlinkOpenIdFailed": "OpenID থেকে ব্যবহারকারীকে আনলিঙ্ক করতে ব্যর্থ হয়েছে",
+ "ToastUnlinkOpenIdSuccess": "OpenID থেকে ব্যবহারকারীকে লিঙ্কমুক্ত করা হয়েছে",
"ToastUserDeleteFailed": "ব্যবহারকারী মুছতে ব্যর্থ",
- "ToastUserDeleteSuccess": "ব্যবহারকারী মুছে ফেলা হয়েছে"
+ "ToastUserDeleteSuccess": "ব্যবহারকারী মুছে ফেলা হয়েছে",
+ "ToastUserPasswordChangeSuccess": "পাসওয়ার্ড সফলভাবে পরিবর্তন করা হয়েছে",
+ "ToastUserPasswordMismatch": "পাসওয়ার্ড মিলছে না",
+ "ToastUserPasswordMustChange": "নতুন পাসওয়ার্ড পুরানো পাসওয়ার্ডের সাথে মিলতে পারবে না",
+ "ToastUserRootRequireName": "একটি রুট ব্যবহারকারীর নাম লিখতে হবে"
}
diff --git a/client/strings/cs.json b/client/strings/cs.json
index 3ffb08df1c..1f6237fa01 100644
--- a/client/strings/cs.json
+++ b/client/strings/cs.json
@@ -44,12 +44,9 @@
"ButtonMatchAllAuthors": "Spárovat všechny autory",
"ButtonMatchBooks": "Spárovat Knihy",
"ButtonNevermind": "Nevadí",
- "ButtonNext": "Next",
"ButtonNextChapter": "Další Kapitola",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Otevřít kanál",
"ButtonOpenManager": "Otevřít správce",
- "ButtonPause": "Pause",
"ButtonPlay": "Přehrát",
"ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb",
@@ -59,6 +56,7 @@
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
"ButtonQueueAddItem": "Přidat do fronty",
"ButtonQueueRemoveItem": "Odstranit z fronty",
+ "ButtonQuickEmbedMetadata": "Rychle Zapsat Metadata",
"ButtonQuickMatch": "Rychlé přiřazení",
"ButtonReScan": "Znovu prohledat",
"ButtonRead": "Číst",
@@ -88,8 +86,8 @@
"ButtonShow": "Zobrazit",
"ButtonStartM4BEncode": "Spustit kódování M4B",
"ButtonStartMetadataEmbed": "Spustit vkládání metadat",
+ "ButtonStats": "Statistiky",
"ButtonSubmit": "Odeslat",
- "ButtonTest": "Test",
"ButtonUpload": "Nahrát",
"ButtonUploadBackup": "Nahrát zálohu",
"ButtonUploadCover": "Nahrát obálku",
@@ -106,7 +104,6 @@
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
"HeaderAudioTracks": "Zvukové stopy",
"HeaderAudiobookTools": "Nástroje pro správu souborů audioknih",
- "HeaderAuthentication": "Authentication",
"HeaderBackups": "Zálohy",
"HeaderChangePassword": "Změnit heslo",
"HeaderChapters": "Kapitoly",
@@ -151,9 +148,9 @@
"HeaderOpenIDConnectAuthentication": "Ověřování pomocí OpenID Connect",
"HeaderOpenRSSFeed": "Otevřít RSS kanál",
"HeaderOtherFiles": "Ostatní soubory",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Oprávnění",
"HeaderPlayerQueue": "Fronta přehrávače",
+ "HeaderPlayerSettings": "Nastavení přehrávače",
"HeaderPlaylist": "Seznam skladeb",
"HeaderPlaylistItems": "Položky seznamu přehrávání",
"HeaderPodcastsToAdd": "Podcasty k přidání",
@@ -202,7 +199,6 @@
"LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce",
"LabelAddToPlaylist": "Přidat do seznamu přehrávání",
"LabelAddToPlaylistBatch": "Přidat {0} položky do seznamu přehrávání",
- "LabelAdded": "Přidáno",
"LabelAddedAt": "Přidáno v",
"LabelAdminUsersOnly": "Pouze administrátoři",
"LabelAll": "Vše",
@@ -226,14 +222,13 @@
"LabelBackupLocation": "Umístění zálohy",
"LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování",
"LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups",
- "LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB)",
+ "LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB) (0 bez omezení)",
"LabelBackupsMaxBackupSizeHelp": "Ochrana proti chybné konfiguraci: Zálohování se nezdaří, pokud překročí nastavenou velikost.",
"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",
"LabelBooks": "Knihy",
"LabelButtonText": "Text tlačítka",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Změnit heslo",
"LabelChannels": "Kanály",
"LabelChapterTitle": "Název kapitoly",
@@ -258,6 +253,7 @@
"LabelCurrently": "Aktuálně:",
"LabelCustomCronExpression": "Vlastní výraz cronu:",
"LabelDatetime": "Datum a čas",
+ "LabelDays": "Dny",
"LabelDeleteFromFileSystemCheckbox": "Smazat ze souborového systému (zrušte zaškrtnutí pro odstranění pouze z databáze)",
"LabelDescription": "Popis",
"LabelDeselectAll": "Odznačit vše",
@@ -288,10 +284,12 @@
"LabelEmbeddedCover": "Vložená obálka",
"LabelEnable": "Povolit",
"LabelEnd": "Konec",
+ "LabelEndOfChapter": "Konec kapitoly",
"LabelEpisode": "Epizoda",
"LabelEpisodeTitle": "Název epizody",
"LabelEpisodeType": "Typ epizody",
"LabelExample": "Příklad",
+ "LabelExpandSeries": "Rozbalit série",
"LabelExplicit": "Explicitní",
"LabelExplicitChecked": "Explicitní (zaškrtnuto)",
"LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)",
@@ -309,7 +307,6 @@
"LabelFontBold": "Tučně",
"LabelFontBoldness": "Výraznost písma",
"LabelFontFamily": "Rodina písem",
- "LabelFontItalic": "Italic",
"LabelFontScale": "Měřítko písma",
"LabelFontStrikethrough": "Přeškrtnutí",
"LabelFormat": "Formát",
@@ -318,15 +315,16 @@
"LabelHardDeleteFile": "Trvale smazat soubor",
"LabelHasEbook": "Obsahuje elektronickou knihu",
"LabelHasSupplementaryEbook": "Obsahuje doplňkovou elektronickou knihu",
+ "LabelHideSubtitles": "Skrýt titulky",
"LabelHighestPriority": "Nejvyšší priorita",
"LabelHost": "Hostitel",
"LabelHour": "Hodina",
+ "LabelHours": "Hodiny",
"LabelIcon": "Ikona",
"LabelImageURLFromTheWeb": "URL obrázku z webu",
"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",
@@ -337,6 +335,8 @@
"LabelIntervalEveryHour": "Každou hodinu",
"LabelInvert": "Invertovat",
"LabelItem": "Položka",
+ "LabelJumpBackwardAmount": "Přeskočit zpět o",
+ "LabelJumpForwardAmount": "Přeskočit dopředu o",
"LabelLanguage": "Jazyk",
"LabelLanguageDefaultServer": "Výchozí jazyk serveru",
"LabelLanguages": "Jazyky",
@@ -351,7 +351,6 @@
"LabelLess": "Méně",
"LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli",
"LabelLibrary": "Knihovna",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Položka knihovny",
"LabelLibraryName": "Název knihovny",
"LabelLimit": "Omezit",
@@ -371,6 +370,7 @@
"LabelMetadataOrderOfPrecedenceDescription": "Zdroje metadat s vyšší prioritou budou mít přednost před zdroji metadat s nižší prioritou.",
"LabelMetadataProvider": "Poskytovatel metadat",
"LabelMinute": "Minuta",
+ "LabelMinutes": "Minuty",
"LabelMissing": "Chybějící",
"LabelMissingEbook": "Nemá elektronickou knihu",
"LabelMissingSupplementaryEbook": "Nemá žádnou doplňkovou elektronickou knihu",
@@ -410,6 +410,7 @@
"LabelOverwrite": "Přepsat",
"LabelPassword": "Heslo",
"LabelPath": "Cesta",
+ "LabelPermanent": "Trvalé",
"LabelPermissionsAccessAllLibraries": "Má přístup ke všem knihovnám",
"LabelPermissionsAccessAllTags": "Má přístup ke všem značkám",
"LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu",
@@ -420,13 +421,10 @@
"LabelPersonalYearReview": "Váš přehled roku ({0})",
"LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlayMethod": "Metoda přehrávání",
- "LabelPlayerChapterNumberMarker": "{0} of {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",
@@ -442,6 +440,7 @@
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
"LabelRSSFeedSlug": "RSS kanál Slug",
"LabelRSSFeedURL": "URL RSS kanálu",
+ "LabelRandomly": "Náhodně",
"LabelRead": "Číst",
"LabelReadAgain": "Číst znovu",
"LabelReadEbookWithoutProgress": "Číst e-knihu bez zachování průběhu",
@@ -449,7 +448,6 @@
"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",
@@ -507,11 +505,13 @@
"LabelSettingsStoreMetadataWithItem": "Uložit metadata s položkou",
"LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny",
"LabelSettingsTimeFormat": "Formát času",
+ "LabelShare": "Sdílet",
+ "LabelShareURL": "Sdílet URL",
"LabelShowAll": "Zobrazit vše",
"LabelShowSeconds": "Zobrazit sekundy",
+ "LabelShowSubtitles": "Zobrazit titulky",
"LabelSize": "Velikost",
"LabelSleepTimer": "Časovač vypnutí",
- "LabelSlug": "Slug",
"LabelStart": "Spustit",
"LabelStartTime": "Čas Spuštění",
"LabelStarted": "Spuštěno",
@@ -539,13 +539,16 @@
"LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli",
"LabelTasks": "Spuštěné Úlohy",
"LabelTextEditorBulletedList": "Seznam s odrážkami",
- "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Seznam s čísly",
"LabelTextEditorUnlink": "Zrušit odkaz",
"LabelTheme": "Téma",
"LabelThemeDark": "Tmavé",
"LabelThemeLight": "Světlé",
"LabelTimeBase": "Časová základna",
+ "LabelTimeDurationXHours": "{0} hodin",
+ "LabelTimeDurationXMinutes": "{0} minut",
+ "LabelTimeDurationXSeconds": "{0} sekund",
+ "LabelTimeInMinutes": "Čas v minutách",
"LabelTimeListened": "Čas poslechu",
"LabelTimeListenedToday": "Čas poslechu dnes",
"LabelTimeRemaining": "{0} zbývá",
@@ -585,9 +588,12 @@
"LabelVersion": "Verze",
"LabelViewBookmarks": "Zobrazit záložky",
"LabelViewChapters": "Zobrazit kapitoly",
+ "LabelViewPlayerSettings": "Zobrazit nastavení přehrávače",
"LabelViewQueue": "Zobrazit frontu přehrávače",
"LabelVolume": "Hlasitost",
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
+ "LabelXBooks": "{0} knih",
+ "LabelXItems": "{0} položky",
"LabelYearReviewHide": "Skrýt rok v přehledu",
"LabelYearReviewShow": "Zobrazit rok v přehledu",
"LabelYourAudiobookDuration": "Doba trvání vaší audioknihy",
@@ -597,6 +603,9 @@
"MessageAddToPlayerQueue": "Přidat do fronty přehrávače",
"MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci
Apprise API nebo API, které bude zpracovávat stejné požadavky.
Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese
http://192.168.1.1:8337
pak byste měli zadat
http://192.168.1.1:8337/notify
.",
"MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v
/metadata/items
a
/metadata/authors
. Zálohy
ne zahrnují všechny soubory uložené ve složkách knihovny.",
+ "MessageBackupsLocationEditNote": "Poznámka: Změna umístění záloh nepřesune ani nezmění existující zálohy",
+ "MessageBackupsLocationNoEditNote": "Poznámka: Umístění záloh je nastavené z proměnných prostředí a nelze zde změnit.",
+ "MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné",
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
"MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály",
@@ -642,8 +651,9 @@
"MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?",
"MessageDownloadingEpisode": "Stahuji epizodu",
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
+ "MessageEmbedFailed": "Vložení selhalo!",
"MessageEmbedFinished": "Vložení dokončeno!",
- "MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení",
+ "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í...",
@@ -692,10 +702,10 @@
"MessageNoSeries": "Žádné série",
"MessageNoTags": "Žádné značky",
"MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy",
- "MessageNoUpdateNecessary": "Není nutná žádná aktualizace",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessageNotYetImplemented": "Ještě není implementováno",
+ "MessageOpmlPreviewNote": "Poznámka: Toto je náhled načteného OMPL souboru. Aktuální název podcastu bude načten z RSS feedu.",
"MessageOr": "nebo",
"MessagePauseChapter": "Pozastavit přehrávání kapitoly",
"MessagePlayChapter": "Poslechnout si začátek kapitoly",
@@ -714,6 +724,9 @@
"MessageSelected": "{0} vybráno",
"MessageServerCouldNotBeReached": "Server je nedostupný",
"MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru",
+ "MessageShareExpirationWillBe": "Expiruje
{0} ",
+ "MessageShareExpiresIn": "Expiruje za {0}",
+ "MessageShareURLWillBe": "Sdílené URL bude
{0} ",
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
"MessageThinking": "Přemýšlení...",
"MessageUploaderItemFailed": "Nahrávání se nezdařilo",
@@ -737,9 +750,21 @@
"PlaceholderNewPlaylist": "Nový název seznamu přehrávání",
"PlaceholderSearch": "Hledat..",
"PlaceholderSearchEpisode": "Hledat epizodu..",
+ "StatsAuthorsAdded": "autoři přidáni",
+ "StatsBooksAdded": "knihy přidány",
+ "StatsBooksFinished": "dokončené knihy",
+ "StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…",
+ "StatsSessions": "sezení",
+ "StatsSpentListening": "stráveno posloucháním",
+ "StatsTopAuthor": "TOP AUTOR",
+ "StatsTopAuthors": "TOP AUTOŘI",
+ "StatsTopGenre": "TOP ŽÁNR",
+ "StatsTopGenres": "TOP ŽÁNRY",
+ "StatsTopMonth": "TOP MĚSÍC",
+ "StatsTotalDuration": "S celkovou dobou…",
+ "StatsYearInReview": "ROK V PŘEHLEDU",
"ToastAccountUpdateFailed": "Aktualizace účtu se nezdařila",
"ToastAccountUpdateSuccess": "Účet aktualizován",
- "ToastAuthorImageRemoveFailed": "Nepodařilo se odstranit obrázek",
"ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn",
"ToastAuthorUpdateFailed": "Aktualizace autora se nezdařila",
"ToastAuthorUpdateMerged": "Autor sloučen",
@@ -756,7 +781,6 @@
"ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně",
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
"ToastBookmarkCreateSuccess": "Přidána záložka",
- "ToastBookmarkRemoveFailed": "Nepodařilo se odstranit záložku",
"ToastBookmarkRemoveSuccess": "Záložka odstraněna",
"ToastBookmarkUpdateFailed": "Aktualizace záložky se nezdařila",
"ToastBookmarkUpdateSuccess": "Záložka aktualizována",
@@ -764,20 +788,18 @@
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
- "ToastCollectionItemsRemoveFailed": "Nepodařilo se odstranit položky z kolekce",
"ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce",
- "ToastCollectionRemoveFailed": "Nepodařilo se odstranit kolekci",
"ToastCollectionRemoveSuccess": "Kolekce odstraněna",
"ToastCollectionUpdateFailed": "Aktualizace kolekce se nezdařila",
"ToastCollectionUpdateSuccess": "Kolekce aktualizována",
"ToastDeleteFileFailed": "Nepodařilo se smazat soubor",
"ToastDeleteFileSuccess": "Soubor smazán",
+ "ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
"ToastFailedToLoadData": "Nepodařilo se načíst data",
"ToastItemCoverUpdateFailed": "Aktualizace obálky se nezdařila",
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
"ToastItemDetailsUpdateFailed": "Nepodařilo se aktualizovat podrobnosti o položce",
"ToastItemDetailsUpdateSuccess": "Podrobnosti o položce byly aktualizovány",
- "ToastItemDetailsUpdateUnneeded": "Podrobnosti o položce nejsou potřeba aktualizovat",
"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é",
@@ -792,7 +814,6 @@
"ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována",
"ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo",
"ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen",
- "ToastPlaylistRemoveFailed": "Nepodařilo se odstranit seznamu přehrávání",
"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",
diff --git a/client/strings/da.json b/client/strings/da.json
index 9dfbd8f240..2e055b42a3 100644
--- a/client/strings/da.json
+++ b/client/strings/da.json
@@ -1,10 +1,7 @@
{
"ButtonAdd": "Tilføj",
"ButtonAddChapters": "Tilføj kapitler",
- "ButtonAddDevice": "Add Device",
- "ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "Tilføj podcasts",
- "ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Tilføj din første bibliotek",
"ButtonApply": "Anvend",
"ButtonApplyChapters": "Anvend kapitler",
@@ -33,8 +30,6 @@
"ButtonHide": "Skjul",
"ButtonHome": "Hjem",
"ButtonIssues": "Problemer",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Seneste",
"ButtonLibrary": "Bibliotek",
"ButtonLogout": "Log ud",
@@ -44,17 +39,12 @@
"ButtonMatchAllAuthors": "Match alle forfattere",
"ButtonMatchBooks": "Match bøger",
"ButtonNevermind": "Glem det",
- "ButtonNext": "Next",
- "ButtonNextChapter": "Next Chapter",
"ButtonOk": "OK",
"ButtonOpenFeed": "Åbn feed",
"ButtonOpenManager": "Åbn manager",
- "ButtonPause": "Pause",
"ButtonPlay": "Afspil",
"ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister",
- "ButtonPrevious": "Previous",
- "ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Ryd al cache",
"ButtonPurgeItemsCache": "Ryd elementcache",
"ButtonQueueAddItem": "Tilføj til kø",
@@ -62,9 +52,6 @@
"ButtonQuickMatch": "Hurtig Match",
"ButtonReScan": "Gen-scan",
"ButtonRead": "Læs",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
"ButtonRemove": "Fjern",
"ButtonRemoveAll": "Fjern Alle",
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
@@ -72,41 +59,31 @@
"ButtonRemoveFromContinueReading": "Fjern fra Fortsæt Læsning",
"ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra Fortsæt Serie",
"ButtonReset": "Nulstil",
- "ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Gendan",
"ButtonSave": "Gem",
"ButtonSaveAndClose": "Gem & Luk",
"ButtonSaveTracklist": "Gem Sporliste",
- "ButtonScan": "Scan",
"ButtonScanLibrary": "Scan Bibliotek",
"ButtonSearch": "Søg",
"ButtonSelectFolderPath": "Vælg Mappen Sti",
"ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
- "ButtonShare": "Share",
"ButtonShiftTimes": "Skift Tider",
"ButtonShow": "Vis",
"ButtonStartM4BEncode": "Start M4B Kode",
"ButtonStartMetadataEmbed": "Start Metadata Indlejring",
"ButtonSubmit": "Send",
- "ButtonTest": "Test",
- "ButtonUpload": "Upload",
- "ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Omslag",
"ButtonUploadOPMLFile": "Upload OPML Fil",
"ButtonUserDelete": "Slet bruger {0}",
"ButtonUserEdit": "Rediger bruger {0}",
"ButtonViewAll": "Vis Alle",
"ButtonYes": "Ja",
- "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Avanceret",
"HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
"HeaderAudioTracks": "Lydspor",
"HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer",
- "HeaderAuthentication": "Authentication",
"HeaderBackups": "Sikkerhedskopier",
"HeaderChangePassword": "Skift Adgangskode",
"HeaderChapters": "Kapitler",
@@ -115,12 +92,9 @@
"HeaderCollectionItems": "Samlingselementer",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Nuværende Downloads",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Download Kø",
"HeaderEbookFiles": "E-bogsfiler",
- "HeaderEmail": "Email",
"HeaderEmailSettings": "Email Indstillinger",
"HeaderEpisodes": "Episoder",
"HeaderEreaderDevices": "E-læser Enheder",
@@ -138,20 +112,15 @@
"HeaderListeningSessions": "Lyttesessioner",
"HeaderListeningStats": "Lyttestatistik",
"HeaderLogin": "Log ind",
- "HeaderLogs": "Logs",
"HeaderManageGenres": "Administrer Genrer",
"HeaderManageTags": "Administrer Tags",
"HeaderMapDetails": "Kort Detaljer",
- "HeaderMatch": "Match",
- "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metadata til indlejring",
"HeaderNewAccount": "Ny Konto",
"HeaderNewLibrary": "Nyt Bibliotek",
"HeaderNotifications": "Meddelelser",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Åbn RSS Feed",
"HeaderOtherFiles": "Andre Filer",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Tilladelser",
"HeaderPlayerQueue": "Afspilningskø",
"HeaderPlaylist": "Afspilningsliste",
@@ -160,19 +129,16 @@
"HeaderPreviewCover": "Forhåndsvis Omslag",
"HeaderRSSFeedGeneral": "RSS Detaljer",
"HeaderRSSFeedIsOpen": "RSS Feed er Åben",
- "HeaderRSSFeeds": "RSS Feeds",
"HeaderRemoveEpisode": "Fjern Episode",
"HeaderRemoveEpisodes": "Fjern {0} Episoder",
"HeaderSavedMediaProgress": "Gemt Medieforløb",
"HeaderSchedule": "Planlæg",
"HeaderScheduleLibraryScans": "Planlæg Automatiske Biblioteksscanninger",
- "HeaderSession": "Session",
"HeaderSetBackupSchedule": "Indstil Sikkerhedskopieringsplan",
"HeaderSettings": "Indstillinger",
"HeaderSettingsDisplay": "Skærm",
"HeaderSettingsExperimental": "Eksperimentelle Funktioner",
"HeaderSettingsGeneral": "Generelt",
- "HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Søvntimer",
"HeaderStatsLargestItems": "Største Elementer",
"HeaderStatsLongestItems": "Længste Elementer (timer)",
@@ -187,12 +153,7 @@
"HeaderUpdateDetails": "Opdater Detaljer",
"HeaderUpdateLibrary": "Opdater Bibliotek",
"HeaderUsers": "Brugere",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Dine Statistikker",
- "LabelAbridged": "Abridged",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Kontotype",
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gæst",
@@ -202,13 +163,9 @@
"LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste",
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
- "LabelAdded": "Tilføjet",
"LabelAddedAt": "Tilføjet Kl.",
- "LabelAdminUsersOnly": "Admin users only",
"LabelAll": "Alle",
"LabelAllUsers": "Alle Brugere",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Allerede i dit bibliotek",
"LabelAppend": "Tilføj",
"LabelAuthor": "Forfatter",
@@ -216,12 +173,6 @@
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
"LabelAuthors": "Forfattere",
"LabelAutoDownloadEpisodes": "Auto Download Episoder",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Tilbage til Bruger",
"LabelBackupLocation": "Backup Placering",
"LabelBackupsEnableAutomaticBackups": "Aktivér automatisk sikkerhedskopiering",
@@ -230,18 +181,13 @@
"LabelBackupsMaxBackupSizeHelp": "Som en beskyttelse mod fejlkonfiguration fejler sikkerhedskopier, hvis de overstiger den konfigurerede størrelse.",
"LabelBackupsNumberToKeep": "Antal sikkerhedskopier at beholde",
"LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.",
- "LabelBitrate": "Bitrate",
"LabelBooks": "Bøger",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Ændre Adgangskode",
"LabelChannels": "Kanaler",
"LabelChapterTitle": "Kapitel Titel",
"LabelChapters": "Kapitler",
"LabelChaptersFound": "fundne kapitler",
- "LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Luk afspiller",
- "LabelCodec": "Codec",
"LabelCollapseSeries": "Fold Serie Sammen",
"LabelCollection": "Samling",
"LabelCollections": "Samlinger",
@@ -258,45 +204,31 @@
"LabelCurrently": "Aktuelt:",
"LabelCustomCronExpression": "Brugerdefineret Cron Udtryk:",
"LabelDatetime": "Dato og Tid",
- "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Beskrivelse",
"LabelDeselectAll": "Fravælg Alle",
"LabelDevice": "Enheds",
"LabelDeviceInfo": "Enhedsinformation",
- "LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Mappe",
"LabelDiscFromFilename": "Disk fra Filnavn",
"LabelDiscFromMetadata": "Disk fra Metadata",
"LabelDiscover": "Opdag",
- "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episoder",
"LabelDuration": "Varighed",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Fundet varighed:",
"LabelEbook": "E-bog",
"LabelEbooks": "E-bøger",
"LabelEdit": "Rediger",
- "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Fra Adresse",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "Sikker",
"LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Adresse",
"LabelEmbeddedCover": "Indlejret Omslag",
"LabelEnable": "Aktivér",
"LabelEnd": "Slut",
- "LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episodetitel",
"LabelEpisodeType": "Episodetype",
"LabelExample": "Eksempel",
"LabelExplicit": "Eksplisit",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
- "LabelFeedURL": "Feed URL",
- "LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Fil",
"LabelFileBirthtime": "Fødselstidspunkt for fil",
"LabelFileModified": "Fil ændret",
@@ -306,27 +238,18 @@
"LabelFinished": "Færdig",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
- "LabelFontBold": "Bold",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Fontfamilie",
- "LabelFontItalic": "Italic",
"LabelFontScale": "Skriftstørrelse",
- "LabelFontStrikethrough": "Strikethrough",
- "LabelFormat": "Format",
- "LabelGenre": "Genre",
"LabelGenres": "Genrer",
"LabelHardDeleteFile": "Permanent slet fil",
"LabelHasEbook": "Har e-bog",
"LabelHasSupplementaryEbook": "Har supplerende e-bog",
- "LabelHighestPriority": "Highest priority",
"LabelHost": "Vært",
"LabelHour": "Time",
"LabelIcon": "Ikon",
- "LabelImageURLFromTheWeb": "Image URL from the web",
"LabelInProgress": "I gang",
"LabelIncludeInTracklist": "Inkluder i afspilningsliste",
"LabelIncomplete": "Ufuldstændig",
- "LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Tilpasset dagligt/ugentligt",
"LabelIntervalEvery12Hours": "Hver 12. time",
"LabelIntervalEvery15Minutes": "Hver 15. minut",
@@ -339,19 +262,16 @@
"LabelItem": "Element",
"LabelLanguage": "Sprog",
"LabelLanguageDefaultServer": "Standard server sprog",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "Senest tilføjede bog",
"LabelLastBookUpdated": "Senest opdaterede bog",
"LabelLastSeen": "Sidst set",
"LabelLastTime": "Sidste gang",
"LabelLastUpdate": "Seneste opdatering",
- "LabelLayout": "Layout",
"LabelLayoutSinglePage": "Enkeltside",
"LabelLayoutSplitPage": "Opdelt side",
"LabelLess": "Mindre",
"LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger",
"LabelLibrary": "Bibliotek",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Bibliotekselement",
"LabelLibraryName": "Biblioteksnavn",
"LabelLimit": "Grænse",
@@ -361,21 +281,13 @@
"LabelLogLevelInfo": "Information",
"LabelLogLevelWarn": "Advarsel",
"LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Medieafspiller",
"LabelMediaType": "Medietype",
"LabelMetaTag": "Meta-tag",
"LabelMetaTags": "Meta-tags",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadataudbyder",
"LabelMinute": "Minut",
"LabelMissing": "Mangler",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
"LabelMore": "Mere",
"LabelMoreInfo": "Mere info",
"LabelName": "Navn",
@@ -387,7 +299,6 @@
"LabelNewestEpisodes": "Nyeste episoder",
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
"LabelNextScheduledRun": "Næste planlagte kørsel",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Ingen episoder valgt",
"LabelNotFinished": "Ikke færdig",
"LabelNotStarted": "Ikke påbegyndt",
@@ -403,9 +314,6 @@
"LabelNotificationsMaxQueueSizeHelp": "Hændelser begrænses til at udløse en gang pr. sekund. Hændelser ignoreres, hvis køen er fyldt. Dette forhindrer meddelelsesspam.",
"LabelNumberOfBooks": "Antal bøger",
"LabelNumberOfEpisodes": "Antal episoder",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Åbn RSS-feed",
"LabelOverwrite": "Overskriv",
"LabelPassword": "Kodeord",
@@ -417,16 +325,11 @@
"LabelPermissionsDownload": "Kan downloade",
"LabelPermissionsUpdate": "Kan opdatere",
"LabelPermissionsUpload": "Kan uploade",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto sti/URL",
"LabelPlayMethod": "Afspilningsmetode",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Afspilningslister",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcast søgeområde",
"LabelPodcastType": "Podcast type",
- "LabelPodcasts": "Podcasts",
- "LabelPort": "Port",
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
"LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger",
"LabelPrimaryEbook": "Primær e-bog",
@@ -435,7 +338,6 @@
"LabelPubDate": "Udgivelsesdato",
"LabelPublishYear": "Udgivelsesår",
"LabelPublisher": "Forlag",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "Åben RSS-feed",
@@ -448,25 +350,19 @@
"LabelRecentSeries": "Seneste serie",
"LabelRecentlyAdded": "Senest tilføjet",
"LabelRecommended": "Anbefalet",
- "LabelRedo": "Redo",
- "LabelRegion": "Region",
"LabelReleaseDate": "Udgivelsesdato",
"LabelRemoveCover": "Fjern omslag",
- "LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Søgeterm",
"LabelSearchTitle": "Søg efter titel",
"LabelSearchTitleOrASIN": "Søg efter titel eller ASIN",
"LabelSeason": "Sæson",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Vælg alle episoder",
"LabelSelectEpisodesShowing": "Vælg {0} episoder vist",
- "LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send e-bog til...",
"LabelSequence": "Sekvens",
"LabelSeries": "Serie",
"LabelSeriesName": "Serienavn",
"LabelSeriesProgress": "Seriefremskridt",
- "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Indstil som primær",
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
@@ -480,8 +376,6 @@
"LabelSettingsEnableWatcher": "Aktiver overvågning",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner",
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.",
"LabelSettingsFindCovers": "Find omslag",
@@ -490,8 +384,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.",
"LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside",
"LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket",
- "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": "Fortolk undertekster",
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.
Undertitler skal adskilles af \" - \"
f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
@@ -508,11 +400,8 @@
"LabelSettingsStoreMetadataWithItemHelp": "Som standard gemmes metadatafiler i /metadata/items, aktivering af denne indstilling vil gemme metadatafiler i dine bibliotekselementmapper",
"LabelSettingsTimeFormat": "Tidsformat",
"LabelShowAll": "Vis alle",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Størrelse",
"LabelSleepTimer": "Søvntimer",
- "LabelSlug": "Slug",
- "LabelStart": "Start",
"LabelStartTime": "Starttid",
"LabelStarted": "Startet",
"LabelStartedAt": "Startet klokken",
@@ -538,10 +427,6 @@
"LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger",
"LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger",
"LabelTasks": "Kører opgaver",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Tema",
"LabelThemeDark": "Mørk",
"LabelThemeLight": "Lys",
@@ -565,9 +450,7 @@
"LabelTracksMultiTrack": "Flerspors",
"LabelTracksNone": "Ingen spor",
"LabelTracksSingleTrack": "Enkeltspors",
- "LabelType": "Type",
"LabelUnabridged": "Uforkortet",
- "LabelUndo": "Undo",
"LabelUnknown": "Ukendt",
"LabelUpdateCover": "Opdater omslag",
"LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match",
@@ -576,20 +459,16 @@
"LabelUpdatedAt": "Opdateret ved",
"LabelUploaderDragAndDrop": "Træk og slip filer eller mapper",
"LabelUploaderDropFiles": "Smid filer",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Brug kapitel-spor",
"LabelUseFullTrack": "Brug fuldt spor",
"LabelUser": "Bruger",
"LabelUsername": "Brugernavn",
"LabelValue": "Værdi",
- "LabelVersion": "Version",
"LabelViewBookmarks": "Se bogmærker",
"LabelViewChapters": "Se kapitler",
"LabelViewQueue": "Se afspilningskø",
"LabelVolume": "Volumen",
"LabelWeekdaysToRun": "Ugedage til kørsel",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Din lydbogsvarighed",
"LabelYourBookmarks": "Dine bogmærker",
"LabelYourPlaylists": "Dine spillelister",
@@ -601,7 +480,6 @@
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Du har ingen serier",
"MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog",
"MessageChapterErrorFirstNotZero": "Første kapitel skal starte ved 0",
@@ -613,24 +491,17 @@
"MessageConfirmDeleteBackup": "Er du sikker på, at du vil slette backup for {0}?",
"MessageConfirmDeleteFile": "Dette vil slette filen fra dit filsystem. Er du sikker?",
"MessageConfirmDeleteLibrary": "Er du sikker på, at du vil slette biblioteket permanent \"{0}\"?",
- "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Er du sikker på, at du vil slette denne session?",
"MessageConfirmForceReScan": "Er du sikker på, at du vil tvinge en genindlæsning?",
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på, at du vil markere alle episoder som afsluttet?",
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på, at du vil markere alle episoder som ikke afsluttet?",
"MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?",
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?",
- "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?",
- "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?",
"MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?",
"MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?",
"MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?",
- "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Er du sikker på, at du vil fjerne fortælleren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Er du sikker på, at du vil fjerne din spilleliste \"{0}\"?",
"MessageConfirmRenameGenre": "Er du sikker på, at du vil omdøbe genre \"{0}\" til \"{1}\" for alle elementer?",
@@ -644,7 +515,6 @@
"MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden",
"MessageEmbedFinished": "Indlejring færdig!",
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til 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 vil være {0}",
"MessageFetching": "Henter...",
"MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.",
@@ -656,7 +526,6 @@
"MessageListeningSessionsInTheLastYear": "{0} lyttesessioner i det sidste år",
"MessageLoading": "Indlæser...",
"MessageLoadingFolders": "Indlæser mapper...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B mislykkedes!",
"MessageM4BFinished": "M4B afsluttet!",
"MessageMapChapterTitles": "Tilknyt kapiteloverskrifter til dine eksisterende lydbogskapitler uden at justere tidsstempler",
@@ -692,7 +561,6 @@
"MessageNoSeries": "Ingen serier",
"MessageNoTags": "Ingen tags",
"MessageNoTasksRunning": "Ingen opgaver kører",
- "MessageNoUpdateNecessary": "Ingen opdatering nødvendig",
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
"MessageNotYetImplemented": "Endnu ikke implementeret",
@@ -711,7 +579,6 @@
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den",
"MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.
Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.
Alle klienter, der bruger din server, opdateres automatisk.",
"MessageSearchResultsFor": "Søgeresultater for",
- "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
"MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn",
"MessageStartPlaybackAtTime": "Start afspilning for \"{0}\" kl. {1}?",
@@ -739,7 +606,6 @@
"PlaceholderSearchEpisode": "Søg efter episode..",
"ToastAccountUpdateFailed": "Mislykkedes opdatering af konto",
"ToastAccountUpdateSuccess": "Konto opdateret",
- "ToastAuthorImageRemoveFailed": "Mislykkedes fjernelse af forfatterbillede",
"ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet",
"ToastAuthorUpdateFailed": "Mislykkedes opdatering af forfatter",
"ToastAuthorUpdateMerged": "Forfatter fusioneret",
@@ -756,28 +622,19 @@
"ToastBatchUpdateSuccess": "Batchopdatering lykkedes",
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
"ToastBookmarkCreateSuccess": "Bogmærke tilføjet",
- "ToastBookmarkRemoveFailed": "Mislykkedes fjernelse af bogmærke",
"ToastBookmarkRemoveSuccess": "Bogmærke fjernet",
"ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke",
"ToastBookmarkUpdateSuccess": "Bogmærke opdateret",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Kapitler har fejl",
"ToastChaptersMustHaveTitles": "Kapitler skal have titler",
- "ToastCollectionItemsRemoveFailed": "Mislykkedes fjernelse af element(er) fra samlingen",
"ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen",
- "ToastCollectionRemoveFailed": "Mislykkedes fjernelse af samling",
"ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateFailed": "Mislykkedes opdatering af samling",
"ToastCollectionUpdateSuccess": "Samling opdateret",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Mislykkedes opdatering af varens omslag",
"ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
"ToastItemDetailsUpdateFailed": "Mislykkedes opdatering af varedetaljer",
"ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
- "ToastItemDetailsUpdateUnneeded": "Ingen opdateringer er nødvendige for varedetaljer",
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
"ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
"ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet",
@@ -792,7 +649,6 @@
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret",
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
"ToastPlaylistCreateSuccess": "Afspilningsliste oprettet",
- "ToastPlaylistRemoveFailed": "Mislykkedes fjernelse af afspilningsliste",
"ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet",
"ToastPlaylistUpdateFailed": "Mislykkedes opdatering af afspilningsliste",
"ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret",
@@ -806,16 +662,11 @@
"ToastSendEbookToDeviceSuccess": "E-bog afsendt til enhed \"{0}\"",
"ToastSeriesUpdateFailed": "Mislykkedes opdatering af serie",
"ToastSeriesUpdateSuccess": "Serieopdatering lykkedes",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Mislykkedes sletning af session",
"ToastSessionDeleteSuccess": "Session slettet",
"ToastSocketConnected": "Socket forbundet",
"ToastSocketDisconnected": "Socket afbrudt",
"ToastSocketFailedToConnect": "Socket kunne ikke oprettes",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Mislykkedes sletning af bruger",
"ToastUserDeleteSuccess": "Bruger slettet"
}
diff --git a/client/strings/de.json b/client/strings/de.json
index 749a0973d8..4a7e6ce9ce 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Wähle eine Datei",
"ButtonClearFilter": "Filter löschen",
"ButtonCloseFeed": "Feed schließen",
+ "ButtonCloseSession": "Offene Session schließen",
"ButtonCollections": "Sammlungen",
"ButtonConfigureScanner": "Scannereinstellungen",
"ButtonCreate": "Erstellen",
@@ -28,6 +29,9 @@
"ButtonEdit": "Bearbeiten",
"ButtonEditChapters": "Kapitel bearbeiten",
"ButtonEditPodcast": "Podcast bearbeiten",
+ "ButtonEnable": "Aktivieren",
+ "ButtonFireAndFail": "Abfeuern und versagen",
+ "ButtonFireOnTest": "Test-Event abfeuern",
"ButtonForceReScan": "Komplett-Scan (alle Medien)",
"ButtonFullPath": "Vollständiger Pfad",
"ButtonHide": "Ausblenden",
@@ -46,15 +50,17 @@
"ButtonNevermind": "Abbrechen",
"ButtonNext": "Vor",
"ButtonNextChapter": "Nächstes Kapitel",
+ "ButtonNextItemInQueue": "Das nächste Element in der Warteschlange",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Feed öffnen",
"ButtonOpenManager": "Manager öffnen",
- "ButtonPause": "Pause",
+ "ButtonPause": "Pausieren",
"ButtonPlay": "Abspielen",
"ButtonPlaying": "Spielt",
"ButtonPlaylists": "Wiedergabelisten",
"ButtonPrevious": "Zurück",
"ButtonPreviousChapter": "Vorheriges Kapitel",
+ "ButtonProbeAudioFile": "Audiodatei untersuchen",
"ButtonPurgeAllCache": "Cache leeren",
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
@@ -84,7 +90,7 @@
"ButtonSelectFolderPath": "Ordnerpfad auswählen",
"ButtonSeries": "Serien",
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
- "ButtonShare": "Teilen",
+ "ButtonShare": "Freigeben",
"ButtonShiftTimes": "Zeitverschiebung",
"ButtonShow": "Anzeigen",
"ButtonStartM4BEncode": "M4B-Kodierung starten",
@@ -92,6 +98,7 @@
"ButtonStats": "Statistiken",
"ButtonSubmit": "Ok",
"ButtonTest": "Test",
+ "ButtonUnlinkOpenId": "OpenID trennen",
"ButtonUpload": "Hochladen",
"ButtonUploadBackup": "Sicherung hochladen",
"ButtonUploadCover": "Titelbild hochladen",
@@ -104,6 +111,7 @@
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und oder den Autor zu aktualisieren",
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
"HeaderAccount": "Konto",
+ "HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen",
"HeaderAdvanced": "Erweitert",
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
"HeaderAudioTracks": "Audiodateien",
@@ -149,6 +157,8 @@
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
"HeaderNewAccount": "Neues Konto",
"HeaderNewLibrary": "Neue Bibliothek",
+ "HeaderNotificationCreate": "Benachrichtigung erstellen",
+ "HeaderNotificationUpdate": "Benachrichtigung updaten",
"HeaderNotifications": "Benachrichtigungen",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
@@ -205,8 +215,8 @@
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
- "LabelAdded": "Hinzugefügt",
"LabelAddedAt": "Hinzugefügt am",
+ "LabelAddedDate": "Hinzugefügt {0}",
"LabelAdminUsersOnly": "Nur Admin Benutzer",
"LabelAll": "Alle",
"LabelAllUsers": "Alle Benutzer",
@@ -245,7 +255,8 @@
"LabelClickForMoreInfo": "Klicken für mehr Informationen",
"LabelClosePlayer": "Player schließen",
"LabelCodec": "Codec",
- "LabelCollapseSeries": "Serien zusammenfassen",
+ "LabelCollapseSeries": "Serien einklappen",
+ "LabelCollapseSubSeries": "Unterserien einklappen",
"LabelCollection": "Sammlung",
"LabelCollections": "Sammlungen",
"LabelComplete": "Vollständig",
@@ -296,8 +307,10 @@
"LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episodentitel",
"LabelEpisodeType": "Episodentyp",
+ "LabelEpisodes": "Episoden",
"LabelExample": "Beispiel",
- "LabelExpandSeries": "Serie erweitern",
+ "LabelExpandSeries": "Serie ausklappen",
+ "LabelExpandSubSeries": "Unterserie ausklappen",
"LabelExplicit": "Explizit (Altersbeschränkung)",
"LabelExplicitChecked": "Explicit (Altersbeschränkung) (angehakt)",
"LabelExplicitUnchecked": "Not Explicit (Altersbeschränkung) (nicht angehakt)",
@@ -306,7 +319,9 @@
"LabelFetchingMetadata": "Abholen der Metadaten",
"LabelFile": "Datei",
"LabelFileBirthtime": "Datei erstellt",
+ "LabelFileBornDate": "Geboren {0}",
"LabelFileModified": "Datei geändert",
+ "LabelFileModifiedDate": "Geändert {0}",
"LabelFilename": "Dateiname",
"LabelFilterByUser": "Nach Benutzern filtern",
"LabelFindEpisodes": "Episoden suchen",
@@ -445,8 +460,10 @@
"LabelPrimaryEbook": "Primäres E-Book",
"LabelProgress": "Fortschritt",
"LabelProvider": "Anbieter",
+ "LabelProviderAuthorizationValue": "Autorisierungsheader-Wert",
"LabelPubDate": "Veröffentlichungsdatum",
"LabelPublishYear": "Jahr",
+ "LabelPublishedDate": "Veröffentlicht {0}",
"LabelPublisher": "Herausgeber",
"LabelPublishers": "Herausgeber",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
@@ -522,9 +539,9 @@
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
"LabelSettingsTimeFormat": "Zeitformat",
- "LabelShare": "Teilen",
- "LabelShareOpen": "Teilen öffnen",
- "LabelShareURL": "URL teilen",
+ "LabelShare": "Freigeben",
+ "LabelShareOpen": "Freigabe",
+ "LabelShareURL": "Freigabe URL",
"LabelShowAll": "Alles anzeigen",
"LabelShowSeconds": "Zeige Sekunden",
"LabelShowSubtitles": "Untertitel anzeigen",
@@ -592,6 +609,7 @@
"LabelUnabridged": "Ungekürzt",
"LabelUndo": "Rückgängig machen",
"LabelUnknown": "Unbekannt",
+ "LabelUnknownPublishDate": "Unbekanntes Veröffentlichungsdatum",
"LabelUpdateCover": "Titelbild aktualisieren",
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
"LabelUpdateDetails": "Details aktualisieren",
@@ -640,16 +658,22 @@
"MessageCheckingCron": "Überprüfe Cron...",
"MessageConfirmCloseFeed": "Feed wird geschlossen! Bist du dir sicher?",
"MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Bist du dir sicher?",
+ "MessageConfirmDeleteDevice": "Möchtest Du das E-Reader-Gerät „{0}“ wirklich löschen?",
"MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Bist du dir sicher?",
"MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?",
"MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?",
"MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?",
+ "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?",
"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?",
+ "MessageConfirmMarkItemFinished": "Möchtest du \"{0}\" wirklich als fertig markieren?",
+ "MessageConfirmMarkItemNotFinished": "Möchtest du \"{0}\" wirklich als nicht fertig markieren?",
"MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Bist du dir sicher?",
+ "MessageConfirmNotificationTestTrigger": "Diese Benachrichtigung mit Testdaten abfeuern?",
"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?",
@@ -668,7 +692,9 @@
"MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
+ "MessageConfirmResetProgress": "Möchtest du Ihren Fortschritt wirklich zurücksetzen?",
"MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" wird auf das Gerät \"{2}\" gesendet! Bist du dir sicher?",
+ "MessageConfirmUnlinkOpenId": "Möchtest du die Verknüpfung dieses Benutzers mit OpenID wirklich löschen?",
"MessageDownloadingEpisode": "Episode wird heruntergeladen",
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
"MessageEmbedFailed": "Einbetten fehlgeschlagen!",
@@ -703,6 +729,7 @@
"MessageNoCollections": "Keine Sammlungen",
"MessageNoCoversFound": "Keine Titelbilder gefunden",
"MessageNoDescription": "Keine Beschreibung",
+ "MessageNoDevices": "Keine Geräte",
"MessageNoDownloadsInProgress": "Derzeit keine Downloads in Arbeit",
"MessageNoDownloadsQueued": "Keine Downloads in der Warteschlange",
"MessageNoEpisodeMatchesFound": "Keine Episodenübereinstimmungen gefunden",
@@ -722,7 +749,6 @@
"MessageNoSeries": "Keine Serien",
"MessageNoTags": "Keine Tags",
"MessageNoTasksRunning": "Keine laufenden Aufgaben",
- "MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
"MessageNotYetImplemented": "Noch nicht implementiert",
@@ -731,6 +757,7 @@
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
"MessagePlayChapter": "Kapitelanfang anhören",
"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",
"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",
@@ -747,7 +774,7 @@
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
"MessageShareExpirationWillBe": "Läuft am
{0} ab",
"MessageShareExpiresIn": "Läuft in {0} ab",
- "MessageShareURLWillBe": "Der geteilte Link wird
{0} sein.",
+ "MessageShareURLWillBe": "Der Freigabe Link wird
{0} sein.",
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
"MessageThinking": "Nachdenken...",
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
@@ -771,26 +798,52 @@
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
"PlaceholderSearch": "Suche...",
"PlaceholderSearchEpisode": "Suche Episode...",
+ "StatsAuthorsAdded": "Autoren hinzugefügt",
+ "StatsBooksAdded": "Bücher hinzugefügt",
+ "StatsBooksAdditional": "Einige der Neuzugänge umfassen…",
+ "StatsBooksFinished": "Beendete Bücher",
+ "StatsBooksFinishedThisYear": "Einige Bücher, die dieses Jahr beendet wurden…",
+ "StatsBooksListenedTo": "gehörte Bücher",
+ "StatsCollectionGrewTo": "Deine Bückersammlung ist gewachsen auf…",
+ "StatsSessions": "Sitzungen",
+ "StatsSpentListening": "zugehört",
+ "StatsTopAuthor": "TOP AUTOR",
+ "StatsTopAuthors": "TOP AUTOREN",
+ "StatsTopGenre": "TOP GENRE",
+ "StatsTopGenres": "TOP GENRES",
+ "StatsTopMonth": "TOP MONAT",
+ "StatsTopNarrator": "TOP SPRECHER",
+ "StatsTopNarrators": "TOP SPRECHER",
+ "StatsTotalDuration": "Mit einer totalen Dauer von…",
+ "StatsYearInReview": "DAS JAHR IM RÜCKBLICK",
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
"ToastAccountUpdateSuccess": "Konto aktualisiert",
- "ToastAuthorImageRemoveFailed": "Bild konnte nicht entfernt werden",
+ "ToastAppriseUrlRequired": "Eine Apprise-URL ist notwendig",
"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)",
+ "ToastBackupAppliedSuccess": "Backup anwenden",
"ToastBackupCreateFailed": "Sicherung konnte nicht erstellt werden",
"ToastBackupCreateSuccess": "Sicherung erstellt",
"ToastBackupDeleteFailed": "Sicherung konnte nicht gelöscht werden",
"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",
"ToastBatchUpdateFailed": "Stapelaktualisierung fehlgeschlagen",
"ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich",
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
- "ToastBookmarkRemoveFailed": "Lesezeichen konnte nicht entfernt werden",
"ToastBookmarkRemoveSuccess": "Lesezeichen entfernt",
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
@@ -798,24 +851,46 @@
"ToastCachePurgeSuccess": "Cache geleert",
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
- "ToastCollectionItemsRemoveFailed": "Fehler beim Entfernen der Medien aus der Sammlung",
+ "ToastChaptersRemoved": "Kapitel entfernt",
+ "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",
- "ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
+ "ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
"ToastDeleteFileSuccess": "Datei gelöscht",
+ "ToastDeviceAddFailed": "Gerät konnte nicht hinzugefügt werden",
+ "ToastDeviceNameAlreadyExists": "E-Reader-Gerät mit diesem Namen existiert bereits",
+ "ToastDeviceTestEmailFailed": "Senden der Test-E-Mail fehlgeschlagen",
+ "ToastDeviceTestEmailSuccess": "Test-E-Mail versand",
+ "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",
+ "ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden",
"ToastFailedToLoadData": "Daten laden fehlgeschlagen",
+ "ToastFailedToShare": "Fehler beim Teilen",
+ "ToastFailedToUpdateAccount": "Fehler beim ändern des Accounts",
+ "ToastFailedToUpdateUser": "Fehler beim ändern des Benutzers",
+ "ToastInvalidImageUrl": "Ungültiger Bild URL",
+ "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",
- "ToastItemDetailsUpdateUnneeded": "Keine Aktualisierung für die Artikeldetails erforderlich",
- "ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Mediums als \"Beendet\"",
- "ToastItemMarkedAsFinishedSuccess": "Medium als \"Beendet\" markiert",
- "ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Mediums als \"Nicht Beendet\"",
- "ToastItemMarkedAsNotFinishedSuccess": "Medium als \"Nicht Beendet\" markiert",
+ "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",
"ToastLibraryDeleteFailed": "Bibliothek konnte nicht gelöscht werden",
@@ -824,32 +899,78 @@
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
+ "ToastNameEmailRequired": "Name und Email sind erforderlich",
+ "ToastNameRequired": "Name ist erforderlich",
+ "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",
+ "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",
- "ToastPlaylistRemoveFailed": "Löschen der Wiedergabeliste fehlgeschlagen",
"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",
+ "ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters",
+ "ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt",
+ "ToastProviderNameAndUrlRequired": "Name und URL notwendig",
+ "ToastProviderRemoveSuccess": "Anbieter entfernt",
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
+ "ToastRemoveFailed": "Fehler beim entfernen",
"ToastRemoveItemFromCollectionFailed": "Löschen des Mediums aus der Sammlung fehlgeschlagen",
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
+ "ToastRemoveItemsWithIssuesFailed": "Entfernen von fehlerhaften Bibliotheksartikeln fehlgeschlagenen",
+ "ToastRemoveItemsWithIssuesSuccess": "Fehlerhafte Bibliotheksartikel entfernt",
+ "ToastRenameFailed": "Umbenennen fehlgeschlagen",
+ "ToastRescanFailed": "Erneut scannen fehlgeschlagen für {0}",
+ "ToastRescanRemoved": "Erneut scannen erledigt, Artikel wurde entfernt",
+ "ToastRescanUpToDate": "Erneut scannen erledigt, Artikel wahr auf dem neusten Stand",
+ "ToastRescanUpdated": "Erneut scannen erledigt, Artikel wurde verändert",
+ "ToastScanFailed": "Fehler beim scannen des Artikels der Bibliothek",
+ "ToastSelectAtLeastOneUser": "Wähle mindestens einen Benutzer aus",
"ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden",
"ToastSendEbookToDeviceSuccess": "E-Book 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",
+ "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",
+ "ToastUnlinkOpenIdFailed": "Fehler beim entkoppeln des Benutzers von OpenID",
+ "ToastUnlinkOpenIdSuccess": "Benutzer entkoppelt von OpenID",
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
- "ToastUserDeleteSuccess": "Benutzer gelöscht"
+ "ToastUserDeleteSuccess": "Benutzer gelöscht",
+ "ToastUserPasswordChangeSuccess": "Passwort erfolgreich verändert",
+ "ToastUserPasswordMismatch": "Passwörter stimmen nicht überein",
+ "ToastUserPasswordMustChange": "Neues Passwort muss sich von altem Passwort unterscheiden",
+ "ToastUserRootRequireName": "Root Benutzername muss angegeben werden"
}
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index 7420c1e75a..b3c0dec6c6 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Choose files",
"ButtonClearFilter": "Clear Filter",
"ButtonCloseFeed": "Close Feed",
+ "ButtonCloseSession": "Close Open Session",
"ButtonCollections": "Collections",
"ButtonConfigureScanner": "Configure Scanner",
"ButtonCreate": "Create",
@@ -28,6 +29,9 @@
"ButtonEdit": "Edit",
"ButtonEditChapters": "Edit Chapters",
"ButtonEditPodcast": "Edit Podcast",
+ "ButtonEnable": "Enable",
+ "ButtonFireAndFail": "Fire and Fail",
+ "ButtonFireOnTest": "Fire onTest event",
"ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Full Path",
"ButtonHide": "Hide",
@@ -56,6 +60,7 @@
"ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
+ "ButtonProbeAudioFile": "Probe Audio File",
"ButtonPurgeAllCache": "Purge All Cache",
"ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonQueueAddItem": "Add to queue",
@@ -93,6 +98,7 @@
"ButtonStats": "Stats",
"ButtonSubmit": "Submit",
"ButtonTest": "Test",
+ "ButtonUnlinkOpenId": "Unlink OpenID",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Cover",
@@ -105,6 +111,7 @@
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Account",
+ "HeaderAddCustomMetadataProvider": "Add Custom Metadata Provider",
"HeaderAdvanced": "Advanced",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
"HeaderAudioTracks": "Audio Tracks",
@@ -150,6 +157,8 @@
"HeaderMetadataToEmbed": "Metadata to embed",
"HeaderNewAccount": "New Account",
"HeaderNewLibrary": "New Library",
+ "HeaderNotificationCreate": "Create Notification",
+ "HeaderNotificationUpdate": "Update Notification",
"HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Open RSS Feed",
@@ -206,8 +215,8 @@
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
"LabelAddToPlaylist": "Add to Playlist",
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
- "LabelAdded": "Added",
"LabelAddedAt": "Added At",
+ "LabelAddedDate": "Added {0}",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "All",
"LabelAllUsers": "All Users",
@@ -298,6 +307,7 @@
"LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episode Title",
"LabelEpisodeType": "Episode Type",
+ "LabelEpisodes": "Episodes",
"LabelExample": "Example",
"LabelExpandSeries": "Expand Series",
"LabelExpandSubSeries": "Expand Sub Series",
@@ -309,7 +319,9 @@
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File",
"LabelFileBirthtime": "File Birthtime",
+ "LabelFileBornDate": "Born {0}",
"LabelFileModified": "File Modified",
+ "LabelFileModifiedDate": "Modified {0}",
"LabelFilename": "Filename",
"LabelFilterByUser": "Filter by User",
"LabelFindEpisodes": "Find Episodes",
@@ -448,8 +460,10 @@
"LabelPrimaryEbook": "Primary ebook",
"LabelProgress": "Progress",
"LabelProvider": "Provider",
+ "LabelProviderAuthorizationValue": "Authorization Header Value",
"LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year",
+ "LabelPublishedDate": "Published {0}",
"LabelPublisher": "Publisher",
"LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
@@ -595,6 +609,7 @@
"LabelUnabridged": "Unabridged",
"LabelUndo": "Undo",
"LabelUnknown": "Unknown",
+ "LabelUnknownPublishDate": "Unknown publish date",
"LabelUpdateCover": "Update Cover",
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
"LabelUpdateDetails": "Update Details",
@@ -643,16 +658,22 @@
"MessageCheckingCron": "Checking cron...",
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
+ "MessageConfirmDeleteDevice": "Are you sure you want to delete e-reader device \"{0}\"?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
+ "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?",
"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?",
+ "MessageConfirmMarkItemFinished": "Are you sure you want to mark \"{0}\" as finished?",
+ "MessageConfirmMarkItemNotFinished": "Are you sure you want to mark \"{0}\" as not finished?",
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
+ "MessageConfirmNotificationTestTrigger": "Trigger this notification with test data?",
"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?",
@@ -671,7 +692,9 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
+ "MessageConfirmResetProgress": "Are you sure you want to reset your progress?",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
+ "MessageConfirmUnlinkOpenId": "Are you sure you want to unlink this user from OpenID?",
"MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFailed": "Embed Failed!",
@@ -706,6 +729,7 @@
"MessageNoCollections": "No Collections",
"MessageNoCoversFound": "No Covers Found",
"MessageNoDescription": "No description",
+ "MessageNoDevices": "No devices",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoEpisodeMatchesFound": "No episode matches found",
@@ -725,7 +749,6 @@
"MessageNoSeries": "No Series",
"MessageNoTags": "No Tags",
"MessageNoTasksRunning": "No Tasks Running",
- "MessageNoUpdateNecessary": "No update necessary",
"MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists",
"MessageNotYetImplemented": "Not yet implemented",
@@ -734,6 +757,7 @@
"MessagePauseChapter": "Pause chapter playback",
"MessagePlayChapter": "Listen to beginning of chapter",
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
+ "MessagePleaseWait": "Please wait...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
"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",
@@ -794,24 +818,32 @@
"StatsYearInReview": "YEAR IN REVIEW",
"ToastAccountUpdateFailed": "Failed to update account",
"ToastAccountUpdateSuccess": "Account updated",
- "ToastAuthorImageRemoveFailed": "Failed to remove image",
+ "ToastAppriseUrlRequired": "Must enter an Apprise URL",
"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)",
+ "ToastBackupAppliedSuccess": "Backup applied",
"ToastBackupCreateFailed": "Failed to create backup",
"ToastBackupCreateSuccess": "Backup created",
"ToastBackupDeleteFailed": "Failed to delete backup",
"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",
"ToastBatchUpdateFailed": "Batch update failed",
"ToastBatchUpdateSuccess": "Batch update success",
"ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkCreateSuccess": "Bookmark added",
- "ToastBookmarkRemoveFailed": "Failed to remove bookmark",
"ToastBookmarkRemoveSuccess": "Bookmark removed",
"ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastBookmarkUpdateSuccess": "Bookmark updated",
@@ -819,25 +851,46 @@
"ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Chapters have errors",
"ToastChaptersMustHaveTitles": "Chapters must have titles",
- "ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
+ "ToastChaptersRemoved": "Chapters removed",
+ "ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
+ "ToastCollectionItemsAddSuccess": "Item(s) added to collection success",
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
- "ToastCollectionRemoveFailed": "Failed to remove collection",
"ToastCollectionRemoveSuccess": "Collection removed",
"ToastCollectionUpdateFailed": "Failed to update collection",
"ToastCollectionUpdateSuccess": "Collection updated",
+ "ToastCoverUpdateFailed": "Cover update failed",
"ToastDeleteFileFailed": "Failed to delete file",
"ToastDeleteFileSuccess": "File deleted",
+ "ToastDeviceAddFailed": "Failed to add device",
+ "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",
"ToastErrorCannotShare": "Cannot share natively on this device",
"ToastFailedToLoadData": "Failed to load data",
+ "ToastFailedToShare": "Failed to share",
+ "ToastFailedToUpdateAccount": "Failed to update account",
+ "ToastFailedToUpdateUser": "Failed to update user",
+ "ToastInvalidImageUrl": "Invalid image URL",
+ "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",
- "ToastItemDetailsUpdateUnneeded": "No updates needed for item details",
"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",
"ToastLibraryDeleteFailed": "Failed to delete library",
@@ -846,32 +899,78 @@
"ToastLibraryScanStarted": "Library scan started",
"ToastLibraryUpdateFailed": "Failed to update library",
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
+ "ToastNameEmailRequired": "Name and email are required",
+ "ToastNameRequired": "Name is required",
+ "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",
+ "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",
- "ToastPlaylistRemoveFailed": "Failed to remove playlist",
"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",
+ "ToastProviderCreatedFailed": "Failed to add provider",
+ "ToastProviderCreatedSuccess": "New provider added",
+ "ToastProviderNameAndUrlRequired": "Name and Url required",
+ "ToastProviderRemoveSuccess": "Provider removed",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed",
+ "ToastRemoveFailed": "Failed to remove",
"ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
+ "ToastRemoveItemsWithIssuesFailed": "Failed to remove library items with issues",
+ "ToastRemoveItemsWithIssuesSuccess": "Removed library items with issues",
+ "ToastRenameFailed": "Failed to rename",
+ "ToastRescanFailed": "Re-Scan Failed for {0}",
+ "ToastRescanRemoved": "Re-Scan complete item was removed",
+ "ToastRescanUpToDate": "Re-Scan complete item was up to date",
+ "ToastRescanUpdated": "Re-Scan complete item was updated",
+ "ToastScanFailed": "Failed to scan library item",
+ "ToastSelectAtLeastOneUser": "Select at least one user",
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
"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",
+ "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",
+ "ToastUnlinkOpenIdFailed": "Failed to unlink user from OpenID",
+ "ToastUnlinkOpenIdSuccess": "User unlinked from OpenID",
"ToastUserDeleteFailed": "Failed to delete user",
- "ToastUserDeleteSuccess": "User deleted"
+ "ToastUserDeleteSuccess": "User deleted",
+ "ToastUserPasswordChangeSuccess": "Password changed successfully",
+ "ToastUserPasswordMismatch": "Passwords do not match",
+ "ToastUserPasswordMustChange": "New password cannot match old password",
+ "ToastUserRootRequireName": "Must enter a root username"
}
diff --git a/client/strings/es.json b/client/strings/es.json
index 4f9c6141ca..2f7781db02 100644
--- a/client/strings/es.json
+++ b/client/strings/es.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Escoger un Archivo",
"ButtonClearFilter": "Quitar filtros",
"ButtonCloseFeed": "Cerrar fuente",
+ "ButtonCloseSession": "Cerrar la sesión abierta",
"ButtonCollections": "Colecciones",
"ButtonConfigureScanner": "Configurar Escáner",
"ButtonCreate": "Crear",
@@ -28,6 +29,8 @@
"ButtonEdit": "Editar",
"ButtonEditChapters": "Editar Capítulo",
"ButtonEditPodcast": "Editar Podcast",
+ "ButtonEnable": "Permitir",
+ "ButtonFireOnTest": "Activar evento de prueba",
"ButtonForceReScan": "Forzar Re-Escaneo",
"ButtonFullPath": "Ruta de Acceso Completa",
"ButtonHide": "Esconder",
@@ -44,9 +47,10 @@
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
"ButtonMatchBooks": "Encontrar Libros",
"ButtonNevermind": "Olvidar",
- "ButtonNext": "Next",
+ "ButtonNext": "Siguiente",
"ButtonNextChapter": "Siguiente Capítulo",
- "ButtonOk": "Ok",
+ "ButtonNextItemInQueue": "El siguiente elemento en cola",
+ "ButtonOk": "De acuerdo",
"ButtonOpenFeed": "Abrir fuente",
"ButtonOpenManager": "Abrir Editor",
"ButtonPause": "Pausar",
@@ -55,6 +59,7 @@
"ButtonPlaylists": "Listas de reproducción",
"ButtonPrevious": "Anterior",
"ButtonPreviousChapter": "Capítulo Anterior",
+ "ButtonProbeAudioFile": "Examinar archivo de audio",
"ButtonPurgeAllCache": "Purgar Todo el Cache",
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
"ButtonQueueAddItem": "Agregar a la Fila",
@@ -92,6 +97,7 @@
"ButtonStats": "Estadísticas",
"ButtonSubmit": "Enviar",
"ButtonTest": "Prueba",
+ "ButtonUnlinkOpenId": "Desvincular OpenID",
"ButtonUpload": "Subir",
"ButtonUploadBackup": "Subir Respaldo",
"ButtonUploadCover": "Subir Portada",
@@ -104,6 +110,7 @@
"ErrorUploadFetchMetadataNoResults": "No se pudo obtener metadatos - Intenta actualizar el título y/o autor",
"ErrorUploadLacksTitle": "Se debe tener título",
"HeaderAccount": "Cuenta",
+ "HeaderAddCustomMetadataProvider": "Agregar proveedor de metadatos personalizado",
"HeaderAdvanced": "Avanzado",
"HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise",
"HeaderAudioTracks": "Pistas de audio",
@@ -122,7 +129,7 @@
"HeaderDetails": "Detalles",
"HeaderDownloadQueue": "Lista de Descarga",
"HeaderEbookFiles": "Archivos de libros digitales",
- "HeaderEmail": "Email",
+ "HeaderEmail": "Correo electrónico",
"HeaderEmailSettings": "Opciones de Email",
"HeaderEpisodes": "Episodios",
"HeaderEreaderDevices": "Dispositivos Ereader",
@@ -149,6 +156,8 @@
"HeaderMetadataToEmbed": "Metadatos para Insertar",
"HeaderNewAccount": "Nueva Cuenta",
"HeaderNewLibrary": "Nueva Biblioteca",
+ "HeaderNotificationCreate": "Crear notificación",
+ "HeaderNotificationUpdate": "Notificación de actualización",
"HeaderNotifications": "Notificaciones",
"HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect",
"HeaderOpenRSSFeed": "Abrir fuente RSS",
@@ -205,8 +214,8 @@
"LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección",
"LabelAddToPlaylist": "Añadido a la lista de reproducción",
"LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción",
- "LabelAdded": "Añadido",
"LabelAddedAt": "Añadido",
+ "LabelAddedDate": "Añadido {0}",
"LabelAdminUsersOnly": "Solamente usuarios administradores",
"LabelAll": "Todos",
"LabelAllUsers": "Todos los Usuarios",
@@ -233,10 +242,10 @@
"LabelBackupsMaxBackupSizeHelp": "Como protección contra una configuración errónea, los respaldos fallarán si se excede el tamaño configurado.",
"LabelBackupsNumberToKeep": "Numero de respaldos para conservar",
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.",
- "LabelBitrate": "Bitrate",
+ "LabelBitrate": "Tasa de bits",
"LabelBooks": "Libros",
"LabelButtonText": "Texto del botón",
- "LabelByAuthor": "by {0}",
+ "LabelByAuthor": "por {0}",
"LabelChangePassword": "Cambiar Contraseña",
"LabelChannels": "Canales",
"LabelChapterTitle": "Titulo del Capítulo",
@@ -246,6 +255,7 @@
"LabelClosePlayer": "Cerrar reproductor",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Colapsar serie",
+ "LabelCollapseSubSeries": "Contraer la subserie",
"LabelCollection": "Colección",
"LabelCollections": "Colecciones",
"LabelComplete": "Completo",
@@ -282,7 +292,7 @@
"LabelEbook": "Libro electrónico",
"LabelEbooks": "Libros electrónicos",
"LabelEdit": "Editar",
- "LabelEmail": "Email",
+ "LabelEmail": "Correo electrónico",
"LabelEmailSettingsFromAddress": "Remitente",
"LabelEmailSettingsRejectUnauthorized": "Rechazar certificados no autorizados",
"LabelEmailSettingsRejectUnauthorizedHelp": "Deshabilitar la validación de certificados SSL puede exponer tu conexión a riesgos de seguridad, como ataques man-in-the-middle. Desactiva esta opción sólo si conoces las implicaciones y confias en el servidor de correo al que te conectas.",
@@ -296,8 +306,10 @@
"LabelEpisode": "Episodio",
"LabelEpisodeTitle": "Titulo de Episodio",
"LabelEpisodeType": "Tipo de Episodio",
+ "LabelEpisodes": "Episodios",
"LabelExample": "Ejemplo",
"LabelExpandSeries": "Ampliar serie",
+ "LabelExpandSubSeries": "Expandir la subserie",
"LabelExplicit": "Explicito",
"LabelExplicitChecked": "Explícito (marcado)",
"LabelExplicitUnchecked": "No Explícito (sin marcar)",
@@ -306,7 +318,9 @@
"LabelFetchingMetadata": "Obteniendo metadatos",
"LabelFile": "Archivo",
"LabelFileBirthtime": "Archivo creado en",
+ "LabelFileBornDate": "Creado {0}",
"LabelFileModified": "Archivo modificado",
+ "LabelFileModifiedDate": "Modificado {0}",
"LabelFilename": "Nombre del archivo",
"LabelFilterByUser": "Filtrar por Usuario",
"LabelFindEpisodes": "Buscar Episodio",
@@ -357,18 +371,18 @@
"LabelLastTime": "Última Vez",
"LabelLastUpdate": "Última Actualización",
"LabelLayout": "Distribución",
- "LabelLayoutSinglePage": "Una Página",
+ "LabelLayoutSinglePage": "Página única",
"LabelLayoutSplitPage": "Dos Páginas",
"LabelLess": "Menos",
"LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario",
"LabelLibrary": "Biblioteca",
- "LabelLibraryFilterSublistEmpty": "No {0}",
+ "LabelLibraryFilterSublistEmpty": "Sin {0}",
"LabelLibraryItem": "Elemento de Biblioteca",
"LabelLibraryName": "Nombre de Biblioteca",
"LabelLimit": "Limites",
"LabelLineSpacing": "Interlineado",
"LabelListenAgain": "Volver a escuchar",
- "LabelLogLevelDebug": "Debug",
+ "LabelLogLevelDebug": "Depurar",
"LabelLogLevelInfo": "Información",
"LabelLogLevelWarn": "Advertencia",
"LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha",
@@ -416,7 +430,7 @@
"LabelNumberOfBooks": "Numero de Libros",
"LabelNumberOfEpisodes": "# de Episodios",
"LabelOpenIDAdvancedPermsClaimDescription": "Nombre de la notificación de OpenID que contiene permisos avanzados para acciones de usuario dentro de la aplicación que se aplicarán a roles que no sean de administrador (
si están configurados ). Si el reclamo no aparece en la respuesta, se denegará el acceso a ABS. Si falta una sola opción, se tratará como
falsa
. Asegúrese de que la notificación del proveedor de identidades coincida con la estructura esperada:",
- "LabelOpenIDClaims": "Deje las siguientes opciones vacías para deshabilitar la asignación avanzada de grupos y permisos, lo que asignaría de manera automática al grupo 'Usuario'",
+ "LabelOpenIDClaims": "Deje las siguientes opciones vacías para deshabilitar la asignación avanzada de grupos y permisos, lo que asignaría de manera automática al grupo 'Usuario'.",
"LabelOpenIDGroupClaimDescription": "Nombre de la declaración OpenID que contiene una lista de grupos del usuario. Comúnmente conocidos como
grupos
.
Si se configura , la aplicación asignará automáticamente roles en función de la pertenencia a grupos del usuario, siempre que estos grupos se denominen \"admin\", \"user\" o \"guest\" en la notificación. La solicitud debe contener una lista, y si un usuario pertenece a varios grupos, la aplicación asignará el rol correspondiente al mayor nivel de acceso. Si ningún grupo coincide, se denegará el acceso.",
"LabelOpenRSSFeed": "Abrir Fuente RSS",
"LabelOverwrite": "Sobrescribir",
@@ -433,7 +447,7 @@
"LabelPersonalYearReview": "Revisión de tu año ({0})",
"LabelPhotoPathURL": "Ruta de Acceso/URL de Foto",
"LabelPlayMethod": "Método de Reproducción",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
+ "LabelPlayerChapterNumberMarker": "{0} de {1}",
"LabelPlaylists": "Lista de Reproducción",
"LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Región de búsqueda de podcasts",
@@ -447,6 +461,7 @@
"LabelProvider": "Proveedor",
"LabelPubDate": "Fecha de publicación",
"LabelPublishYear": "Año de publicación",
+ "LabelPublishedDate": "Publicado {0}",
"LabelPublisher": "Editor",
"LabelPublishers": "Editores",
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado",
@@ -455,7 +470,7 @@
"LabelRSSFeedPreventIndexing": "Prevenir indexado",
"LabelRSSFeedSlug": "Fuente RSS Slug",
"LabelRSSFeedURL": "URL de Fuente RSS",
- "LabelRandomly": "Aleatoriamente",
+ "LabelRandomly": "Aleatorio",
"LabelReAddSeriesToContinueListening": "Volver a agregar la serie para continuar escuchándola",
"LabelRead": "Leído",
"LabelReadAgain": "Volver a leer",
@@ -592,6 +607,7 @@
"LabelUnabridged": "No Abreviado",
"LabelUndo": "Deshacer",
"LabelUnknown": "Desconocido",
+ "LabelUnknownPublishDate": "Fecha de publicación desconocida",
"LabelUpdateCover": "Actualizar Portada",
"LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes para los libros seleccionados cuando se encuentra una coincidencia",
"LabelUpdateDetails": "Actualizar Detalles",
@@ -627,29 +643,35 @@
"MessageBackupsLocationNoEditNote": "Nota: La ubicación de la copia de seguridad se establece a través de una variable de entorno y no se puede cambiar aquí.",
"MessageBackupsLocationPathEmpty": "La ruta de la copia de seguridad no puede estar vacía",
"MessageBatchQuickMatchDescription": "\"Encontrar Rápido\" tratará de agregar portadas y metadatos faltantes de los elementos seleccionados. Habilite la opción de abajo para que \"Encontrar Rápido\" pueda sobrescribir portadas y/o metadatos existentes.",
- "MessageBookshelfNoCollections": "No tienes ninguna colección.",
+ "MessageBookshelfNoCollections": "No tienes ninguna colección",
"MessageBookshelfNoRSSFeeds": "Ninguna Fuente RSS esta abierta",
"MessageBookshelfNoResultsForFilter": "Ningún Resultado para el filtro \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "No hay resultados para la consulta",
"MessageBookshelfNoSeries": "No tienes ninguna serie",
- "MessageChapterEndIsAfter": "El final del capítulo es después del final de tu audiolibro.",
+ "MessageChapterEndIsAfter": "El final del capítulo es después del final de tu audiolibro",
"MessageChapterErrorFirstNotZero": "El primer capitulo debe iniciar en 0",
- "MessageChapterErrorStartGteDuration": "El tiempo de inicio no es válido: debe ser inferior a la duración del audiolibro.",
+ "MessageChapterErrorStartGteDuration": "El tiempo de inicio no es válido: debe ser inferior a la duración del audiolibro",
"MessageChapterErrorStartLtPrev": "El tiempo de inicio no es válido: debe ser mayor o igual que el tiempo de inicio del capítulo anterior",
"MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro",
"MessageCheckingCron": "Revisando cron...",
"MessageConfirmCloseFeed": "Está seguro de que desea cerrar esta fuente?",
"MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?",
+ "MessageConfirmDeleteDevice": "¿Estás seguro de que deseas eliminar el lector electrónico \"{0}\"?",
"MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?",
"MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Esto removerá la librería de la base de datos y archivos en tu sistema. ¿Estás seguro?",
"MessageConfirmDeleteLibraryItems": "Esto removerá {0} elemento(s) de la librería en base de datos y archivos en tu sistema. ¿Estás seguro?",
+ "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?",
"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?",
+ "MessageConfirmMarkItemFinished": "¿Estás seguro de que deseas marcar \"{0}\" como terminado?",
+ "MessageConfirmMarkItemNotFinished": "¿Estás seguro de que deseas marcar \"{0}\" como no acabado?",
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
+ "MessageConfirmNotificationTestTrigger": "¿Activar esta notificación con datos de prueba?",
"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?",
@@ -668,9 +690,11 @@
"MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.",
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
+ "MessageConfirmResetProgress": "¿Estás seguro de que quieres reiniciar tu progreso?",
"MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?",
+ "MessageConfirmUnlinkOpenId": "¿Estás seguro de que deseas desvincular este usuario de OpenID?",
"MessageDownloadingEpisode": "Descargando Capitulo",
- "MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
+ "MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas",
"MessageEmbedFailed": "¡Error al insertar!",
"MessageEmbedFinished": "Incrustación Terminada!",
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
@@ -703,6 +727,7 @@
"MessageNoCollections": "Sin Colecciones",
"MessageNoCoversFound": "Ninguna Portada Encontrada",
"MessageNoDescription": "Sin Descripción",
+ "MessageNoDevices": "Sin dispositivos",
"MessageNoDownloadsInProgress": "No hay descargas actualmente en curso",
"MessageNoDownloadsQueued": "Sin Lista de Descarga",
"MessageNoEpisodeMatchesFound": "No se encontraron episodios que coinciden",
@@ -722,7 +747,6 @@
"MessageNoSeries": "Sin Series",
"MessageNoTags": "Sin Etiquetas",
"MessageNoTasksRunning": "Ninguna Tarea Corriendo",
- "MessageNoUpdateNecessary": "No es necesario actualizar",
"MessageNoUpdatesWereNecessary": "No fue necesario actualizar",
"MessageNoUserPlaylists": "No tienes lista de reproducciones",
"MessageNotYetImplemented": "Aun no implementado",
@@ -731,6 +755,7 @@
"MessagePauseChapter": "Pausar la reproducción del capítulo",
"MessagePlayChapter": "Escuchar el comienzo del capítulo",
"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",
"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",
@@ -771,26 +796,52 @@
"PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción",
"PlaceholderSearch": "Buscar..",
"PlaceholderSearchEpisode": "Buscar Episodio..",
+ "StatsAuthorsAdded": "autores añadidos",
+ "StatsBooksAdded": "libros añadidos",
+ "StatsBooksAdditional": "Algunas adiciones incluyen…",
+ "StatsBooksFinished": "libros terminados",
+ "StatsBooksFinishedThisYear": "Algunos libros terminados este año…",
+ "StatsBooksListenedTo": "libros escuchados",
+ "StatsCollectionGrewTo": "Tu colección de libros creció hasta…",
+ "StatsSessions": "sesiones",
+ "StatsSpentListening": "dedicado a la escucha",
+ "StatsTopAuthor": "AUTOR DESTACADO",
+ "StatsTopAuthors": "AUTORES DESTACADOS",
+ "StatsTopGenre": "GÉNERO PRINCIPAL",
+ "StatsTopGenres": "GÉNEROS PRINCIPALES",
+ "StatsTopMonth": "DESTACADO DEL MES",
+ "StatsTopNarrator": "NARRADOR DESTACADO",
+ "StatsTopNarrators": "NARRADORES DESTACADOS",
+ "StatsTotalDuration": "Con una duración total de…",
+ "StatsYearInReview": "RESEÑA DEL AÑO",
"ToastAccountUpdateFailed": "Error al actualizar cuenta",
"ToastAccountUpdateSuccess": "Cuenta actualizada",
- "ToastAuthorImageRemoveFailed": "Error al eliminar la imagen",
+ "ToastAppriseUrlRequired": "Debes ingresar una URL de Apprise",
"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)",
+ "ToastBackupAppliedSuccess": "Copia de seguridad aplicada",
"ToastBackupCreateFailed": "Error al crear respaldo",
"ToastBackupCreateSuccess": "Respaldo creado",
"ToastBackupDeleteFailed": "Error al eliminar respaldo",
"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",
"ToastBatchUpdateFailed": "Subida masiva fallida",
"ToastBatchUpdateSuccess": "Subida masiva exitosa",
"ToastBookmarkCreateFailed": "Error al crear marcador",
"ToastBookmarkCreateSuccess": "Marcador Agregado",
- "ToastBookmarkRemoveFailed": "Error al eliminar marcador",
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
"ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
"ToastBookmarkUpdateSuccess": "Marcador actualizado",
@@ -798,24 +849,46 @@
"ToastCachePurgeSuccess": "Caché purgado de manera exitosa",
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
"ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título",
- "ToastCollectionItemsRemoveFailed": "Error al remover elemento(s) de la colección",
+ "ToastChaptersRemoved": "Capítulos eliminados",
+ "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",
- "ToastCollectionRemoveFailed": "Error al remover 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",
"ToastDeleteFileSuccess": "Archivo eliminado",
+ "ToastDeviceAddFailed": "Error al añadir dispositivo",
+ "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",
+ "ToastErrorCannotShare": "No se puede compartir de forma nativa en este dispositivo",
"ToastFailedToLoadData": "Error al cargar data",
+ "ToastFailedToShare": "Error al compartir",
+ "ToastFailedToUpdateAccount": "Error al actualizar la cuenta",
+ "ToastFailedToUpdateUser": "Error al actualizar el usuario",
+ "ToastInvalidImageUrl": "URL de la imagen no válida",
+ "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",
- "ToastItemDetailsUpdateUnneeded": "No se necesitan actualizaciones para los detalles del Elemento",
"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",
"ToastLibraryDeleteFailed": "Error al eliminar biblioteca",
@@ -824,32 +897,78 @@
"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",
+ "ToastNameRequired": "Nombre obligatorio",
+ "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",
+ "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",
- "ToastPlaylistRemoveFailed": "Error al eliminar la lista de reproducción",
"ToastPlaylistRemoveSuccess": "Lista de reproducción eliminada",
- "ToastPlaylistUpdateFailed": "Error al actualizar la lista de reproducción.",
+ "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",
+ "ToastProviderCreatedFailed": "Error al añadir el proveedor",
+ "ToastProviderCreatedSuccess": "Nuevo proveedor añadido",
+ "ToastProviderNameAndUrlRequired": "Nombre y Url obligatorios",
+ "ToastProviderRemoveSuccess": "Proveedor eliminado",
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
+ "ToastRemoveFailed": "Error al eliminar",
"ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección",
- "ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.",
+ "ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección",
+ "ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca incorrectos",
+ "ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca incorrectos",
+ "ToastRenameFailed": "Error al cambiar el nombre",
+ "ToastRescanFailed": "Error al volver a escanear para {0}",
+ "ToastRescanRemoved": "Se eliminó el elemento reescaneado",
+ "ToastRescanUpToDate": "Reescaneado del artículo completo, estaba actualizado",
+ "ToastRescanUpdated": "Reescaneado completado, el artículo ha sido actualizado",
+ "ToastScanFailed": "No se pudo escanear el elemento de la biblioteca",
+ "ToastSelectAtLeastOneUser": "Selecciona al menos un usuario",
"ToastSendEbookToDeviceFailed": "Error al enviar el ebook al dispositivo",
"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",
+ "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",
+ "ToastUnlinkOpenIdFailed": "Error al desvincular el usuario de OpenID",
+ "ToastUnlinkOpenIdSuccess": "Usuario desvinculado de OpenID",
"ToastUserDeleteFailed": "Error al eliminar el usuario",
- "ToastUserDeleteSuccess": "Usuario eliminado"
+ "ToastUserDeleteSuccess": "Usuario eliminado",
+ "ToastUserPasswordChangeSuccess": "Contraseña modificada correctamente",
+ "ToastUserPasswordMismatch": "No coinciden las contraseñas",
+ "ToastUserPasswordMustChange": "La nueva contraseña no puede ser igual que la anterior",
+ "ToastUserRootRequireName": "Debes introducir un nombre de usuario root"
}
diff --git a/client/strings/et.json b/client/strings/et.json
index 2a1b61ed54..45da898959 100644
--- a/client/strings/et.json
+++ b/client/strings/et.json
@@ -9,7 +9,6 @@
"ButtonApply": "Rakenda",
"ButtonApplyChapters": "Rakenda peatükid",
"ButtonAuthors": "Autorid",
- "ButtonBack": "Back",
"ButtonBrowseForFolder": "Sirvi kausta",
"ButtonCancel": "Tühista",
"ButtonCancelEncode": "Tühista kodeerimine",
@@ -44,16 +43,13 @@
"ButtonMatchAllAuthors": "Sobita kõik autorid",
"ButtonMatchBooks": "Sobita raamatud",
"ButtonNevermind": "Pole tähtis",
- "ButtonNext": "Next",
"ButtonNextChapter": "Järgmine peatükk",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Ava voog",
"ButtonOpenManager": "Ava haldur",
"ButtonPause": "Peata",
"ButtonPlay": "Mängi",
"ButtonPlaying": "Mängib",
"ButtonPlaylists": "Esitusloendid",
- "ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Eelmine peatükk",
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
"ButtonPurgeItemsCache": "Tühjenda esemete vahemälu",
@@ -62,9 +58,6 @@
"ButtonQuickMatch": "Kiire sobitamine",
"ButtonReScan": "Uuestiskaneeri",
"ButtonRead": "Loe",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
"ButtonRemove": "Eemalda",
"ButtonRemoveAll": "Eemalda kõik",
"ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed",
@@ -83,13 +76,11 @@
"ButtonSelectFolderPath": "Vali kaustatee",
"ButtonSeries": "Sarjad",
"ButtonSetChaptersFromTracks": "Määra peatükid lugudest",
- "ButtonShare": "Share",
"ButtonShiftTimes": "Nihke ajad",
"ButtonShow": "Näita",
"ButtonStartM4BEncode": "Alusta M4B kodeerimist",
"ButtonStartMetadataEmbed": "Alusta metaandmete lisamist",
"ButtonSubmit": "Esita",
- "ButtonTest": "Test",
"ButtonUpload": "Lae üles",
"ButtonUploadBackup": "Lae üles varundus",
"ButtonUploadCover": "Lae üles ümbris",
@@ -115,7 +106,6 @@
"HeaderCollectionItems": "Kogu esemed",
"HeaderCover": "Ümbris",
"HeaderCurrentDownloads": "Praegused allalaadimised",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
"HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad",
"HeaderDetails": "Detailid",
"HeaderDownloadQueue": "Allalaadimise järjekord",
@@ -187,12 +177,8 @@
"HeaderUpdateDetails": "Uuenda detaile",
"HeaderUpdateLibrary": "Uuenda raamatukogu",
"HeaderUsers": "Kasutajad",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Sinu statistika",
"LabelAbridged": "Kärbitud",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Konto tüüp",
"LabelAccountTypeAdmin": "Administraator",
"LabelAccountTypeGuest": "Külaline",
@@ -202,7 +188,6 @@
"LabelAddToCollectionBatch": "Lisa {0} raamatut kogusse",
"LabelAddToPlaylist": "Lisa mänguloendisse",
"LabelAddToPlaylistBatch": "Lisa {0} eset mänguloendisse",
- "LabelAdded": "Lisatud",
"LabelAddedAt": "Lisatud",
"LabelAdminUsersOnly": "Ainult administraatorid",
"LabelAll": "Kõik",
@@ -233,7 +218,6 @@
"LabelBitrate": "Bittkiirus",
"LabelBooks": "Raamatud",
"LabelButtonText": "Nupu tekst",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Muuda parooli",
"LabelChannels": "Kanalid",
"LabelChapterTitle": "Peatüki pealkiri",
@@ -271,17 +255,12 @@
"LabelDownload": "Lae alla",
"LabelDownloadNEpisodes": "Lae alla {0} episoodi",
"LabelDuration": "Kestus",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Leitud kestus:",
"LabelEbook": "E-raamat",
"LabelEbooks": "E-raamatud",
"LabelEdit": "Muuda",
"LabelEmail": "E-post",
"LabelEmailSettingsFromAddress": "Saatja aadress",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "Turvaline",
"LabelEmailSettingsSecureHelp": "Kui see on tõene, kasutab ühendus serveriga ühenduse loomisel TLS-i. Kui see on väär, kasutatakse TLS-i, kui server toetab STARTTLS-i laiendust. Enamikul juhtudest seadke see väärtus tõeks, kui ühendate pordile 465. Pordi 587 või 25 korral hoidke seda väär. (nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testi aadress",
@@ -293,8 +272,6 @@
"LabelEpisodeType": "Episoodi tüüp",
"LabelExample": "Näide",
"LabelExplicit": "Vulgaarne",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "Voogu URL",
"LabelFetchingMetadata": "Metaandmete hankimine",
"LabelFile": "Fail",
@@ -307,7 +284,6 @@
"LabelFolder": "Kaust",
"LabelFolders": "Kataloogid",
"LabelFontBold": "Paks",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Fondi pere",
"LabelFontItalic": "Kaldkiri",
"LabelFontScale": "Fondi suurus",
@@ -319,7 +295,6 @@
"LabelHasEbook": "On e-raamat",
"LabelHasSupplementaryEbook": "On täiendav e-raamat",
"LabelHighestPriority": "Kõrgeim prioriteet",
- "LabelHost": "Host",
"LabelHour": "Tund",
"LabelIcon": "Ikoon",
"LabelImageURLFromTheWeb": "Pildi URL veebist",
@@ -339,7 +314,6 @@
"LabelItem": "Kirje",
"LabelLanguage": "Keel",
"LabelLanguageDefaultServer": "Vaikeserveri keel",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "Viimati lisatud raamat",
"LabelLastBookUpdated": "Viimati uuendatud raamat",
"LabelLastSeen": "Viimati nähtud",
@@ -351,7 +325,6 @@
"LabelLess": "Vähem",
"LabelLibrariesAccessibleToUser": "Kasutajale ligipääsetavad raamatukogud",
"LabelLibrary": "Raamatukogu",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Raamatukogu kirje",
"LabelLibraryName": "Raamatukogu nimi",
"LabelLimit": "Piirang",
@@ -372,8 +345,6 @@
"LabelMetadataProvider": "Metaandmete pakkuja",
"LabelMinute": "Minut",
"LabelMissing": "Puudub",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d",
"LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks
audiobookshelf://oauth
, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (
*
) ainukese kirjena kasutamine võimaldab mis tahes URI-d.",
"LabelMore": "Rohkem",
@@ -387,7 +358,6 @@
"LabelNewestEpisodes": "Uusimad episoodid",
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Episoodid pole valitud",
"LabelNotFinished": "Ei ole lõpetatud",
"LabelNotStarted": "Pole alustatud",
@@ -403,9 +373,6 @@
"LabelNotificationsMaxQueueSizeHelp": "Sündmused on piiratud 1 sekundiga. Sündmusi ignoreeritakse, kui järjekord on maksimumsuuruses. See takistab teavituste rämpsposti.",
"LabelNumberOfBooks": "Raamatute arv",
"LabelNumberOfEpisodes": "Episoodide arv",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Ava RSS voog",
"LabelOverwrite": "Kirjuta üle",
"LabelPassword": "Parool",
@@ -417,16 +384,12 @@
"LabelPermissionsDownload": "Saab alla laadida",
"LabelPermissionsUpdate": "Saab uuendada",
"LabelPermissionsUpload": "Saab üles laadida",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto tee/URL",
"LabelPlayMethod": "Esitusmeetod",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Mänguloendid",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcasti otsingu piirkond",
"LabelPodcastType": "Podcasti tüüp",
"LabelPodcasts": "Podcastid",
- "LabelPort": "Port",
"LabelPrefixesToIgnore": "Eiramiseks eesliited (tõstutundetu)",
"LabelPreventIndexing": "Vältige oma voogu indekseerimist iTunes'i ja Google podcasti kataloogides",
"LabelPrimaryEbook": "Esmane e-raamat",
@@ -435,7 +398,6 @@
"LabelPubDate": "Avaldamise kuupäev",
"LabelPublishYear": "Aasta avaldamine",
"LabelPublisher": "Kirjastaja",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Kohandatud omaniku e-post",
"LabelRSSFeedCustomOwnerName": "Kohandatud omaniku nimi",
"LabelRSSFeedOpen": "Ava RSS voog",
@@ -457,7 +419,6 @@
"LabelSearchTitle": "Otsi pealkirja",
"LabelSearchTitleOrASIN": "Otsi pealkirja või ASIN-i",
"LabelSeason": "Hooaeg",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Vali kõik episoodid",
"LabelSelectEpisodesShowing": "Valige {0} näidatavat episoodi",
"LabelSelectUsers": "Valige kasutajad",
@@ -466,7 +427,6 @@
"LabelSeries": "Seeria",
"LabelSeriesName": "Seeria nimi",
"LabelSeriesProgress": "Seeria edenemine",
- "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Määra peamiseks",
"LabelSetEbookAsSupplementary": "Määra täiendavaks",
"LabelSettingsAudiobooksOnly": "Ainult heliraamatud",
@@ -480,8 +440,6 @@
"LabelSettingsEnableWatcher": "Luba vaatamine",
"LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks",
"LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",
"LabelSettingsFindCovers": "Leia ümbrised",
@@ -490,8 +448,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.",
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
- "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": "Lugege subtiitreid",
"LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.
Subtiitrid peavad olema eraldatud \" - \".
Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",
@@ -508,10 +464,8 @@
"LabelSettingsStoreMetadataWithItemHelp": "Vaikimisi salvestatakse metaandmed /metadata/items kausta. Selle seadistuse lubamine salvestab metaandmed teie raamatukogu üksuse kaustadesse",
"LabelSettingsTimeFormat": "Kellaaja vorming",
"LabelShowAll": "Näita kõiki",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Suurus",
"LabelSleepTimer": "Uinaku taimer",
- "LabelSlug": "Slug",
"LabelStart": "Alusta",
"LabelStartTime": "Alustamise aeg",
"LabelStarted": "Alustatud",
@@ -539,7 +493,6 @@
"LabelTagsNotAccessibleToUser": "Kasutajale mittekättesaadavad sildid",
"LabelTasks": "Käimasolevad ülesanded",
"LabelTextEditorBulletedList": "Punktloend",
- "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numberloend",
"LabelTextEditorUnlink": "Eemalda link",
"LabelTheme": "Teema",
@@ -588,8 +541,6 @@
"LabelViewQueue": "Vaata esitusjärjekorda",
"LabelVolume": "Heli tugevus",
"LabelWeekdaysToRun": "Päevad nädalas käivitamiseks",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Teie heliraamatu kestus",
"LabelYourBookmarks": "Teie järjehoidjad",
"LabelYourPlaylists": "Teie esitusloendid",
@@ -601,7 +552,6 @@
"MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud",
"MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud",
"MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Teil pole ühtegi seeriat",
"MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu",
"MessageChapterErrorFirstNotZero": "Esimene peatükk peab algama 0-st",
@@ -621,8 +571,6 @@
"MessageConfirmMarkAllEpisodesNotFinished": "Olete kindel, et soovite kõik episoodid mitte lõpetatuks märkida?",
"MessageConfirmMarkSeriesFinished": "Olete kindel, et soovite selle seeria kõik raamatud lõpetatuks märkida?",
"MessageConfirmMarkSeriesNotFinished": "Olete kindel, et soovite selle seeria kõik raamatud mitte lõpetatuks märkida?",
- "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": "Hoiatus! Quick Embed ei tee varukoopiaid teie helifailidest. Veenduge, et teil oleks varukoopia oma helifailidest.
Kas soovite jätkata?",
"MessageConfirmReScanLibraryItems": "Olete kindel, et soovite uuesti skannida {0} üksust?",
"MessageConfirmRemoveAllChapters": "Olete kindel, et soovite eemaldada kõik peatükid?",
@@ -644,7 +592,6 @@
"MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda",
"MessageEmbedFinished": "Manustamine lõpetatud!",
"MessageEpisodesQueuedForDownload": "{0} Episood(i) on allalaadimiseks järjekorras",
- "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": "Toite URL saab olema {0}",
"MessageFetching": "Hangitakse...",
"MessageForceReScanDescription": "skaneerib kõik failid uuesti nagu värsket skannimist. Heli faili ID3 silte, OPF faile ja tekstifaile skaneeritakse uuesti.",
@@ -656,7 +603,6 @@
"MessageListeningSessionsInTheLastYear": "Kuulamissessioone viimase aasta jooksul: {0}",
"MessageLoading": "Laadimine...",
"MessageLoadingFolders": "Kaustade laadimine...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B ebaõnnestus!",
"MessageM4BFinished": "M4B lõpetatud!",
"MessageMapChapterTitles": "Kaarda peatükkide pealkirjad olemasolevatele heliraamatu peatükkidele, ajatempe ei muudeta",
@@ -692,7 +638,6 @@
"MessageNoSeries": "Ühtegi seeriat pole",
"MessageNoTags": "Ühtegi silti pole",
"MessageNoTasksRunning": "Ühtegi käimasolevat ülesannet pole",
- "MessageNoUpdateNecessary": "Ühtegi värskendust pole vaja",
"MessageNoUpdatesWereNecessary": "Ühtegi värskendust polnud vaja",
"MessageNoUserPlaylists": "Teil pole ühtegi esitusloendit",
"MessageNotYetImplemented": "Pole veel ellu viidud",
@@ -739,7 +684,6 @@
"PlaceholderSearchEpisode": "Otsi episoodi...",
"ToastAccountUpdateFailed": "Konto värskendamine ebaõnnestus",
"ToastAccountUpdateSuccess": "Konto on värskendatud",
- "ToastAuthorImageRemoveFailed": "Pildi eemaldamine ebaõnnestus",
"ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud",
"ToastAuthorUpdateFailed": "Autori värskendamine ebaõnnestus",
"ToastAuthorUpdateMerged": "Autor liidetud",
@@ -756,28 +700,19 @@
"ToastBatchUpdateSuccess": "Partii värskendamine õnnestus",
"ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus",
"ToastBookmarkCreateSuccess": "Järjehoidja lisatud",
- "ToastBookmarkRemoveFailed": "Järjehoidja eemaldamine ebaõnnestus",
"ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud",
"ToastBookmarkUpdateFailed": "Järjehoidja värskendamine ebaõnnestus",
"ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Peatükkidel on vigu",
"ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri",
- "ToastCollectionItemsRemoveFailed": "Üksuse(te) eemaldamine kogumist ebaõnnestus",
"ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist",
- "ToastCollectionRemoveFailed": "Kogumi eemaldamine ebaõnnestus",
"ToastCollectionRemoveSuccess": "Kogum eemaldatud",
"ToastCollectionUpdateFailed": "Kogumi värskendamine ebaõnnestus",
"ToastCollectionUpdateSuccess": "Kogum värskendatud",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Üksuse kaane värskendamine ebaõnnestus",
"ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud",
"ToastItemDetailsUpdateFailed": "Üksuse üksikasjade värskendamine ebaõnnestus",
"ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud",
- "ToastItemDetailsUpdateUnneeded": "Üksuse üksikasjade värskendamine pole vajalik",
"ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus",
"ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud",
"ToastItemMarkedAsNotFinishedFailed": "Märgistamine kui mitte lõpetatud ebaõnnestus",
@@ -792,7 +727,6 @@
"ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud",
"ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus",
"ToastPlaylistCreateSuccess": "Esitusloend loodud",
- "ToastPlaylistRemoveFailed": "Esitusloendi eemaldamine ebaõnnestus",
"ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud",
"ToastPlaylistUpdateFailed": "Esitusloendi värskendamine ebaõnnestus",
"ToastPlaylistUpdateSuccess": "Esitusloend värskendatud",
@@ -806,16 +740,11 @@
"ToastSendEbookToDeviceSuccess": "E-raamat saadetud seadmesse \"{0}\"",
"ToastSeriesUpdateFailed": "Sarja värskendamine ebaõnnestus",
"ToastSeriesUpdateSuccess": "Sarja värskendamine õnnestus",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Seansi kustutamine ebaõnnestus",
"ToastSessionDeleteSuccess": "Sessioon kustutatud",
"ToastSocketConnected": "Pesa ühendatud",
"ToastSocketDisconnected": "Pesa ühendus katkenud",
"ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Kasutaja kustutamine ebaõnnestus",
"ToastUserDeleteSuccess": "Kasutaja kustutatud"
}
diff --git a/client/strings/fi.json b/client/strings/fi.json
index 95e9254995..b1840bb062 100644
--- a/client/strings/fi.json
+++ b/client/strings/fi.json
@@ -46,7 +46,6 @@
"ButtonNevermind": "Ei sittenkään",
"ButtonNext": "Seuraava",
"ButtonNextChapter": "Seuraava luku",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Avaa syöte",
"ButtonOpenManager": "Avaa hallinta",
"ButtonPause": "Pysäytä",
@@ -166,7 +165,6 @@
"LabelAddToCollectionBatch": "Lisää {0} kirjaa kokoelmaan",
"LabelAddToPlaylist": "Lisää soittolistaan",
"LabelAddToPlaylistBatch": "Lisää {0} kohdetta soittolistaan",
- "LabelAdded": "Lisätty",
"LabelAddedAt": "Lisätty listalle",
"LabelAll": "Kaikki",
"LabelAllUsers": "Kaikki käyttäjät",
@@ -231,7 +229,6 @@
"LabelNewestEpisodes": "Uusimmat jaksot",
"LabelPassword": "Salasana",
"LabelPath": "Polku",
- "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastit",
"LabelPublishYear": "Julkaisuvuosi",
"LabelRSSFeedPreventIndexing": "Estä indeksointi",
@@ -269,7 +266,6 @@
"MessageNoUserPlaylists": "Sinulla ei ole soittolistoja",
"MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu",
"ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui",
- "ToastBookmarkRemoveFailed": "Kirjanmerkin poistaminen epäonnistui",
"ToastBookmarkUpdateFailed": "Kirjanmerkin päivittäminen epäonnistui",
"ToastItemMarkedAsFinishedFailed": "Valmiiksi merkitseminen epäonnistui",
"ToastPlaylistCreateFailed": "Soittolistan luominen epäonnistui",
diff --git a/client/strings/fr.json b/client/strings/fr.json
index 32520aaa5e..6b01995a9b 100644
--- a/client/strings/fr.json
+++ b/client/strings/fr.json
@@ -9,16 +9,17 @@
"ButtonApply": "Appliquer",
"ButtonApplyChapters": "Appliquer aux chapitres",
"ButtonAuthors": "Auteurs",
- "ButtonBack": "Retour",
+ "ButtonBack": "Reculer",
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
"ButtonCancel": "Annuler",
"ButtonCancelEncode": "Annuler l’encodage",
"ButtonChangeRootPassword": "Modifier le mot de passe Administrateur",
"ButtonCheckAndDownloadNewEpisodes": "Vérifier et télécharger de nouveaux épisodes",
- "ButtonChooseAFolder": "Choisir un dossier",
- "ButtonChooseFiles": "Choisir les fichiers",
+ "ButtonChooseAFolder": "Sélectionner un dossier",
+ "ButtonChooseFiles": "Sélectionner des fichiers",
"ButtonClearFilter": "Effacer le filtre",
"ButtonCloseFeed": "Fermer le flux",
+ "ButtonCloseSession": "Fermer la session",
"ButtonCollections": "Collections",
"ButtonConfigureScanner": "Configurer l’analyse",
"ButtonCreate": "Créer",
@@ -28,6 +29,9 @@
"ButtonEdit": "Modifier",
"ButtonEditChapters": "Modifier les chapitres",
"ButtonEditPodcast": "Modifier les podcasts",
+ "ButtonEnable": "Activer",
+ "ButtonFireAndFail": "Échec de l’action",
+ "ButtonFireOnTest": "Déclencher l’événement onTest",
"ButtonForceReScan": "Forcer une nouvelle analyse",
"ButtonFullPath": "Chemin complet",
"ButtonHide": "Cacher",
@@ -46,6 +50,7 @@
"ButtonNevermind": "Non merci",
"ButtonNext": "Suivant",
"ButtonNextChapter": "Chapitre suivant",
+ "ButtonNextItemInQueue": "Élément suivant dans la file d’attente",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Ouvrir le flux",
"ButtonOpenManager": "Ouvrir le gestionnaire",
@@ -55,6 +60,7 @@
"ButtonPlaylists": "Listes de lecture",
"ButtonPrevious": "Précédent",
"ButtonPreviousChapter": "Chapitre précédent",
+ "ButtonProbeAudioFile": "Analyser le fichier audio",
"ButtonPurgeAllCache": "Purger tout le cache",
"ButtonPurgeItemsCache": "Purger le cache des éléments",
"ButtonQueueAddItem": "Ajouter à la liste de lecture",
@@ -66,7 +72,7 @@
"ButtonReadLess": "Lire moins",
"ButtonReadMore": "Lire la suite",
"ButtonRefresh": "Rafraîchir",
- "ButtonRemove": "Retirer",
+ "ButtonRemove": "Supprimer",
"ButtonRemoveAll": "Supprimer tout",
"ButtonRemoveAllLibraryItems": "Supprimer tous les éléments de la bibliothèque",
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
@@ -104,11 +110,12 @@
"ErrorUploadFetchMetadataNoResults": "Impossible de récupérer les métadonnées - essayez de mettre à jour le titre et/ou l’auteur",
"ErrorUploadLacksTitle": "Doit avoir un titre",
"HeaderAccount": "Compte",
+ "HeaderAddCustomMetadataProvider": "Ajouter un fournisseur de métadonnées personnalisé",
"HeaderAdvanced": "Avancé",
"HeaderAppriseNotificationSettings": "Configuration des notifications Apprise",
"HeaderAudioTracks": "Pistes audio",
"HeaderAudiobookTools": "Outils de gestion de fichiers de livres audio",
- "HeaderAuthentication": "Authentication",
+ "HeaderAuthentication": "Authentification",
"HeaderBackups": "Sauvegardes",
"HeaderChangePassword": "Modifier le mot de passe",
"HeaderChapters": "Chapitres",
@@ -122,8 +129,8 @@
"HeaderDetails": "Détails",
"HeaderDownloadQueue": "File d’attente de téléchargements",
"HeaderEbookFiles": "Fichiers des livres numériques",
- "HeaderEmail": "Courriels",
- "HeaderEmailSettings": "Configuration des courriels",
+ "HeaderEmail": "Courriel",
+ "HeaderEmailSettings": "Configuration de l’envoie des courriels",
"HeaderEpisodes": "Épisodes",
"HeaderEreaderDevices": "Lecteur de livres numériques",
"HeaderEreaderSettings": "Paramètres de la liseuse",
@@ -149,6 +156,8 @@
"HeaderMetadataToEmbed": "Métadonnées à intégrer",
"HeaderNewAccount": "Nouveau compte",
"HeaderNewLibrary": "Nouvelle bibliothèque",
+ "HeaderNotificationCreate": "Créer une notification",
+ "HeaderNotificationUpdate": "Mise à jour de la notification",
"HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "Authentification via OpenID Connect",
"HeaderOpenRSSFeed": "Ouvrir le flux RSS",
@@ -205,8 +214,8 @@
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
- "LabelAdded": "Ajouté",
"LabelAddedAt": "Date d’ajout",
+ "LabelAddedDate": "{0} ajoutés",
"LabelAdminUsersOnly": "Administrateurs uniquement",
"LabelAll": "Tout",
"LabelAllUsers": "Tous les utilisateurs",
@@ -246,6 +255,7 @@
"LabelClosePlayer": "Fermer le lecteur",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Réduire les séries",
+ "LabelCollapseSubSeries": "Replier les sous-séries",
"LabelCollection": "Collection",
"LabelCollections": "Collections",
"LabelComplete": "Complet",
@@ -287,7 +297,7 @@
"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 vous activez cette option, TLS sera utiliser lors de la connexion au serveur. Sinon, TLS est utilisé uniquement si le serveur supporte l’extension STARTTLS. Dans la plupart des cas, activez l’option, vous vous connecterai sur le port 465. Pour le port 587 ou 25, désactiver l’option. (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",
@@ -296,8 +306,10 @@
"LabelEpisode": "Épisode",
"LabelEpisodeTitle": "Titre de l’épisode",
"LabelEpisodeType": "Type de l’épisode",
+ "LabelEpisodes": "Épisodes",
"LabelExample": "Exemple",
"LabelExpandSeries": "Développer la série",
+ "LabelExpandSubSeries": "Développer les sous-séries",
"LabelExplicit": "Restriction",
"LabelExplicitChecked": "Explicite (vérifié)",
"LabelExplicitUnchecked": "Non explicite (non vérifié)",
@@ -306,7 +318,9 @@
"LabelFetchingMetadata": "Récupération des métadonnées",
"LabelFile": "Fichier",
"LabelFileBirthtime": "Création du fichier",
+ "LabelFileBornDate": "Créé {0}",
"LabelFileModified": "Modification du fichier",
+ "LabelFileModifiedDate": "Modifié le {0}",
"LabelFilename": "Nom de fichier",
"LabelFilterByUser": "Filtrer par utilisateur",
"LabelFindEpisodes": "Trouver des épisodes",
@@ -346,6 +360,8 @@
"LabelIntervalEveryHour": "Toutes les heures",
"LabelInvert": "Inverser",
"LabelItem": "Élément",
+ "LabelJumpBackwardAmount": "Dans le lecteur, reculer de",
+ "LabelJumpForwardAmount": "Dans le lecteur, avancer de",
"LabelLanguage": "Langue",
"LabelLanguageDefaultServer": "Langue par défaut",
"LabelLanguages": "Langues",
@@ -366,7 +382,7 @@
"LabelLimit": "Limite",
"LabelLineSpacing": "Espacement des lignes",
"LabelListenAgain": "Écouter à nouveau",
- "LabelLogLevelDebug": "Debug",
+ "LabelLogLevelDebug": "Débogage",
"LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Rechercher les nouveaux épisodes après cette date",
@@ -407,7 +423,7 @@
"LabelNotificationBodyTemplate": "Modèle de message",
"LabelNotificationEvent": "Evènement de Notification",
"LabelNotificationTitleTemplate": "Modèle de titre",
- "LabelNotificationsMaxFailedAttempts": "Nombres de tentatives d’envoi",
+ "LabelNotificationsMaxFailedAttempts": "Nombre maximal de tentatives échouées atteint",
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Les notifications seront ignorées si la file d’attente est à son maximum. Cela empêche un flot trop important.",
@@ -443,15 +459,17 @@
"LabelPrimaryEbook": "Premier livre numérique",
"LabelProgress": "Progression",
"LabelProvider": "Fournisseur",
+ "LabelProviderAuthorizationValue": "Valeur de l’en-tête d’autorisation",
"LabelPubDate": "Date de publication",
"LabelPublishYear": "Année de publication",
+ "LabelPublishedDate": "{0} publiés",
"LabelPublisher": "Éditeur",
"LabelPublishers": "Éditeurs",
- "LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
+ "LabelRSSFeedCustomOwnerEmail": "Courriel personnalisée du propriétaire",
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedOpen": "Flux RSS ouvert",
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
- "LabelRSSFeedSlug": "Balise URL du flux RSS",
+ "LabelRSSFeedSlug": "Identifiant d’URL du flux RSS",
"LabelRSSFeedURL": "Adresse du flux RSS",
"LabelRandomly": "Au hasard",
"LabelReAddSeriesToContinueListening": "Ajouter à nouveau la série pour continuer à l’écouter",
@@ -528,7 +546,7 @@
"LabelShowSubtitles": "Afficher les sous-titres",
"LabelSize": "Taille",
"LabelSleepTimer": "Minuterie de mise en veille",
- "LabelSlug": "Balise",
+ "LabelSlug": "Identifiant d’URL",
"LabelStart": "Démarrer",
"LabelStartTime": "Heure de démarrage",
"LabelStarted": "Démarré",
@@ -590,6 +608,7 @@
"LabelUnabridged": "Version intégrale",
"LabelUndo": "Annuler",
"LabelUnknown": "Inconnu",
+ "LabelUnknownPublishDate": "Date de publication inconnue",
"LabelUpdateCover": "Mettre à jour la couverture",
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu’une correspondance est trouvée",
"LabelUpdateDetails": "Mettre à jours les détails",
@@ -638,16 +657,22 @@
"MessageCheckingCron": "Vérification du cron…",
"MessageConfirmCloseFeed": "Êtes-vous sûr de vouloir fermer ce flux ?",
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la sauvegarde de « {0} » ?",
+ "MessageConfirmDeleteDevice": "Êtes-vous sûr de vouloir supprimer la liseuse « {0} » ?",
"MessageConfirmDeleteFile": "Cela supprimera le fichier de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
"MessageConfirmDeleteLibraryItem": "Cette opération supprimera l’élément de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteLibraryItems": "Cette opération supprimera {0} éléments de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
+ "MessageConfirmDeleteMetadataProvider": "Êtes-vous sûr de vouloir supprimer le fournisseur de métadonnées personnalisées « {0} » ?",
+ "MessageConfirmDeleteNotification": "Êtes-vous sûr de vouloir supprimer cette notification ?",
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?",
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?",
+ "MessageConfirmMarkItemFinished": "Êtes-vous sûr de vouloir marquer \"{0}\" comme terminé ?",
+ "MessageConfirmMarkItemNotFinished": "Êtes-vous sûr de vouloir marquer \"{0}\" comme non terminé ?",
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?",
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme non terminés ?",
+ "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 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 ?",
@@ -666,13 +691,15 @@
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les éléments ?",
"MessageConfirmRenameTagMergeNote": "Information : Cette étiquette existe déjà et sera fusionnée.",
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
+ "MessageConfirmResetProgress": "Êtes-vous sûr de vouloir réinitialiser votre progression ?",
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer {0} livre numérique « {1} » à l'appareil « {2} » ?",
+ "MessageConfirmUnlinkOpenId": "Êtes-vous sûr de vouloir dissocier cet utilisateur d’OpenID ?",
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
"MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes",
"MessageEmbedFailed": "Échec de l’intégration !",
"MessageEmbedFinished": "Intégration terminée !",
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
- "MessageEreaderDevices": "Pour garantir l’envoie des livres électroniques, il se peut que vous deviez ajouter l’adresse électronique ci-dessus en tant qu’expéditeur valide pour chaque appareil répertorié ci-dessous.",
+ "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}",
"MessageFetching": "Récupération…",
"MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme s’ils étaient nouveaux.",
@@ -685,7 +712,7 @@
"MessageLoading": "Chargement…",
"MessageLoadingFolders": "Chargement des dossiers…",
"MessageLogsDescription": "Les journaux sont stockés dans
/metadata/logs
sous forme de fichiers JSON. Les journaux d’incidents sont stockés dans
/metadata/logs/crash_logs.txt
.",
- "MessageM4BFailed": "M4B a échoué !",
+ "MessageM4BFailed": "Échec de la conversion en M4B !",
"MessageM4BFinished": "M4B terminé !",
"MessageMapChapterTitles": "Faire correspondre les titres de chapitres avec ceux de vos livres audio existants sans ajuster les horodatages",
"MessageMarkAllEpisodesFinished": "Marquer tous les épisodes terminés",
@@ -701,6 +728,7 @@
"MessageNoCollections": "Aucune collection",
"MessageNoCoversFound": "Aucune couverture trouvée",
"MessageNoDescription": "Aucune description",
+ "MessageNoDevices": "Aucun appareil",
"MessageNoDownloadsInProgress": "Aucun téléchargement en cours",
"MessageNoDownloadsQueued": "Aucun téléchargement en attente",
"MessageNoEpisodeMatchesFound": "Aucune correspondance d’épisode trouvée",
@@ -720,7 +748,6 @@
"MessageNoSeries": "Aucune série",
"MessageNoTags": "Aucune étiquette",
"MessageNoTasksRunning": "Aucune tâche en cours",
- "MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
"MessageNoUserPlaylists": "Vous n’avez aucune liste de lecture",
"MessageNotYetImplemented": "Non implémenté",
@@ -729,6 +756,7 @@
"MessagePauseChapter": "Suspendre la lecture du chapitre",
"MessagePlayChapter": "Écouter depuis le début du chapitre",
"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",
"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",
@@ -769,26 +797,52 @@
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
"PlaceholderSearch": "Recherche…",
"PlaceholderSearchEpisode": "Recherche d’épisode…",
+ "StatsAuthorsAdded": "auteurs ajoutés",
+ "StatsBooksAdded": "livres ajoutés",
+ "StatsBooksAdditional": "Les ajouts comprennent…",
+ "StatsBooksFinished": "livres terminés",
+ "StatsBooksFinishedThisYear": "Quelques livres terminés cette année…",
+ "StatsBooksListenedTo": "livres écoutés",
+ "StatsCollectionGrewTo": "Votre collection de livres a atteint…",
+ "StatsSessions": "sessions",
+ "StatsSpentListening": "temps passé à écouter",
+ "StatsTopAuthor": "TOP AUTEUR",
+ "StatsTopAuthors": "TOP AUTEURS",
+ "StatsTopGenre": "TOP GENRE",
+ "StatsTopGenres": "TOP GENRES",
+ "StatsTopMonth": "TOP MOIS",
+ "StatsTopNarrator": "TOP NARRATEUR",
+ "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",
- "ToastAuthorImageRemoveFailed": "Échec de la suppression de l’image",
+ "ToastAppriseUrlRequired": "Vous devez entrer une URL Apprise",
"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)",
+ "ToastBackupAppliedSuccess": "Sauvegarde appliquée",
"ToastBackupCreateFailed": "Échec de la création de sauvegarde",
"ToastBackupCreateSuccess": "Sauvegarde créée",
"ToastBackupDeleteFailed": "Échec de la suppression de sauvegarde",
"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",
"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é",
- "ToastBookmarkRemoveFailed": "Échec de la suppression de signet",
"ToastBookmarkRemoveSuccess": "Signet supprimé",
"ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet",
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
@@ -796,24 +850,46 @@
"ToastCachePurgeSuccess": "Cache purgé avec succès",
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
- "ToastCollectionItemsRemoveFailed": "Échec de la suppression d’un ou plusieurs éléments de la collection",
+ "ToastChaptersRemoved": "Chapitres supprimés",
+ "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",
- "ToastCollectionRemoveFailed": "Échec de la suppression 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",
"ToastDeleteFileSuccess": "Fichier supprimé",
+ "ToastDeviceAddFailed": "Échec de l’ajout de l’appareil",
+ "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",
+ "ToastErrorCannotShare": "Impossible de partager nativement sur cet appareil",
"ToastFailedToLoadData": "Échec du chargement des données",
+ "ToastFailedToShare": "Échec du partage",
+ "ToastFailedToUpdateAccount": "Échec de la mise à jour du compte",
+ "ToastFailedToUpdateUser": "La mise a jour de l'utilisateur à échouée",
+ "ToastInvalidImageUrl": "URL de l'image invalide",
+ "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",
- "ToastItemDetailsUpdateUnneeded": "Aucune mise à jour n’est nécessaire pour les détails de l’élément",
"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",
"ToastLibraryDeleteFailed": "Échec de la suppression de la bibliothèque",
@@ -822,32 +898,78 @@
"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",
+ "ToastNameEmailRequired": "Le nom et le courriel sont requis",
+ "ToastNameRequired": "Le nom est requis",
+ "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 un tag est requis",
+ "ToastNewUserUsernameError": "Entrez un nom d’utilisateur",
+ "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",
- "ToastPlaylistRemoveFailed": "Échec de la suppression de la liste de lecture",
"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",
+ "ToastProviderCreatedFailed": "Échec de l’ajout du fournisseur",
+ "ToastProviderCreatedSuccess": "Nouveau fournisseur ajouté",
+ "ToastProviderNameAndUrlRequired": "Nom et URL requis",
+ "ToastProviderRemoveSuccess": "Fournisseur supprimé",
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
+ "ToastRemoveFailed": "Échec de la suppression",
"ToastRemoveItemFromCollectionFailed": "Échec de la suppression d’un élément de la collection",
"ToastRemoveItemFromCollectionSuccess": "Élément supprimé de la collection",
+ "ToastRemoveItemsWithIssuesFailed": "Échec de la suppression des éléments de bibliothèque présentant des problèmes",
+ "ToastRemoveItemsWithIssuesSuccess": "Éléments de bibliothèque supprimés avec des problèmes",
+ "ToastRenameFailed": "Échec du renommage",
+ "ToastRescanFailed": "Échec de la nouvelle analyse pour {0}",
+ "ToastRescanRemoved": "Nouvelle analyse terminée, l’élément a été supprimé",
+ "ToastRescanUpToDate": "Nouvelle analyse terminée, l’élément était déjà à jour",
+ "ToastRescanUpdated": "Nouvelle analyse terminée, l’élément a été mis à jour",
+ "ToastScanFailed": "Échec de l’analyse de l’élément de la bibliothèque",
+ "ToastSelectAtLeastOneUser": "Sélectionnez au moins un utilisateur",
"ToastSendEbookToDeviceFailed": "Échec de l’envoi du livre numérique à l’appareil",
"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",
+ "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",
+ "ToastUnlinkOpenIdFailed": "Échec de la dissociation de l’utilisateur l’OpenID",
+ "ToastUnlinkOpenIdSuccess": "Utilisateur dissocié de OpenID",
"ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur",
- "ToastUserDeleteSuccess": "Utilisateur supprimé"
+ "ToastUserDeleteSuccess": "Utilisateur supprimé",
+ "ToastUserPasswordChangeSuccess": "Mot de passe modifié avec succès",
+ "ToastUserPasswordMismatch": "Les mots de passe ne correspondent pas",
+ "ToastUserPasswordMustChange": "Le nouveau mot de passe ne peut pas être identique à l’ancien",
+ "ToastUserRootRequireName": "Vous devez entrer un nom d’utilisateur root"
}
diff --git a/client/strings/gu.json b/client/strings/gu.json
index c38a005f7c..f0eee434fc 100644
--- a/client/strings/gu.json
+++ b/client/strings/gu.json
@@ -9,7 +9,6 @@
"ButtonApply": "લાગુ કરો",
"ButtonApplyChapters": "પ્રકરણો લાગુ કરો",
"ButtonAuthors": "લેખકો",
- "ButtonBack": "Back",
"ButtonBrowseForFolder": "ફોલ્ડર માટે જુઓ",
"ButtonCancel": "રદ કરો",
"ButtonCancelEncode": "એન્કોડ રદ કરો",
@@ -17,7 +16,7 @@
"ButtonCheckAndDownloadNewEpisodes": "નવા એપિસોડ્સ ચેક કરો અને ડાઉનલોડ કરો",
"ButtonChooseAFolder": "ફોલ્ડર પસંદ કરો",
"ButtonChooseFiles": "ફાઇલો પસંદ કરો",
- "ButtonClearFilter": "ફિલ્ટર જતુ કરો ",
+ "ButtonClearFilter": "ફિલ્ટર જતુ કરો",
"ButtonCloseFeed": "ફીડ બંધ કરો",
"ButtonCollections": "સંગ્રહ",
"ButtonConfigureScanner": "સ્કેનર સેટિંગ બદલો",
@@ -33,8 +32,6 @@
"ButtonHide": "છુપાવો",
"ButtonHome": "ઘર",
"ButtonIssues": "સમસ્યાઓ",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
"ButtonLatest": "નવીનતમ",
"ButtonLibrary": "પુસ્તકાલય",
"ButtonLogout": "લૉગ આઉટ",
@@ -44,17 +41,12 @@
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
"ButtonNevermind": "કંઈ વાંધો નહીં",
- "ButtonNext": "Next",
- "ButtonNextChapter": "Next Chapter",
"ButtonOk": "ઓકે",
"ButtonOpenFeed": "ફીડ ખોલો",
"ButtonOpenManager": "મેનેજર ખોલો",
- "ButtonPause": "Pause",
"ButtonPlay": "ચલાવો",
"ButtonPlaying": "ચલાવી રહ્યું છે",
"ButtonPlaylists": "પ્લેલિસ્ટ",
- "ButtonPrevious": "Previous",
- "ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
"ButtonQueueAddItem": "કતારમાં ઉમેરો",
@@ -62,9 +54,6 @@
"ButtonQuickMatch": "ઝડપી મેળ ખવડાવો",
"ButtonReScan": "ફરીથી સ્કેન કરો",
"ButtonRead": "વાંચો",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
"ButtonRemove": "કાઢી નાખો",
"ButtonRemoveAll": "બધું કાઢી નાખો",
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
@@ -83,7 +72,6 @@
"ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો",
"ButtonSeries": "સિરીઝ",
"ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો",
- "ButtonShare": "Share",
"ButtonShiftTimes": "સમય શિફ્ટ કરો",
"ButtonShow": "બતાવો",
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
@@ -98,15 +86,11 @@
"ButtonUserEdit": "વપરાશકર્તા {0} સંપાદિત કરો",
"ButtonViewAll": "બધું જુઓ",
"ButtonYes": "હા",
- "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "એકાઉન્ટ",
"HeaderAdvanced": "અડ્વાન્સડ",
"HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ",
"HeaderAudioTracks": "ઓડિયો ટ્રેક્સ",
"HeaderAudiobookTools": "ઓડિયોબુક ફાઇલ વ્યવસ્થાપન ટૂલ્સ",
- "HeaderAuthentication": "Authentication",
"HeaderBackups": "બેકઅપ્સ",
"HeaderChangePassword": "પાસવર્ડ બદલો",
"HeaderChapters": "પ્રકરણો",
@@ -115,8 +99,6 @@
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
"HeaderCover": "આવરણ",
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "વિગતો",
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
"HeaderEbookFiles": "ઇબુક ફાઇલો",
@@ -148,10 +130,8 @@
"HeaderNewAccount": "નવું એકાઉન્ટ",
"HeaderNewLibrary": "નવી પુસ્તકાલય",
"HeaderNotifications": "સૂચનાઓ",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "RSS ફીડ ખોલો",
"HeaderOtherFiles": "અન્ય ફાઇલો",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "પરવાનગીઓ",
"HeaderPlayerQueue": "પ્લેયર કતાર",
"HeaderPlaylist": "પ્લેલિસ્ટ",
@@ -178,644 +158,10 @@
"HeaderStatsLongestItems": "સૌથી લાંબી વસ્તુઓ (કલાક)",
"HeaderStatsMinutesListeningChart": "સાંભળવાની મિનિટ (છેલ્લા ૭ દિવસ)",
"HeaderStatsRecentSessions": "છેલ્લી સાંભળતી સેશન્સ",
- "HeaderStatsTop10Authors": "Top 10 Authors",
- "HeaderStatsTop5Genres": "Top 5 Genres",
- "HeaderTableOfContents": "Table of Contents",
- "HeaderTools": "Tools",
- "HeaderUpdateAccount": "Update Account",
- "HeaderUpdateAuthor": "Update Author",
- "HeaderUpdateDetails": "Update Details",
- "HeaderUpdateLibrary": "Update Library",
- "HeaderUsers": "Users",
- "HeaderYearReview": "Year {0} in Review",
- "HeaderYourStats": "Your Stats",
- "LabelAbridged": "Abridged",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
- "LabelAccountType": "Account Type",
- "LabelAccountTypeAdmin": "Admin",
- "LabelAccountTypeGuest": "Guest",
- "LabelAccountTypeUser": "User",
- "LabelActivity": "Activity",
- "LabelAddToCollection": "Add to Collection",
- "LabelAddToCollectionBatch": "Add {0} Books to Collection",
- "LabelAddToPlaylist": "Add to Playlist",
- "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
- "LabelAdded": "Added",
- "LabelAddedAt": "Added At",
- "LabelAdminUsersOnly": "Admin users only",
- "LabelAll": "All",
- "LabelAllUsers": "All Users",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
- "LabelAlreadyInYourLibrary": "Already in your library",
- "LabelAppend": "Append",
- "LabelAuthor": "Author",
- "LabelAuthorFirstLast": "Author (First Last)",
- "LabelAuthorLastFirst": "Author (Last, First)",
- "LabelAuthors": "Authors",
- "LabelAutoDownloadEpisodes": "Auto Download Episodes",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
- "LabelBackToUser": "Back to User",
- "LabelBackupLocation": "Backup Location",
- "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
- "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB)",
- "LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
- "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",
- "LabelBooks": "Books",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
- "LabelChangePassword": "Change Password",
- "LabelChannels": "Channels",
- "LabelChapterTitle": "Chapter Title",
- "LabelChapters": "Chapters",
- "LabelChaptersFound": "chapters found",
- "LabelClickForMoreInfo": "Click for more info",
- "LabelClosePlayer": "Close player",
- "LabelCodec": "Codec",
- "LabelCollapseSeries": "Collapse Series",
- "LabelCollection": "Collection",
- "LabelCollections": "Collections",
- "LabelComplete": "Complete",
- "LabelConfirmPassword": "Confirm Password",
- "LabelContinueListening": "Continue Listening",
- "LabelContinueReading": "Continue Reading",
- "LabelContinueSeries": "Continue Series",
- "LabelCover": "Cover",
- "LabelCoverImageURL": "Cover Image URL",
- "LabelCreatedAt": "Created At",
- "LabelCronExpression": "Cron Expression",
- "LabelCurrent": "Current",
- "LabelCurrently": "Currently:",
- "LabelCustomCronExpression": "Custom Cron Expression:",
- "LabelDatetime": "Datetime",
- "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
- "LabelDescription": "Description",
- "LabelDeselectAll": "Deselect All",
- "LabelDevice": "Device",
- "LabelDeviceInfo": "Device Info",
- "LabelDeviceIsAvailableTo": "Device is available to...",
- "LabelDirectory": "Directory",
- "LabelDiscFromFilename": "Disc from Filename",
- "LabelDiscFromMetadata": "Disc from Metadata",
- "LabelDiscover": "Discover",
- "LabelDownload": "Download",
- "LabelDownloadNEpisodes": "Download {0} episodes",
- "LabelDuration": "Duration",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
- "LabelDurationFound": "Duration found:",
- "LabelEbook": "Ebook",
- "LabelEbooks": "Ebooks",
- "LabelEdit": "Edit",
- "LabelEmail": "Email",
- "LabelEmailSettingsFromAddress": "From Address",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
- "LabelEmailSettingsSecure": "Secure",
- "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
- "LabelEmailSettingsTestAddress": "Test Address",
- "LabelEmbeddedCover": "Embedded Cover",
- "LabelEnable": "Enable",
- "LabelEnd": "End",
- "LabelEpisode": "Episode",
- "LabelEpisodeTitle": "Episode Title",
- "LabelEpisodeType": "Episode Type",
- "LabelExample": "Example",
- "LabelExplicit": "Explicit",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
- "LabelFeedURL": "Feed URL",
- "LabelFetchingMetadata": "Fetching Metadata",
- "LabelFile": "File",
- "LabelFileBirthtime": "File Birthtime",
- "LabelFileModified": "File Modified",
- "LabelFilename": "Filename",
- "LabelFilterByUser": "Filter by User",
- "LabelFindEpisodes": "Find Episodes",
- "LabelFinished": "Finished",
- "LabelFolder": "Folder",
- "LabelFolders": "Folders",
- "LabelFontBold": "Bold",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "ફોન્ટ કુટુંબ",
- "LabelFontItalic": "Italic",
- "LabelFontScale": "Font scale",
- "LabelFontStrikethrough": "Strikethrough",
- "LabelFormat": "Format",
- "LabelGenre": "Genre",
- "LabelGenres": "Genres",
- "LabelHardDeleteFile": "Hard delete file",
- "LabelHasEbook": "Has ebook",
- "LabelHasSupplementaryEbook": "Has supplementary ebook",
- "LabelHighestPriority": "Highest priority",
- "LabelHost": "Host",
- "LabelHour": "Hour",
- "LabelIcon": "Icon",
- "LabelImageURLFromTheWeb": "Image URL from the web",
- "LabelInProgress": "In Progress",
- "LabelIncludeInTracklist": "Include in Tracklist",
- "LabelIncomplete": "Incomplete",
- "LabelInterval": "Interval",
- "LabelIntervalCustomDailyWeekly": "Custom daily/weekly",
- "LabelIntervalEvery12Hours": "Every 12 hours",
- "LabelIntervalEvery15Minutes": "Every 15 minutes",
- "LabelIntervalEvery2Hours": "Every 2 hours",
- "LabelIntervalEvery30Minutes": "Every 30 minutes",
- "LabelIntervalEvery6Hours": "Every 6 hours",
- "LabelIntervalEveryDay": "Every day",
- "LabelIntervalEveryHour": "Every hour",
- "LabelInvert": "Invert",
- "LabelItem": "Item",
- "LabelLanguage": "Language",
- "LabelLanguageDefaultServer": "Default Server Language",
- "LabelLanguages": "Languages",
- "LabelLastBookAdded": "Last Book Added",
- "LabelLastBookUpdated": "Last Book Updated",
- "LabelLastSeen": "Last Seen",
- "LabelLastTime": "Last Time",
- "LabelLastUpdate": "Last Update",
- "LabelLayout": "Layout",
- "LabelLayoutSinglePage": "Single page",
- "LabelLayoutSplitPage": "Split page",
- "LabelLess": "Less",
- "LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
- "LabelLibrary": "Library",
- "LabelLibraryFilterSublistEmpty": "No {0}",
- "LabelLibraryItem": "Library Item",
- "LabelLibraryName": "Library Name",
- "LabelLimit": "Limit",
- "LabelLineSpacing": "Line spacing",
- "LabelListenAgain": "Listen Again",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
- "LabelLogLevelWarn": "Warn",
- "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
- "LabelMediaPlayer": "Media Player",
- "LabelMediaType": "Media Type",
- "LabelMetaTag": "Meta Tag",
- "LabelMetaTags": "Meta Tags",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
- "LabelMetadataProvider": "Metadata Provider",
- "LabelMinute": "Minute",
- "LabelMissing": "Missing",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
- "LabelMore": "More",
- "LabelMoreInfo": "More Info",
- "LabelName": "Name",
- "LabelNarrator": "Narrator",
- "LabelNarrators": "Narrators",
- "LabelNew": "New",
- "LabelNewPassword": "New Password",
- "LabelNewestAuthors": "Newest Authors",
- "LabelNewestEpisodes": "Newest Episodes",
- "LabelNextBackupDate": "Next backup date",
- "LabelNextScheduledRun": "Next scheduled run",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
- "LabelNoEpisodesSelected": "No episodes selected",
- "LabelNotFinished": "Not Finished",
- "LabelNotStarted": "Not Started",
- "LabelNotes": "Notes",
- "LabelNotificationAppriseURL": "Apprise URL(s)",
- "LabelNotificationAvailableVariables": "Available variables",
- "LabelNotificationBodyTemplate": "Body Template",
- "LabelNotificationEvent": "Notification Event",
- "LabelNotificationTitleTemplate": "Title Template",
- "LabelNotificationsMaxFailedAttempts": "Max failed attempts",
- "LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times",
- "LabelNotificationsMaxQueueSize": "Max queue size for notification events",
- "LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
- "LabelNumberOfBooks": "Number of Books",
- "LabelNumberOfEpisodes": "# of Episodes",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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",
- "LabelPassword": "Password",
- "LabelPath": "Path",
- "LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
- "LabelPermissionsAccessAllTags": "Can Access All Tags",
- "LabelPermissionsAccessExplicitContent": "Can Access Explicit Content",
- "LabelPermissionsDelete": "Can Delete",
- "LabelPermissionsDownload": "Can Download",
- "LabelPermissionsUpdate": "Can Update",
- "LabelPermissionsUpload": "Can Upload",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
- "LabelPhotoPathURL": "Photo Path/URL",
- "LabelPlayMethod": "Play Method",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
- "LabelPlaylists": "Playlists",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ",
- "LabelPodcastType": "Podcast Type",
- "LabelPodcasts": "Podcasts",
- "LabelPort": "Port",
- "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
- "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
- "LabelPrimaryEbook": "Primary ebook",
- "LabelProgress": "Progress",
- "LabelProvider": "Provider",
- "LabelPubDate": "Pub Date",
- "LabelPublishYear": "Publish Year",
- "LabelPublisher": "Publisher",
- "LabelPublishers": "Publishers",
- "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
- "LabelRSSFeedCustomOwnerName": "Custom owner Name",
- "LabelRSSFeedOpen": "RSS Feed Open",
- "LabelRSSFeedPreventIndexing": "Prevent Indexing",
- "LabelRSSFeedSlug": "RSS Feed Slug",
- "LabelRSSFeedURL": "RSS Feed URL",
- "LabelRead": "Read",
- "LabelReadAgain": "Read Again",
- "LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
- "LabelRecentSeries": "Recent Series",
- "LabelRecentlyAdded": "Recently Added",
- "LabelRecommended": "Recommended",
- "LabelRedo": "Redo",
- "LabelRegion": "Region",
- "LabelReleaseDate": "Release Date",
- "LabelRemoveCover": "Remove cover",
- "LabelRowsPerPage": "Rows per page",
- "LabelSearchTerm": "Search Term",
- "LabelSearchTitle": "Search Title",
- "LabelSearchTitleOrASIN": "Search Title or ASIN",
- "LabelSeason": "Season",
- "LabelSelectAll": "Select all",
- "LabelSelectAllEpisodes": "Select all episodes",
- "LabelSelectEpisodesShowing": "Select {0} episodes showing",
- "LabelSelectUsers": "Select users",
- "LabelSendEbookToDevice": "Send Ebook to...",
- "LabelSequence": "Sequence",
- "LabelSeries": "Series",
- "LabelSeriesName": "Series Name",
- "LabelSeriesProgress": "Series Progress",
- "LabelServerYearReview": "Server Year in Review ({0})",
- "LabelSetEbookAsPrimary": "Set as primary",
- "LabelSetEbookAsSupplementary": "Set as supplementary",
- "LabelSettingsAudiobooksOnly": "Audiobooks only",
- "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
- "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
- "LabelSettingsChromecastSupport": "Chromecast support",
- "LabelSettingsDateFormat": "Date Format",
- "LabelSettingsDisableWatcher": "Disable Watcher",
- "LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library",
- "LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
- "LabelSettingsEnableWatcher": "Enable Watcher",
- "LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
- "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
- "LabelSettingsExperimentalFeatures": "Experimental features",
- "LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
- "LabelSettingsFindCovers": "Find covers",
- "LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.
Note: This will extend scan time",
- "LabelSettingsHideSingleBookSeries": "Hide single book series",
- "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",
- "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",
- "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.
Subtitle must be seperated by \" - \"
i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
- "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
- "LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
- "LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
- "LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
- "LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
- "LabelSettingsSquareBookCovers": "Use square book covers",
- "LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
- "LabelSettingsStoreCoversWithItem": "Store covers with item",
- "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
- "LabelSettingsStoreMetadataWithItem": "Store metadata with item",
- "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
- "LabelSettingsTimeFormat": "Time Format",
- "LabelShowAll": "Show All",
- "LabelShowSeconds": "Show seconds",
- "LabelSize": "Size",
- "LabelSleepTimer": "Sleep timer",
- "LabelSlug": "Slug",
- "LabelStart": "Start",
- "LabelStartTime": "Start Time",
- "LabelStarted": "Started",
- "LabelStartedAt": "Started At",
- "LabelStatsAudioTracks": "Audio Tracks",
- "LabelStatsAuthors": "Authors",
- "LabelStatsBestDay": "Best Day",
- "LabelStatsDailyAverage": "Daily Average",
- "LabelStatsDays": "Days",
- "LabelStatsDaysListened": "Days Listened",
- "LabelStatsHours": "Hours",
- "LabelStatsInARow": "in a row",
- "LabelStatsItemsFinished": "Items Finished",
- "LabelStatsItemsInLibrary": "Items in Library",
- "LabelStatsMinutes": "minutes",
- "LabelStatsMinutesListening": "Minutes Listening",
- "LabelStatsOverallDays": "Overall Days",
- "LabelStatsOverallHours": "Overall Hours",
- "LabelStatsWeekListening": "Week Listening",
- "LabelSubtitle": "Subtitle",
- "LabelSupportedFileTypes": "Supported File Types",
- "LabelTag": "Tag",
- "LabelTags": "Tags",
- "LabelTagsAccessibleToUser": "Tags Accessible to User",
- "LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
- "LabelTasks": "Tasks Running",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
- "LabelTheme": "Theme",
- "LabelThemeDark": "Dark",
- "LabelThemeLight": "Light",
- "LabelTimeBase": "Time Base",
- "LabelTimeListened": "Time Listened",
- "LabelTimeListenedToday": "Time Listened Today",
- "LabelTimeRemaining": "{0} remaining",
- "LabelTimeToShift": "Time to shift in seconds",
- "LabelTitle": "Title",
- "LabelToolsEmbedMetadata": "Embed Metadata",
- "LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
- "LabelToolsMakeM4b": "Make M4B Audiobook File",
- "LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
- "LabelToolsSplitM4b": "Split M4B to MP3's",
- "LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
- "LabelTotalDuration": "Total Duration",
- "LabelTotalTimeListened": "Total Time Listened",
- "LabelTrackFromFilename": "Track from Filename",
- "LabelTrackFromMetadata": "Track from Metadata",
- "LabelTracks": "Tracks",
- "LabelTracksMultiTrack": "Multi-track",
- "LabelTracksNone": "No tracks",
- "LabelTracksSingleTrack": "Single-track",
- "LabelType": "Type",
- "LabelUnabridged": "Unabridged",
- "LabelUndo": "Undo",
- "LabelUnknown": "Unknown",
- "LabelUpdateCover": "Update Cover",
- "LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
- "LabelUpdateDetails": "Update Details",
- "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
- "LabelUpdatedAt": "Updated At",
- "LabelUploaderDragAndDrop": "Drag & drop files or folders",
- "LabelUploaderDropFiles": "Drop files",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
- "LabelUseChapterTrack": "Use chapter track",
- "LabelUseFullTrack": "Use full track",
- "LabelUser": "User",
- "LabelUsername": "Username",
- "LabelValue": "Value",
- "LabelVersion": "Version",
- "LabelViewBookmarks": "View bookmarks",
- "LabelViewChapters": "View chapters",
- "LabelViewQueue": "View player queue",
- "LabelVolume": "Volume",
- "LabelWeekdaysToRun": "Weekdays to run",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
- "LabelYourAudiobookDuration": "Your audiobook duration",
- "LabelYourBookmarks": "Your Bookmarks",
- "LabelYourPlaylists": "Your Playlists",
- "LabelYourProgress": "Your Progress",
- "MessageAddToPlayerQueue": "Add to player queue",
- "MessageAppriseDescription": "To use this feature you will need to have an instance of
Apprise API running or an api that will handle those same requests.
The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at
http://192.168.1.1:8337
then you would put
http://192.168.1.1:8337/notify
.",
- "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in
/metadata/items
&
/metadata/authors
. Backups
do not include any files stored in your library folders.",
- "MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
- "MessageBookshelfNoCollections": "You haven't made any collections yet",
- "MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
- "MessageBookshelfNoSeries": "You have no series",
- "MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
- "MessageChapterErrorFirstNotZero": "First chapter must start at 0",
- "MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
- "MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
- "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
- "MessageCheckingCron": "Checking cron...",
- "MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
- "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
- "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
- "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
- "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
- "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
- "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?",
- "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
- "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
- "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?",
- "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}\"?",
- "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
- "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?",
- "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?",
- "MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
- "MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
- "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
- "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
- "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
- "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
- "MessageDownloadingEpisode": "Downloading episode",
- "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
- "MessageEmbedFinished": "Embed Finished!",
- "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}",
- "MessageFetching": "Fetching...",
- "MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
- "MessageImportantNotice": "Important Notice!",
- "MessageInsertChapterBelow": "Insert chapter below",
- "MessageItemsSelected": "{0} Items Selected",
- "MessageItemsUpdated": "{0} Items Updated",
- "MessageJoinUsOn": "Join us on",
- "MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
- "MessageLoading": "Loading...",
- "MessageLoadingFolders": "Loading folders...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
- "MessageM4BFailed": "M4B Failed!",
- "MessageM4BFinished": "M4B Finished!",
- "MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
- "MessageMarkAllEpisodesFinished": "Mark all episodes finished",
- "MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
- "MessageMarkAsFinished": "Mark as Finished",
- "MessageMarkAsNotFinished": "Mark as Not Finished",
- "MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
- "MessageNoAudioTracks": "No audio tracks",
- "MessageNoAuthors": "No Authors",
- "MessageNoBackups": "No Backups",
- "MessageNoBookmarks": "No Bookmarks",
- "MessageNoChapters": "No Chapters",
- "MessageNoCollections": "No Collections",
- "MessageNoCoversFound": "No Covers Found",
- "MessageNoDescription": "No description",
- "MessageNoDownloadsInProgress": "No downloads currently in progress",
- "MessageNoDownloadsQueued": "No downloads queued",
- "MessageNoEpisodeMatchesFound": "No episode matches found",
- "MessageNoEpisodes": "No Episodes",
- "MessageNoFoldersAvailable": "No Folders Available",
- "MessageNoGenres": "No Genres",
- "MessageNoIssues": "No Issues",
- "MessageNoItems": "No Items",
- "MessageNoItemsFound": "No items found",
- "MessageNoListeningSessions": "No Listening Sessions",
- "MessageNoLogs": "No Logs",
- "MessageNoMediaProgress": "No Media Progress",
- "MessageNoNotifications": "No Notifications",
- "MessageNoPodcastsFound": "No podcasts found",
- "MessageNoResults": "No Results",
- "MessageNoSearchResultsFor": "No search results for \"{0}\"",
- "MessageNoSeries": "No Series",
- "MessageNoTags": "No Tags",
- "MessageNoTasksRunning": "No Tasks Running",
- "MessageNoUpdateNecessary": "No update necessary",
- "MessageNoUpdatesWereNecessary": "No updates were necessary",
- "MessageNoUserPlaylists": "You have no playlists",
- "MessageNotYetImplemented": "Not yet implemented",
- "MessageOr": "or",
- "MessagePauseChapter": "Pause chapter playback",
- "MessagePlayChapter": "Listen to beginning of chapter",
- "MessagePlaylistCreateFromCollection": "Create playlist from collection",
- "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
- "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)",
- "MessageRemoveFromPlayerQueue": "Remove from player queue",
- "MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
- "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
- "MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
- "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
- "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.
Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.
All clients using your server will be automatically refreshed.",
- "MessageSearchResultsFor": "Search results for",
- "MessageSelected": "{0} selected",
- "MessageServerCouldNotBeReached": "Server could not be reached",
- "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
- "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
- "MessageThinking": "Thinking...",
- "MessageUploaderItemFailed": "Failed to upload",
- "MessageUploaderItemSuccess": "Successfully Uploaded!",
- "MessageUploading": "Uploading...",
- "MessageValidCronExpression": "Valid cron expression",
- "MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings",
- "MessageXLibraryIsEmpty": "{0} Library is empty!",
- "MessageYourAudiobookDurationIsLonger": "Your audiobook duration is longer than the duration found",
- "MessageYourAudiobookDurationIsShorter": "Your audiobook duration is shorter than duration found",
- "NoteChangeRootPassword": "Root user is the only user that can have an empty password",
- "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.",
- "NoteFolderPicker": "Note: folders already mapped will not be shown",
- "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
- "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
- "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.",
- "PlaceholderNewCollection": "New collection name",
- "PlaceholderNewFolderPath": "New folder path",
- "PlaceholderNewPlaylist": "New playlist name",
- "PlaceholderSearch": "Search..",
- "PlaceholderSearchEpisode": "Search episode..",
- "ToastAccountUpdateFailed": "Failed to update account",
- "ToastAccountUpdateSuccess": "Account updated",
- "ToastAuthorImageRemoveFailed": "Failed to remove image",
- "ToastAuthorImageRemoveSuccess": "Author image removed",
- "ToastAuthorUpdateFailed": "Failed to update author",
- "ToastAuthorUpdateMerged": "Author merged",
- "ToastAuthorUpdateSuccess": "Author updated",
- "ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
- "ToastBackupCreateFailed": "Failed to create backup",
- "ToastBackupCreateSuccess": "Backup created",
- "ToastBackupDeleteFailed": "Failed to delete backup",
- "ToastBackupDeleteSuccess": "Backup deleted",
- "ToastBackupRestoreFailed": "Failed to restore backup",
- "ToastBackupUploadFailed": "Failed to upload backup",
- "ToastBackupUploadSuccess": "Backup uploaded",
- "ToastBatchUpdateFailed": "Batch update failed",
- "ToastBatchUpdateSuccess": "Batch update success",
- "ToastBookmarkCreateFailed": "Failed to create bookmark",
- "ToastBookmarkCreateSuccess": "Bookmark added",
- "ToastBookmarkRemoveFailed": "Failed to remove bookmark",
- "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",
- "ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
- "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
- "ToastCollectionRemoveFailed": "Failed to remove collection",
- "ToastCollectionRemoveSuccess": "Collection removed",
- "ToastCollectionUpdateFailed": "Failed to update collection",
- "ToastCollectionUpdateSuccess": "Collection updated",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
- "ToastItemCoverUpdateFailed": "Failed to update item cover",
- "ToastItemCoverUpdateSuccess": "Item cover updated",
- "ToastItemDetailsUpdateFailed": "Failed to update item details",
- "ToastItemDetailsUpdateSuccess": "Item details updated",
- "ToastItemDetailsUpdateUnneeded": "No updates needed for item details",
- "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
- "ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
- "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
- "ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
- "ToastLibraryCreateFailed": "Failed to create library",
- "ToastLibraryCreateSuccess": "Library \"{0}\" created",
- "ToastLibraryDeleteFailed": "Failed to delete library",
- "ToastLibraryDeleteSuccess": "Library deleted",
- "ToastLibraryScanFailedToStart": "Failed to start scan",
- "ToastLibraryScanStarted": "Library scan started",
- "ToastLibraryUpdateFailed": "Failed to update library",
- "ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
- "ToastPlaylistCreateFailed": "Failed to create playlist",
- "ToastPlaylistCreateSuccess": "Playlist created",
- "ToastPlaylistRemoveFailed": "Failed to remove playlist",
- "ToastPlaylistRemoveSuccess": "Playlist removed",
- "ToastPlaylistUpdateFailed": "Failed to update playlist",
- "ToastPlaylistUpdateSuccess": "Playlist updated",
- "ToastPodcastCreateFailed": "Failed to create podcast",
- "ToastPodcastCreateSuccess": "Podcast created successfully",
- "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
- "ToastRSSFeedCloseSuccess": "RSS feed closed",
- "ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
- "ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
- "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
- "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
- "ToastSeriesUpdateFailed": "Series update failed",
- "ToastSeriesUpdateSuccess": "Series update success",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
- "ToastSessionDeleteFailed": "Failed to delete session",
- "ToastSessionDeleteSuccess": "Session deleted",
- "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)",
- "ToastUserDeleteFailed": "Failed to delete user",
- "ToastUserDeleteSuccess": "User deleted"
+ "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device"
}
diff --git a/client/strings/he.json b/client/strings/he.json
index 514639405d..0b584f916f 100644
--- a/client/strings/he.json
+++ b/client/strings/he.json
@@ -190,9 +190,6 @@
"HeaderYearReview": "שנת {0} בסקירה",
"HeaderYourStats": "הסטטיסטיקות שלך",
"LabelAbridged": "מקוצר",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "סוג חשבון",
"LabelAccountTypeAdmin": "מנהל",
"LabelAccountTypeGuest": "אורח",
@@ -202,7 +199,6 @@
"LabelAddToCollectionBatch": "הוסף {0} ספרים לאוסף",
"LabelAddToPlaylist": "הוסף לרשימת השמעה",
"LabelAddToPlaylistBatch": "הוסף {0} פריטים לרשימת השמעה",
- "LabelAdded": "נוסף",
"LabelAddedAt": "נוסף בתאריך",
"LabelAdminUsersOnly": "רק מנהלים",
"LabelAll": "הכל",
@@ -233,7 +229,6 @@
"LabelBitrate": "קצב סיביות",
"LabelBooks": "ספרים",
"LabelButtonText": "טקסט לחצן",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "שינוי סיסמה",
"LabelChannels": "ערוצים",
"LabelChapterTitle": "כותרת הפרק",
@@ -241,7 +236,6 @@
"LabelChaptersFound": "פרקים שנמצאו",
"LabelClickForMoreInfo": "לחץ למידע נוסף",
"LabelClosePlayer": "סגור נגן",
- "LabelCodec": "Codec",
"LabelCollapseSeries": "צמצום סדרה",
"LabelCollection": "אוסף",
"LabelCollections": "אוספים",
@@ -253,11 +247,8 @@
"LabelCover": "כריכה",
"LabelCoverImageURL": "כתובת התמונה ברשת",
"LabelCreatedAt": "נוצר בתאריך",
- "LabelCronExpression": "Cron Expression",
"LabelCurrent": "נוכחי",
"LabelCurrently": "כעת:",
- "LabelCustomCronExpression": "Custom Cron Expression:",
- "LabelDatetime": "Datetime",
"LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק ממסד הנתונים)",
"LabelDescription": "תיאור",
"LabelDeselectAll": "הסר בחירת כל הפריטים",
@@ -271,17 +262,12 @@
"LabelDownload": "הורד",
"LabelDownloadNEpisodes": "הורד {0} פרקים",
"LabelDuration": "משך",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "משך נמצא:",
"LabelEbook": "ספר אלקטרוני",
"LabelEbooks": "ספרים אלקטרוניים",
"LabelEdit": "עריכה",
"LabelEmail": "דואר אלקטרוני",
"LabelEmailSettingsFromAddress": "מאת",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "מאובטח",
"LabelEmailSettingsSecureHelp": "אם מופעל, החיבור ישתמש ב-TLS בעת ההתחברות לשרת. אם לא, אז TLS יהיה בשימוש אם השרת תומך בהרחבת STARTTLS. ברוב המקרים מומלץ להפעיל את הגדרה זו אם אתה מתחבר לפורט 465. לפורט 587 או 25, השאר כבוי. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "כתובת לבדיקה",
@@ -293,8 +279,6 @@
"LabelEpisodeType": "סוג הפרק",
"LabelExample": "דוגמה",
"LabelExplicit": "בוטה",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "כתובת ערוץ",
"LabelFetchingMetadata": "מושך מטא-נתונים",
"LabelFile": "קובץ",
@@ -307,7 +291,6 @@
"LabelFolder": "תיקייה",
"LabelFolders": "תיקיות",
"LabelFontBold": "מודגש",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "משפחת הפונטים",
"LabelFontItalic": "נטוי",
"LabelFontScale": "קנה מידה של הפונט",
@@ -339,7 +322,6 @@
"LabelItem": "פריט",
"LabelLanguage": "שפה",
"LabelLanguageDefaultServer": "שפת ברירת המחדל של השרת",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "הספר האחרון שנוסף",
"LabelLastBookUpdated": "הספר האחרון שעודכן",
"LabelLastSeen": "נראה לאחרונה",
@@ -351,7 +333,6 @@
"LabelLess": "פחות",
"LabelLibrariesAccessibleToUser": "ספריות נגישות למשתמש",
"LabelLibrary": "ספרייה",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "פריט ספרייה",
"LabelLibraryName": "שם הספרייה",
"LabelLimit": "מגבלה",
@@ -387,7 +368,6 @@
"LabelNewestEpisodes": "הפרקים החדשים ביותר",
"LabelNextBackupDate": "תאריך הגיבוי הבא",
"LabelNextScheduledRun": "הרצה מתוזמנת הבאה",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "לא נבחרו פרקים",
"LabelNotFinished": "לא הושלם",
"LabelNotStarted": "לא התחיל",
@@ -403,9 +383,6 @@
"LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא. הגדרה זו נועדה למנוע ספאם התראות.",
"LabelNumberOfBooks": "מספר הספרים",
"LabelNumberOfEpisodes": "מספר הפרקים",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "פתח ערוץ RSS",
"LabelOverwrite": "לשכפל",
"LabelPassword": "סיסמה",
@@ -420,7 +397,6 @@
"LabelPersonalYearReview": "השנה שלך בסקירה ({0})",
"LabelPhotoPathURL": "נתיב/URL לתמונה",
"LabelPlayMethod": "שיטת הפעלה",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "רשימות השמעה",
"LabelPodcast": "פודקאסט",
"LabelPodcastSearchRegion": "אזור חיפוש פודקאסט",
@@ -435,7 +411,6 @@
"LabelPubDate": "תאריך פרסום",
"LabelPublishYear": "שנת הפרסום",
"LabelPublisher": "מוציא לאור",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית",
"LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית",
"LabelRSSFeedOpen": "פתח ערוץ RSS",
@@ -457,7 +432,6 @@
"LabelSearchTitle": "כותרת חיפוש",
"LabelSearchTitleOrASIN": "כותרת חיפוש או ASIN",
"LabelSeason": "עונה",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "בחר את כל הפרקים",
"LabelSelectEpisodesShowing": "בחר {0} פרקים המוצגים",
"LabelSelectUsers": "בחר משתמשים",
@@ -480,8 +454,6 @@
"LabelSettingsEnableWatcher": "הפעל עוקב",
"LabelSettingsEnableWatcherForLibrary": "הפעל עוקב תיקייה עבור ספרייה",
"LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "תכונות ניסיוניות",
"LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.",
"LabelSettingsFindCovers": "מצא כריכות",
@@ -508,10 +480,8 @@
"LabelSettingsStoreMetadataWithItemHelp": "כברירת מחדל, קבצי מטה-נתונים מאוחסנים ב- /metadata/items, הפעלת ההגדרה תאחסן קבצי מטה-נתונים בתיקיית פריט שלך בספרייה",
"LabelSettingsTimeFormat": "פורמט זמן",
"LabelShowAll": "הצג הכל",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "גודל",
"LabelSleepTimer": "טיימר שינה",
- "LabelSlug": "Slug",
"LabelStart": "התחלה",
"LabelStartTime": "זמן התחלה",
"LabelStarted": "התחיל",
@@ -601,7 +571,6 @@
"MessageBookshelfNoCollections": "עדיין לא יצרת אוספים",
"MessageBookshelfNoRSSFeeds": "אין ערוצי RSS פתוחים",
"MessageBookshelfNoResultsForFilter": "אין תוצאות עבור סינון \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "אין לך סדרות",
"MessageChapterEndIsAfter": "זמן סיום הפרק אחרי סיום הספר הקולי שלך",
"MessageChapterErrorFirstNotZero": "הפרק הראשון חייב להתחיל ב-0",
@@ -621,8 +590,6 @@
"MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא הסתיימו?",
"MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כהסתיימו?",
"MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא הסתיימו?",
- "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": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך.
האם ברצונך להמשיך?",
"MessageConfirmReScanLibraryItems": "האם אתה בטוח שברצונך לסרוק מחדש {0} פריטים?",
"MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?",
@@ -644,7 +611,6 @@
"MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר ההשמעה נכון",
"MessageEmbedFinished": "ההטמעה הושלמה!",
"MessageEpisodesQueuedForDownload": "{0} פרקים בתור להורדה",
- "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": "כתובת URL של העדכון תהיה {0}",
"MessageFetching": "מושך...",
"MessageForceReScanDescription": "תבוצע סריקה מחדש כמו סריקה חדש מאפס, תגי ID3 של קבצי קול, קבצי OPF, וקבצי טקסט ייסרקו כחדשים.",
@@ -656,7 +622,6 @@
"MessageListeningSessionsInTheLastYear": "{0} מפגשי האזנה בשנה האחרונה",
"MessageLoading": "טוען...",
"MessageLoadingFolders": "טוען תיקיות...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B נכשל!",
"MessageM4BFinished": "M4B הושלם!",
"MessageMapChapterTitles": "מפה שמות פרקים לפרקי הספר השמורים שלך ללא שינוי תגי זמן",
@@ -692,7 +657,6 @@
"MessageNoSeries": "אין סדרות",
"MessageNoTags": "אין תגיות",
"MessageNoTasksRunning": "אין משימות פעילות",
- "MessageNoUpdateNecessary": "לא נדרש עדכון",
"MessageNoUpdatesWereNecessary": "לא היה צורך בעדכונים",
"MessageNoUserPlaylists": "אין לך רשימות השמעה",
"MessageNotYetImplemented": "עדיין לא מיושם",
@@ -739,7 +703,6 @@
"PlaceholderSearchEpisode": "חיפוש פרק..",
"ToastAccountUpdateFailed": "עדכון חשבון נכשל",
"ToastAccountUpdateSuccess": "חשבון עודכן בהצלחה",
- "ToastAuthorImageRemoveFailed": "הסרת התמונה של המחבר נכשלה",
"ToastAuthorImageRemoveSuccess": "תמונת המחבר הוסרה בהצלחה",
"ToastAuthorUpdateFailed": "עדכון המחבר נכשל",
"ToastAuthorUpdateMerged": "המחבר מוזג",
@@ -756,28 +719,19 @@
"ToastBatchUpdateSuccess": "עדכון קבוצתי הצליח",
"ToastBookmarkCreateFailed": "יצירת סימניה נכשלה",
"ToastBookmarkCreateSuccess": "הסימניה נוספה בהצלחה",
- "ToastBookmarkRemoveFailed": "הסרת הסימניה נכשלה",
"ToastBookmarkRemoveSuccess": "הסימניה הוסרה בהצלחה",
"ToastBookmarkUpdateFailed": "עדכון הסימניה נכשל",
"ToastBookmarkUpdateSuccess": "הסימניה עודכנה בהצלחה",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "פרקים מכילים שגיאות",
"ToastChaptersMustHaveTitles": "פרקים חייבים לכלול כותרות",
- "ToastCollectionItemsRemoveFailed": "הסרת הפריט(ים) מהאוסף נכשלה",
"ToastCollectionItemsRemoveSuccess": "הפריט(ים) הוסרו מהאוסף בהצלחה",
- "ToastCollectionRemoveFailed": "מחיקת האוסף נכשלה",
"ToastCollectionRemoveSuccess": "האוסף הוסר בהצלחה",
"ToastCollectionUpdateFailed": "עדכון האוסף נכשל",
"ToastCollectionUpdateSuccess": "האוסף עודכן בהצלחה",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "עדכון כריכת הפריט נכשל",
"ToastItemCoverUpdateSuccess": "כריכת הפריט עודכנה בהצלחה",
"ToastItemDetailsUpdateFailed": "עדכון פרטי הפריט נכשל",
"ToastItemDetailsUpdateSuccess": "פרטי הפריט עודכנו בהצלחה",
- "ToastItemDetailsUpdateUnneeded": "לא נדרשים עדכונים לפרטי הפריט",
"ToastItemMarkedAsFinishedFailed": "סימון כפריט כהושלם נכשל",
"ToastItemMarkedAsFinishedSuccess": "הפריט סומן כהושלם בהצלחה",
"ToastItemMarkedAsNotFinishedFailed": "סימון כפריט שלא הושלם נכשל",
@@ -792,7 +746,6 @@
"ToastLibraryUpdateSuccess": "הספרייה \"{0}\" עודכנה בהצלחה",
"ToastPlaylistCreateFailed": "יצירת רשימת השמעה נכשלה",
"ToastPlaylistCreateSuccess": "רשימת השמעה נוצרה בהצלחה",
- "ToastPlaylistRemoveFailed": "הסרת רשימת השמעה נכשלה",
"ToastPlaylistRemoveSuccess": "רשימת השמעה הוסרה בהצלחה",
"ToastPlaylistUpdateFailed": "עדכון רשימת השמעה נכשל",
"ToastPlaylistUpdateSuccess": "רשימת השמעה עודכנה בהצלחה",
@@ -813,9 +766,6 @@
"ToastSocketConnected": "קצה תקשורת חובר",
"ToastSocketDisconnected": "קצה תקשורת נותק",
"ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "מחיקת המשתמש נכשלה",
"ToastUserDeleteSuccess": "המשתמש נמחק בהצלחה"
}
diff --git a/client/strings/hi.json b/client/strings/hi.json
index f2112979f9..74c7ac019a 100644
--- a/client/strings/hi.json
+++ b/client/strings/hi.json
@@ -1,15 +1,11 @@
{
"ButtonAdd": "जोड़ें",
"ButtonAddChapters": "अध्याय जोड़ें",
- "ButtonAddDevice": "Add Device",
- "ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "पॉडकास्ट जोड़ें",
- "ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "अपनी पहली पुस्तकालय जोड़ें",
"ButtonApply": "लागू करें",
"ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें",
"ButtonAuthors": "लेखक",
- "ButtonBack": "Back",
"ButtonBrowseForFolder": "फ़ोल्डर खोजें",
"ButtonCancel": "रद्द करें",
"ButtonCancelEncode": "एनकोड रद्द करें",
@@ -33,8 +29,6 @@
"ButtonHide": "छुपाएं",
"ButtonHome": "घर",
"ButtonIssues": "समस्याएं",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
"ButtonLatest": "नवीनतम",
"ButtonLibrary": "पुस्तकालय",
"ButtonLogout": "लॉग आउट",
@@ -44,17 +38,12 @@
"ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें",
"ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें",
"ButtonNevermind": "कोई बात नहीं",
- "ButtonNext": "Next",
- "ButtonNextChapter": "Next Chapter",
"ButtonOk": "ठीक है",
"ButtonOpenFeed": "फ़ीड खोलें",
"ButtonOpenManager": "मैनेजर खोलें",
- "ButtonPause": "Pause",
"ButtonPlay": "चलाएँ",
"ButtonPlaying": "चल रही है",
"ButtonPlaylists": "प्लेलिस्ट्स",
- "ButtonPrevious": "Previous",
- "ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "सभी Cache मिटाएं",
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
"ButtonQueueAddItem": "क़तार में जोड़ें",
@@ -62,17 +51,12 @@
"ButtonQuickMatch": "जल्दी से समानता की तलाश करें",
"ButtonReScan": "पुन: स्कैन करें",
"ButtonRead": "पढ़ लिया",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
"ButtonRemove": "हटाएं",
"ButtonRemoveAll": "सभी हटाएं",
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
"ButtonRemoveFromContinueListening": "सुनना जारी रखें से हटाएं",
- "ButtonRemoveFromContinueReading": "Remove from Continue Reading",
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
"ButtonReset": "रीसेट करें",
- "ButtonResetToDefault": "Reset to default",
"ButtonRestore": "पुनर्स्थापित करें",
"ButtonSave": "सहेजें",
"ButtonSaveAndClose": "सहेजें और बंद करें",
@@ -83,13 +67,11 @@
"ButtonSelectFolderPath": "फ़ोल्डर का पथ चुनें",
"ButtonSeries": "सीरीज",
"ButtonSetChaptersFromTracks": "ट्रैक्स से अध्याय बनाएं",
- "ButtonShare": "Share",
"ButtonShiftTimes": "समय खिसकाए",
"ButtonShow": "दिखाएं",
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
"ButtonStartMetadataEmbed": "मेटाडेटा एम्बेडिंग शुरू करें",
"ButtonSubmit": "जमा करें",
- "ButtonTest": "Test",
"ButtonUpload": "अपलोड करें",
"ButtonUploadBackup": "बैकअप अपलोड करें",
"ButtonUploadCover": "कवर अपलोड करें",
@@ -98,724 +80,14 @@
"ButtonUserEdit": "उपयोगकर्ता {0} को संपादित करें",
"ButtonViewAll": "सभी को देखें",
"ButtonYes": "हाँ",
- "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "खाता",
"HeaderAdvanced": "विकसित",
"HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स",
- "HeaderAudioTracks": "Audio Tracks",
- "HeaderAudiobookTools": "Audiobook File Management Tools",
- "HeaderAuthentication": "Authentication",
- "HeaderBackups": "Backups",
- "HeaderChangePassword": "Change Password",
- "HeaderChapters": "Chapters",
- "HeaderChooseAFolder": "Choose a Folder",
- "HeaderCollection": "Collection",
- "HeaderCollectionItems": "Collection Items",
- "HeaderCover": "Cover",
- "HeaderCurrentDownloads": "Current Downloads",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
- "HeaderDetails": "Details",
- "HeaderDownloadQueue": "Download Queue",
- "HeaderEbookFiles": "Ebook Files",
- "HeaderEmail": "Email",
- "HeaderEmailSettings": "Email Settings",
- "HeaderEpisodes": "Episodes",
- "HeaderEreaderDevices": "Ereader Devices",
- "HeaderEreaderSettings": "Ereader Settings",
- "HeaderFiles": "Files",
- "HeaderFindChapters": "Find Chapters",
- "HeaderIgnoredFiles": "Ignored Files",
- "HeaderItemFiles": "Item Files",
- "HeaderItemMetadataUtils": "Item Metadata Utils",
- "HeaderLastListeningSession": "Last Listening Session",
- "HeaderLatestEpisodes": "Latest episodes",
- "HeaderLibraries": "Libraries",
- "HeaderLibraryFiles": "Library Files",
- "HeaderLibraryStats": "Library Stats",
- "HeaderListeningSessions": "Listening Sessions",
- "HeaderListeningStats": "Listening Stats",
- "HeaderLogin": "Login",
- "HeaderLogs": "Logs",
- "HeaderManageGenres": "Manage Genres",
- "HeaderManageTags": "Manage Tags",
- "HeaderMapDetails": "Map details",
- "HeaderMatch": "Match",
- "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
- "HeaderMetadataToEmbed": "Metadata to embed",
- "HeaderNewAccount": "New Account",
- "HeaderNewLibrary": "New Library",
- "HeaderNotifications": "Notifications",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
- "HeaderOpenRSSFeed": "Open RSS Feed",
- "HeaderOtherFiles": "Other Files",
- "HeaderPasswordAuthentication": "Password Authentication",
- "HeaderPermissions": "Permissions",
- "HeaderPlayerQueue": "Player Queue",
- "HeaderPlaylist": "Playlist",
- "HeaderPlaylistItems": "Playlist Items",
- "HeaderPodcastsToAdd": "Podcasts to Add",
- "HeaderPreviewCover": "Preview Cover",
- "HeaderRSSFeedGeneral": "RSS Details",
- "HeaderRSSFeedIsOpen": "RSS Feed is Open",
- "HeaderRSSFeeds": "RSS Feeds",
- "HeaderRemoveEpisode": "Remove Episode",
- "HeaderRemoveEpisodes": "Remove {0} Episodes",
- "HeaderSavedMediaProgress": "Saved Media Progress",
- "HeaderSchedule": "Schedule",
- "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
- "HeaderSession": "Session",
- "HeaderSetBackupSchedule": "Set Backup Schedule",
- "HeaderSettings": "Settings",
- "HeaderSettingsDisplay": "Display",
- "HeaderSettingsExperimental": "Experimental Features",
- "HeaderSettingsGeneral": "General",
- "HeaderSettingsScanner": "Scanner",
- "HeaderSleepTimer": "Sleep Timer",
- "HeaderStatsLargestItems": "Largest Items",
- "HeaderStatsLongestItems": "Longest Items (hrs)",
- "HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
- "HeaderStatsRecentSessions": "Recent Sessions",
- "HeaderStatsTop10Authors": "Top 10 Authors",
- "HeaderStatsTop5Genres": "Top 5 Genres",
- "HeaderTableOfContents": "Table of Contents",
- "HeaderTools": "Tools",
- "HeaderUpdateAccount": "Update Account",
- "HeaderUpdateAuthor": "Update Author",
- "HeaderUpdateDetails": "Update Details",
- "HeaderUpdateLibrary": "Update Library",
- "HeaderUsers": "Users",
- "HeaderYearReview": "Year {0} in Review",
- "HeaderYourStats": "Your Stats",
- "LabelAbridged": "Abridged",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
- "LabelAccountType": "Account Type",
- "LabelAccountTypeAdmin": "Admin",
- "LabelAccountTypeGuest": "Guest",
- "LabelAccountTypeUser": "User",
- "LabelActivity": "Activity",
- "LabelAddToCollection": "Add to Collection",
- "LabelAddToCollectionBatch": "Add {0} Books to Collection",
- "LabelAddToPlaylist": "Add to Playlist",
- "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
- "LabelAdded": "Added",
- "LabelAddedAt": "Added At",
- "LabelAdminUsersOnly": "Admin users only",
- "LabelAll": "All",
- "LabelAllUsers": "All Users",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
- "LabelAlreadyInYourLibrary": "Already in your library",
- "LabelAppend": "Append",
- "LabelAuthor": "Author",
- "LabelAuthorFirstLast": "Author (First Last)",
- "LabelAuthorLastFirst": "Author (Last, First)",
- "LabelAuthors": "Authors",
- "LabelAutoDownloadEpisodes": "Auto Download Episodes",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
- "LabelBackToUser": "Back to User",
- "LabelBackupLocation": "Backup Location",
- "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
- "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB)",
- "LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
- "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",
- "LabelBooks": "Books",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
- "LabelChangePassword": "Change Password",
- "LabelChannels": "Channels",
- "LabelChapterTitle": "Chapter Title",
- "LabelChapters": "Chapters",
- "LabelChaptersFound": "chapters found",
- "LabelClickForMoreInfo": "Click for more info",
- "LabelClosePlayer": "Close player",
- "LabelCodec": "Codec",
- "LabelCollapseSeries": "Collapse Series",
- "LabelCollection": "Collection",
- "LabelCollections": "Collections",
- "LabelComplete": "Complete",
- "LabelConfirmPassword": "Confirm Password",
- "LabelContinueListening": "Continue Listening",
- "LabelContinueReading": "Continue Reading",
- "LabelContinueSeries": "Continue Series",
- "LabelCover": "Cover",
- "LabelCoverImageURL": "Cover Image URL",
- "LabelCreatedAt": "Created At",
- "LabelCronExpression": "Cron Expression",
- "LabelCurrent": "Current",
- "LabelCurrently": "Currently:",
- "LabelCustomCronExpression": "Custom Cron Expression:",
- "LabelDatetime": "Datetime",
- "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
- "LabelDescription": "Description",
- "LabelDeselectAll": "Deselect All",
- "LabelDevice": "Device",
- "LabelDeviceInfo": "Device Info",
- "LabelDeviceIsAvailableTo": "Device is available to...",
- "LabelDirectory": "Directory",
- "LabelDiscFromFilename": "Disc from Filename",
- "LabelDiscFromMetadata": "Disc from Metadata",
- "LabelDiscover": "Discover",
- "LabelDownload": "Download",
- "LabelDownloadNEpisodes": "Download {0} episodes",
- "LabelDuration": "Duration",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
- "LabelDurationFound": "Duration found:",
- "LabelEbook": "Ebook",
- "LabelEbooks": "Ebooks",
- "LabelEdit": "Edit",
- "LabelEmail": "Email",
- "LabelEmailSettingsFromAddress": "From Address",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
- "LabelEmailSettingsSecure": "Secure",
- "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
- "LabelEmailSettingsTestAddress": "Test Address",
- "LabelEmbeddedCover": "Embedded Cover",
- "LabelEnable": "Enable",
- "LabelEnd": "End",
- "LabelEpisode": "Episode",
- "LabelEpisodeTitle": "Episode Title",
- "LabelEpisodeType": "Episode Type",
- "LabelExample": "Example",
- "LabelExplicit": "Explicit",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
- "LabelFeedURL": "Feed URL",
- "LabelFetchingMetadata": "Fetching Metadata",
- "LabelFile": "File",
- "LabelFileBirthtime": "File Birthtime",
- "LabelFileModified": "File Modified",
- "LabelFilename": "Filename",
- "LabelFilterByUser": "Filter by User",
- "LabelFindEpisodes": "Find Episodes",
- "LabelFinished": "Finished",
- "LabelFolder": "Folder",
- "LabelFolders": "Folders",
- "LabelFontBold": "Bold",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "फुहारा परिवार",
- "LabelFontItalic": "Italic",
- "LabelFontScale": "Font scale",
- "LabelFontStrikethrough": "Strikethrough",
- "LabelFormat": "Format",
- "LabelGenre": "Genre",
- "LabelGenres": "Genres",
- "LabelHardDeleteFile": "Hard delete file",
- "LabelHasEbook": "Has ebook",
- "LabelHasSupplementaryEbook": "Has supplementary ebook",
- "LabelHighestPriority": "Highest priority",
- "LabelHost": "Host",
- "LabelHour": "Hour",
- "LabelIcon": "Icon",
- "LabelImageURLFromTheWeb": "Image URL from the web",
- "LabelInProgress": "In Progress",
- "LabelIncludeInTracklist": "Include in Tracklist",
- "LabelIncomplete": "Incomplete",
- "LabelInterval": "Interval",
- "LabelIntervalCustomDailyWeekly": "Custom daily/weekly",
- "LabelIntervalEvery12Hours": "Every 12 hours",
- "LabelIntervalEvery15Minutes": "Every 15 minutes",
- "LabelIntervalEvery2Hours": "Every 2 hours",
- "LabelIntervalEvery30Minutes": "Every 30 minutes",
- "LabelIntervalEvery6Hours": "Every 6 hours",
- "LabelIntervalEveryDay": "Every day",
- "LabelIntervalEveryHour": "Every hour",
- "LabelInvert": "Invert",
- "LabelItem": "Item",
- "LabelLanguage": "Language",
- "LabelLanguageDefaultServer": "Default Server Language",
- "LabelLanguages": "Languages",
- "LabelLastBookAdded": "Last Book Added",
- "LabelLastBookUpdated": "Last Book Updated",
- "LabelLastSeen": "Last Seen",
- "LabelLastTime": "Last Time",
- "LabelLastUpdate": "Last Update",
- "LabelLayout": "Layout",
- "LabelLayoutSinglePage": "Single page",
- "LabelLayoutSplitPage": "Split page",
- "LabelLess": "Less",
- "LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
- "LabelLibrary": "Library",
- "LabelLibraryFilterSublistEmpty": "No {0}",
- "LabelLibraryItem": "Library Item",
- "LabelLibraryName": "Library Name",
- "LabelLimit": "Limit",
- "LabelLineSpacing": "Line spacing",
- "LabelListenAgain": "Listen Again",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
- "LabelLogLevelWarn": "Warn",
- "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
- "LabelMediaPlayer": "Media Player",
- "LabelMediaType": "Media Type",
- "LabelMetaTag": "Meta Tag",
- "LabelMetaTags": "Meta Tags",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
- "LabelMetadataProvider": "Metadata Provider",
- "LabelMinute": "Minute",
- "LabelMissing": "Missing",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
- "LabelMore": "More",
- "LabelMoreInfo": "More Info",
- "LabelName": "Name",
- "LabelNarrator": "Narrator",
- "LabelNarrators": "Narrators",
- "LabelNew": "New",
- "LabelNewPassword": "New Password",
- "LabelNewestAuthors": "Newest Authors",
- "LabelNewestEpisodes": "Newest Episodes",
- "LabelNextBackupDate": "Next backup date",
- "LabelNextScheduledRun": "Next scheduled run",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
- "LabelNoEpisodesSelected": "No episodes selected",
- "LabelNotFinished": "Not Finished",
- "LabelNotStarted": "Not Started",
- "LabelNotes": "Notes",
- "LabelNotificationAppriseURL": "Apprise URL(s)",
- "LabelNotificationAvailableVariables": "Available variables",
- "LabelNotificationBodyTemplate": "Body Template",
- "LabelNotificationEvent": "Notification Event",
- "LabelNotificationTitleTemplate": "Title Template",
- "LabelNotificationsMaxFailedAttempts": "Max failed attempts",
- "LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times",
- "LabelNotificationsMaxQueueSize": "Max queue size for notification events",
- "LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
- "LabelNumberOfBooks": "Number of Books",
- "LabelNumberOfEpisodes": "# of Episodes",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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",
- "LabelPassword": "Password",
- "LabelPath": "Path",
- "LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
- "LabelPermissionsAccessAllTags": "Can Access All Tags",
- "LabelPermissionsAccessExplicitContent": "Can Access Explicit Content",
- "LabelPermissionsDelete": "Can Delete",
- "LabelPermissionsDownload": "Can Download",
- "LabelPermissionsUpdate": "Can Update",
- "LabelPermissionsUpload": "Can Upload",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
- "LabelPhotoPathURL": "Photo Path/URL",
- "LabelPlayMethod": "Play Method",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
- "LabelPlaylists": "Playlists",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र",
- "LabelPodcastType": "Podcast Type",
- "LabelPodcasts": "Podcasts",
- "LabelPort": "Port",
- "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
- "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
- "LabelPrimaryEbook": "Primary ebook",
- "LabelProgress": "Progress",
- "LabelProvider": "Provider",
- "LabelPubDate": "Pub Date",
- "LabelPublishYear": "Publish Year",
- "LabelPublisher": "Publisher",
- "LabelPublishers": "Publishers",
- "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
- "LabelRSSFeedCustomOwnerName": "Custom owner Name",
- "LabelRSSFeedOpen": "RSS Feed Open",
- "LabelRSSFeedPreventIndexing": "Prevent Indexing",
- "LabelRSSFeedSlug": "RSS Feed Slug",
- "LabelRSSFeedURL": "RSS Feed URL",
- "LabelRead": "Read",
- "LabelReadAgain": "Read Again",
- "LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
- "LabelRecentSeries": "Recent Series",
- "LabelRecentlyAdded": "Recently Added",
- "LabelRecommended": "Recommended",
- "LabelRedo": "Redo",
- "LabelRegion": "Region",
- "LabelReleaseDate": "Release Date",
- "LabelRemoveCover": "Remove cover",
- "LabelRowsPerPage": "Rows per page",
- "LabelSearchTerm": "Search Term",
- "LabelSearchTitle": "Search Title",
- "LabelSearchTitleOrASIN": "Search Title or ASIN",
- "LabelSeason": "Season",
- "LabelSelectAll": "Select all",
- "LabelSelectAllEpisodes": "Select all episodes",
- "LabelSelectEpisodesShowing": "Select {0} episodes showing",
- "LabelSelectUsers": "Select users",
- "LabelSendEbookToDevice": "Send Ebook to...",
- "LabelSequence": "Sequence",
- "LabelSeries": "Series",
- "LabelSeriesName": "Series Name",
- "LabelSeriesProgress": "Series Progress",
- "LabelServerYearReview": "Server Year in Review ({0})",
- "LabelSetEbookAsPrimary": "Set as primary",
- "LabelSetEbookAsSupplementary": "Set as supplementary",
- "LabelSettingsAudiobooksOnly": "Audiobooks only",
- "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
- "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
- "LabelSettingsChromecastSupport": "Chromecast support",
- "LabelSettingsDateFormat": "Date Format",
- "LabelSettingsDisableWatcher": "Disable Watcher",
- "LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library",
- "LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
- "LabelSettingsEnableWatcher": "Enable Watcher",
- "LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
- "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
- "LabelSettingsExperimentalFeatures": "Experimental features",
- "LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
- "LabelSettingsFindCovers": "Find covers",
- "LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.
Note: This will extend scan time",
- "LabelSettingsHideSingleBookSeries": "Hide single book series",
- "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",
- "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",
- "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.
Subtitle must be seperated by \" - \"
i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
- "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
- "LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
- "LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
- "LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
- "LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
- "LabelSettingsSquareBookCovers": "Use square book covers",
- "LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
- "LabelSettingsStoreCoversWithItem": "Store covers with item",
- "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
- "LabelSettingsStoreMetadataWithItem": "Store metadata with item",
- "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
- "LabelSettingsTimeFormat": "Time Format",
- "LabelShowAll": "Show All",
- "LabelShowSeconds": "Show seconds",
- "LabelSize": "Size",
- "LabelSleepTimer": "Sleep timer",
- "LabelSlug": "Slug",
- "LabelStart": "Start",
- "LabelStartTime": "Start Time",
- "LabelStarted": "Started",
- "LabelStartedAt": "Started At",
- "LabelStatsAudioTracks": "Audio Tracks",
- "LabelStatsAuthors": "Authors",
- "LabelStatsBestDay": "Best Day",
- "LabelStatsDailyAverage": "Daily Average",
- "LabelStatsDays": "Days",
- "LabelStatsDaysListened": "Days Listened",
- "LabelStatsHours": "Hours",
- "LabelStatsInARow": "in a row",
- "LabelStatsItemsFinished": "Items Finished",
- "LabelStatsItemsInLibrary": "Items in Library",
- "LabelStatsMinutes": "minutes",
- "LabelStatsMinutesListening": "Minutes Listening",
- "LabelStatsOverallDays": "Overall Days",
- "LabelStatsOverallHours": "Overall Hours",
- "LabelStatsWeekListening": "Week Listening",
- "LabelSubtitle": "Subtitle",
- "LabelSupportedFileTypes": "Supported File Types",
- "LabelTag": "Tag",
- "LabelTags": "Tags",
- "LabelTagsAccessibleToUser": "Tags Accessible to User",
- "LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
- "LabelTasks": "Tasks Running",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
- "LabelTheme": "Theme",
- "LabelThemeDark": "Dark",
- "LabelThemeLight": "Light",
- "LabelTimeBase": "Time Base",
- "LabelTimeListened": "Time Listened",
- "LabelTimeListenedToday": "Time Listened Today",
- "LabelTimeRemaining": "{0} remaining",
- "LabelTimeToShift": "Time to shift in seconds",
- "LabelTitle": "Title",
- "LabelToolsEmbedMetadata": "Embed Metadata",
- "LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
- "LabelToolsMakeM4b": "Make M4B Audiobook File",
- "LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
- "LabelToolsSplitM4b": "Split M4B to MP3's",
- "LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
- "LabelTotalDuration": "Total Duration",
- "LabelTotalTimeListened": "Total Time Listened",
- "LabelTrackFromFilename": "Track from Filename",
- "LabelTrackFromMetadata": "Track from Metadata",
- "LabelTracks": "Tracks",
- "LabelTracksMultiTrack": "Multi-track",
- "LabelTracksNone": "No tracks",
- "LabelTracksSingleTrack": "Single-track",
- "LabelType": "Type",
- "LabelUnabridged": "Unabridged",
- "LabelUndo": "Undo",
- "LabelUnknown": "Unknown",
- "LabelUpdateCover": "Update Cover",
- "LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
- "LabelUpdateDetails": "Update Details",
- "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
- "LabelUpdatedAt": "Updated At",
- "LabelUploaderDragAndDrop": "Drag & drop files or folders",
- "LabelUploaderDropFiles": "Drop files",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
- "LabelUseChapterTrack": "Use chapter track",
- "LabelUseFullTrack": "Use full track",
- "LabelUser": "User",
- "LabelUsername": "Username",
- "LabelValue": "Value",
- "LabelVersion": "Version",
- "LabelViewBookmarks": "View bookmarks",
- "LabelViewChapters": "View chapters",
- "LabelViewQueue": "View player queue",
- "LabelVolume": "Volume",
- "LabelWeekdaysToRun": "Weekdays to run",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
- "LabelYourAudiobookDuration": "Your audiobook duration",
- "LabelYourBookmarks": "Your Bookmarks",
- "LabelYourPlaylists": "Your Playlists",
- "LabelYourProgress": "Your Progress",
- "MessageAddToPlayerQueue": "Add to player queue",
- "MessageAppriseDescription": "To use this feature you will need to have an instance of
Apprise API running or an api that will handle those same requests.
The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at
http://192.168.1.1:8337
then you would put
http://192.168.1.1:8337/notify
.",
- "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in
/metadata/items
&
/metadata/authors
. Backups
do not include any files stored in your library folders.",
- "MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
- "MessageBookshelfNoCollections": "You haven't made any collections yet",
- "MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
- "MessageBookshelfNoSeries": "You have no series",
- "MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
- "MessageChapterErrorFirstNotZero": "First chapter must start at 0",
- "MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
- "MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
- "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
- "MessageCheckingCron": "Checking cron...",
- "MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
- "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
- "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
- "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
- "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
- "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
- "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?",
- "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
- "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
- "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?",
- "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}\"?",
- "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
- "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?",
- "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?",
- "MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
- "MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
- "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
- "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
- "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
- "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
- "MessageDownloadingEpisode": "Downloading episode",
- "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
- "MessageEmbedFinished": "Embed Finished!",
- "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}",
- "MessageFetching": "Fetching...",
- "MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
- "MessageImportantNotice": "Important Notice!",
- "MessageInsertChapterBelow": "Insert chapter below",
- "MessageItemsSelected": "{0} Items Selected",
- "MessageItemsUpdated": "{0} Items Updated",
- "MessageJoinUsOn": "Join us on",
- "MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
- "MessageLoading": "Loading...",
- "MessageLoadingFolders": "Loading folders...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
- "MessageM4BFailed": "M4B Failed!",
- "MessageM4BFinished": "M4B Finished!",
- "MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
- "MessageMarkAllEpisodesFinished": "Mark all episodes finished",
- "MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
- "MessageMarkAsFinished": "Mark as Finished",
- "MessageMarkAsNotFinished": "Mark as Not Finished",
- "MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
- "MessageNoAudioTracks": "No audio tracks",
- "MessageNoAuthors": "No Authors",
- "MessageNoBackups": "No Backups",
- "MessageNoBookmarks": "No Bookmarks",
- "MessageNoChapters": "No Chapters",
- "MessageNoCollections": "No Collections",
- "MessageNoCoversFound": "No Covers Found",
- "MessageNoDescription": "No description",
- "MessageNoDownloadsInProgress": "No downloads currently in progress",
- "MessageNoDownloadsQueued": "No downloads queued",
- "MessageNoEpisodeMatchesFound": "No episode matches found",
- "MessageNoEpisodes": "No Episodes",
- "MessageNoFoldersAvailable": "No Folders Available",
- "MessageNoGenres": "No Genres",
- "MessageNoIssues": "No Issues",
- "MessageNoItems": "No Items",
- "MessageNoItemsFound": "No items found",
- "MessageNoListeningSessions": "No Listening Sessions",
- "MessageNoLogs": "No Logs",
- "MessageNoMediaProgress": "No Media Progress",
- "MessageNoNotifications": "No Notifications",
- "MessageNoPodcastsFound": "No podcasts found",
- "MessageNoResults": "No Results",
- "MessageNoSearchResultsFor": "No search results for \"{0}\"",
- "MessageNoSeries": "No Series",
- "MessageNoTags": "No Tags",
- "MessageNoTasksRunning": "No Tasks Running",
- "MessageNoUpdateNecessary": "No update necessary",
- "MessageNoUpdatesWereNecessary": "No updates were necessary",
- "MessageNoUserPlaylists": "You have no playlists",
- "MessageNotYetImplemented": "Not yet implemented",
- "MessageOr": "or",
- "MessagePauseChapter": "Pause chapter playback",
- "MessagePlayChapter": "Listen to beginning of chapter",
- "MessagePlaylistCreateFromCollection": "Create playlist from collection",
- "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
- "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)",
- "MessageRemoveFromPlayerQueue": "Remove from player queue",
- "MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
- "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
- "MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
- "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
- "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.
Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.
All clients using your server will be automatically refreshed.",
- "MessageSearchResultsFor": "Search results for",
- "MessageSelected": "{0} selected",
- "MessageServerCouldNotBeReached": "Server could not be reached",
- "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
- "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
- "MessageThinking": "Thinking...",
- "MessageUploaderItemFailed": "Failed to upload",
- "MessageUploaderItemSuccess": "Successfully Uploaded!",
- "MessageUploading": "Uploading...",
- "MessageValidCronExpression": "Valid cron expression",
- "MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings",
- "MessageXLibraryIsEmpty": "{0} Library is empty!",
- "MessageYourAudiobookDurationIsLonger": "Your audiobook duration is longer than the duration found",
- "MessageYourAudiobookDurationIsShorter": "Your audiobook duration is shorter than duration found",
"NoteChangeRootPassword": "रूट user is the only user that can have an empty password",
- "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.",
- "NoteFolderPicker": "Note: folders already mapped will not be shown",
- "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
- "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
- "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.",
- "PlaceholderNewCollection": "New collection name",
- "PlaceholderNewFolderPath": "New folder path",
- "PlaceholderNewPlaylist": "New playlist name",
- "PlaceholderSearch": "Search..",
- "PlaceholderSearchEpisode": "Search episode..",
- "ToastAccountUpdateFailed": "Failed to update account",
- "ToastAccountUpdateSuccess": "Account updated",
- "ToastAuthorImageRemoveFailed": "Failed to remove image",
- "ToastAuthorImageRemoveSuccess": "Author image removed",
- "ToastAuthorUpdateFailed": "Failed to update author",
- "ToastAuthorUpdateMerged": "Author merged",
- "ToastAuthorUpdateSuccess": "Author updated",
- "ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
- "ToastBackupCreateFailed": "Failed to create backup",
- "ToastBackupCreateSuccess": "Backup created",
- "ToastBackupDeleteFailed": "Failed to delete backup",
- "ToastBackupDeleteSuccess": "Backup deleted",
- "ToastBackupRestoreFailed": "Failed to restore backup",
- "ToastBackupUploadFailed": "Failed to upload backup",
- "ToastBackupUploadSuccess": "Backup uploaded",
- "ToastBatchUpdateFailed": "Batch update failed",
- "ToastBatchUpdateSuccess": "Batch update success",
- "ToastBookmarkCreateFailed": "Failed to create bookmark",
- "ToastBookmarkCreateSuccess": "Bookmark added",
- "ToastBookmarkRemoveFailed": "Failed to remove bookmark",
- "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",
- "ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
- "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
- "ToastCollectionRemoveFailed": "Failed to remove collection",
- "ToastCollectionRemoveSuccess": "Collection removed",
- "ToastCollectionUpdateFailed": "Failed to update collection",
- "ToastCollectionUpdateSuccess": "Collection updated",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
- "ToastItemCoverUpdateFailed": "Failed to update item cover",
- "ToastItemCoverUpdateSuccess": "Item cover updated",
- "ToastItemDetailsUpdateFailed": "Failed to update item details",
- "ToastItemDetailsUpdateSuccess": "Item details updated",
- "ToastItemDetailsUpdateUnneeded": "No updates needed for item details",
- "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
- "ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
- "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
- "ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
- "ToastLibraryCreateFailed": "Failed to create library",
- "ToastLibraryCreateSuccess": "Library \"{0}\" created",
- "ToastLibraryDeleteFailed": "Failed to delete library",
- "ToastLibraryDeleteSuccess": "Library deleted",
- "ToastLibraryScanFailedToStart": "Failed to start scan",
- "ToastLibraryScanStarted": "Library scan started",
- "ToastLibraryUpdateFailed": "Failed to update library",
- "ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
- "ToastPlaylistCreateFailed": "Failed to create playlist",
- "ToastPlaylistCreateSuccess": "Playlist created",
- "ToastPlaylistRemoveFailed": "Failed to remove playlist",
- "ToastPlaylistRemoveSuccess": "Playlist removed",
- "ToastPlaylistUpdateFailed": "Failed to update playlist",
- "ToastPlaylistUpdateSuccess": "Playlist updated",
- "ToastPodcastCreateFailed": "Failed to create podcast",
- "ToastPodcastCreateSuccess": "Podcast created successfully",
- "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
- "ToastRSSFeedCloseSuccess": "RSS feed closed",
- "ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
- "ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
- "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
- "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
- "ToastSeriesUpdateFailed": "Series update failed",
- "ToastSeriesUpdateSuccess": "Series update success",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
- "ToastSessionDeleteFailed": "Failed to delete session",
- "ToastSessionDeleteSuccess": "Session deleted",
- "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)",
- "ToastUserDeleteFailed": "Failed to delete user",
- "ToastUserDeleteSuccess": "User deleted"
+ "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device"
}
diff --git a/client/strings/hr.json b/client/strings/hr.json
index ebe3207cfb..72058d7082 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -2,820 +2,974 @@
"ButtonAdd": "Dodaj",
"ButtonAddChapters": "Dodaj poglavlja",
"ButtonAddDevice": "Dodaj uređaj",
- "ButtonAddLibrary": "Dodaj biblioteku",
- "ButtonAddPodcasts": "Dodaj podkaste",
+ "ButtonAddLibrary": "Dodaj knjižnicu",
+ "ButtonAddPodcasts": "Dodaj podcaste",
"ButtonAddUser": "Dodaj korisnika",
- "ButtonAddYourFirstLibrary": "Dodaj svoju prvu biblioteku",
+ "ButtonAddYourFirstLibrary": "Dodaj svoju prvu knjižnicu",
"ButtonApply": "Primijeni",
"ButtonApplyChapters": "Primijeni poglavlja",
"ButtonAuthors": "Autori",
"ButtonBack": "Natrag",
- "ButtonBrowseForFolder": "Browse for Folder",
+ "ButtonBrowseForFolder": "Pronađi mapu",
"ButtonCancel": "Odustani",
"ButtonCancelEncode": "Otkaži kodiranje",
- "ButtonChangeRootPassword": "Promijeni Root lozinku",
+ "ButtonChangeRootPassword": "Promijeni zaporku root korisnika",
"ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove epizode",
- "ButtonChooseAFolder": "Odaberi folder",
+ "ButtonChooseAFolder": "Odaberi mapu",
"ButtonChooseFiles": "Odaberi datoteke",
"ButtonClearFilter": "Poništi filter",
- "ButtonCloseFeed": "Zatvori feed",
- "ButtonCollections": "Kolekcije",
- "ButtonConfigureScanner": "Configure Scanner",
- "ButtonCreate": "Napravi",
- "ButtonCreateBackup": "Napravi backup",
- "ButtonDelete": "Obriši",
- "ButtonDownloadQueue": "Queue",
+ "ButtonCloseFeed": "Zatvori izvor",
+ "ButtonCloseSession": "Zatvori otvorenu sesiju",
+ "ButtonCollections": "Zbirke",
+ "ButtonConfigureScanner": "Postavi skener",
+ "ButtonCreate": "Izradi",
+ "ButtonCreateBackup": "Izradi sigurnosnu kopiju",
+ "ButtonDelete": "Izbriši",
+ "ButtonDownloadQueue": "Red",
"ButtonEdit": "Uredi",
"ButtonEditChapters": "Uredi poglavlja",
"ButtonEditPodcast": "Uredi podcast",
+ "ButtonEnable": "Omogući",
+ "ButtonFireAndFail": "Okini i vrati status neuspješno",
+ "ButtonFireOnTest": "Okini onTest događaj",
"ButtonForceReScan": "Prisilno ponovno skeniranje",
"ButtonFullPath": "Cijela putanja",
"ButtonHide": "Sakrij",
"ButtonHome": "Početna stranica",
"ButtonIssues": "Problemi",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
+ "ButtonJumpBackward": "Skok unatrag",
+ "ButtonJumpForward": "Skok unaprijed",
"ButtonLatest": "Najnovije",
- "ButtonLibrary": "Biblioteka",
+ "ButtonLibrary": "Knjižnica",
"ButtonLogout": "Odjavi se",
"ButtonLookup": "Potraži",
- "ButtonManageTracks": "Upravljanje pjesmama",
- "ButtonMapChapterTitles": "Mapiraj imena poglavlja",
- "ButtonMatchAllAuthors": "Matchaj sve autore",
- "ButtonMatchBooks": "Matchaj knjige",
+ "ButtonManageTracks": "Upravljanje zvučnim zapisima",
+ "ButtonMapChapterTitles": "Mapiraj naslove poglavlja",
+ "ButtonMatchAllAuthors": "Prepoznaj sve autore",
+ "ButtonMatchBooks": "Prepoznaj knjige",
"ButtonNevermind": "Nije bitno",
- "ButtonNext": "Next",
- "ButtonNextChapter": "Next Chapter",
- "ButtonOk": "Ok",
- "ButtonOpenFeed": "Otvori feed",
- "ButtonOpenManager": "Otvori menadžera",
- "ButtonPause": "Pause",
- "ButtonPlay": "Pokreni",
- "ButtonPlaying": "pušteno",
- "ButtonPlaylists": "plejliste",
- "ButtonPrevious": "Previous",
- "ButtonPreviousChapter": "Previous Chapter",
- "ButtonPurgeAllCache": "Isprazni sav cache",
- "ButtonPurgeItemsCache": "Isprazni Items Cache",
- "ButtonQueueAddItem": "Add to queue",
- "ButtonQueueRemoveItem": "Remove from queue",
- "ButtonQuickMatch": "Brzi match",
- "ButtonReScan": "Skeniraj ponovno",
+ "ButtonNext": "Sljedeće",
+ "ButtonNextChapter": "Sljedeće poglavlje",
+ "ButtonNextItemInQueue": "Sljedeća stavka u redu",
+ "ButtonOk": "OK",
+ "ButtonOpenFeed": "Otvori izvor",
+ "ButtonOpenManager": "Otvori Upravitelja",
+ "ButtonPause": "Pauziraj",
+ "ButtonPlay": "Reproduciraj",
+ "ButtonPlaying": "Izvodi se",
+ "ButtonPlaylists": "Popisi za izvođenje",
+ "ButtonPrevious": "Prethodno",
+ "ButtonPreviousChapter": "Prethodno poglavlje",
+ "ButtonProbeAudioFile": "Ispitaj zvučnu datoteku",
+ "ButtonPurgeAllCache": "Isprazni cijelu predmemoriju",
+ "ButtonPurgeItemsCache": "Isprazni predmemoriju stavki",
+ "ButtonQueueAddItem": "Dodaj u red",
+ "ButtonQueueRemoveItem": "Ukloni iz reda",
+ "ButtonQuickEmbedMetadata": "Brzo ugrađivanje meta-podataka",
+ "ButtonQuickMatch": "Brzo prepoznavanje",
+ "ButtonReScan": "Ponovno skeniraj",
"ButtonRead": "Pročitaj",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
+ "ButtonReadLess": "Pročitaj manje",
+ "ButtonReadMore": "Pročitaj više",
+ "ButtonRefresh": "Osvježi",
"ButtonRemove": "Ukloni",
"ButtonRemoveAll": "Ukloni sve",
- "ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
+ "ButtonRemoveAllLibraryItems": "Ukloni sve stavke iz knjižnice",
"ButtonRemoveFromContinueListening": "Ukloni iz Nastavi slušati",
- "ButtonRemoveFromContinueReading": "Remove from Continue Reading",
+ "ButtonRemoveFromContinueReading": "Ukloni iz Nastavi čitati",
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
"ButtonReset": "Poništi",
- "ButtonResetToDefault": "Reset to default",
+ "ButtonResetToDefault": "Vrati na početne postavke",
"ButtonRestore": "Povrati",
"ButtonSave": "Spremi",
"ButtonSaveAndClose": "Spremi i zatvori",
- "ButtonSaveTracklist": "Spremi popis pjesama",
+ "ButtonSaveTracklist": "Spremi popis zvučnih zapisa",
"ButtonScan": "Skeniraj",
- "ButtonScanLibrary": "Scan Library",
+ "ButtonScanLibrary": "Skeniraj knjižnicu",
"ButtonSearch": "Traži",
- "ButtonSelectFolderPath": "Odaberi putanju do folder",
- "ButtonSeries": "Serije",
- "ButtonSetChaptersFromTracks": "Set chapters from tracks",
- "ButtonShare": "Share",
+ "ButtonSelectFolderPath": "Odaberi putanju mape",
+ "ButtonSeries": "Serijali",
+ "ButtonSetChaptersFromTracks": "Postavi poglavlja iz zvučnih zapisa",
+ "ButtonShare": "Podijeli",
"ButtonShiftTimes": "Pomakni vremena",
"ButtonShow": "Prikaži",
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
- "ButtonStartMetadataEmbed": "Pokreni ugradnju metapodataka",
- "ButtonSubmit": "Submit",
+ "ButtonStartMetadataEmbed": "Pokreni ugradnju meta-podataka",
+ "ButtonStats": "Statistika",
+ "ButtonSubmit": "Podnesi",
"ButtonTest": "Test",
- "ButtonUpload": "Upload",
- "ButtonUploadBackup": "Upload backup",
- "ButtonUploadCover": "Upload Cover",
- "ButtonUploadOPMLFile": "Upload OPML Datoteku",
- "ButtonUserDelete": "Delete user {0}",
- "ButtonUserEdit": "Edit user {0}",
+ "ButtonUpload": "Učitaj",
+ "ButtonUploadBackup": "Učitaj sigurnosnu kopiju",
+ "ButtonUploadCover": "Učitaj naslovnicu",
+ "ButtonUploadOPMLFile": "Učitaj OPML datoteku",
+ "ButtonUserDelete": "Izbriši korisnika {0}",
+ "ButtonUserEdit": "Uredi korisnika {0}",
"ButtonViewAll": "Prikaži sve",
"ButtonYes": "Da",
- "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
+ "ErrorUploadFetchMetadataAPI": "Pogreška pri dohvatu meta-podataka",
+ "ErrorUploadFetchMetadataNoResults": "Dohvat meta-podataka nije uspio - pokušajte ispraviti naslov i/ili autora",
+ "ErrorUploadLacksTitle": "Naslov je obavezan",
"HeaderAccount": "Korisnički račun",
+ "HeaderAddCustomMetadataProvider": "Dodaj prilagođenog pružatelja meta-podataka",
"HeaderAdvanced": "Napredno",
- "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
- "HeaderAudioTracks": "audio trake",
- "HeaderAudiobookTools": "Audiobook File Management alati",
- "HeaderAuthentication": "Authentication",
- "HeaderBackups": "Backups",
- "HeaderChangePassword": "Promijeni lozinku",
+ "HeaderAppriseNotificationSettings": "Postavke obavijesti Apprise",
+ "HeaderAudioTracks": "Zvučni zapisi",
+ "HeaderAudiobookTools": "Alati za upravljanje datotekama zvučnih knjiga",
+ "HeaderAuthentication": "Provjera autentičnosti",
+ "HeaderBackups": "Sigurnosne kopije",
+ "HeaderChangePassword": "Promjena zaporke",
"HeaderChapters": "Poglavlja",
- "HeaderChooseAFolder": "Odaberi folder",
- "HeaderCollection": "Kolekcija",
- "HeaderCollectionItems": "Stvari u kolekciji",
- "HeaderCover": "Cover",
- "HeaderCurrentDownloads": "Current Downloads",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
- "HeaderDetails": "Detalji",
- "HeaderDownloadQueue": "Download Queue",
- "HeaderEbookFiles": "fajlovi elektronske knjige",
- "HeaderEmail": "Email",
- "HeaderEmailSettings": "Email Settings",
- "HeaderEpisodes": "Epizode",
- "HeaderEreaderDevices": "Ereader Devices",
- "HeaderEreaderSettings": "podešavanje elektronskog čitača",
+ "HeaderChooseAFolder": "Odaberi mapu",
+ "HeaderCollection": "Zbirka",
+ "HeaderCollectionItems": "Stavke u zbirci",
+ "HeaderCover": "Naslovnica",
+ "HeaderCurrentDownloads": "Preuzimanja u tijeku",
+ "HeaderCustomMessageOnLogin": "Prilagođena poruka prilikom prijave",
+ "HeaderCustomMetadataProviders": "Prilagođeni pružatelji meta-podataka",
+ "HeaderDetails": "Pojedinosti",
+ "HeaderDownloadQueue": "Red preuzimanja",
+ "HeaderEbookFiles": "Datoteke e-knjiga",
+ "HeaderEmail": "E-pošta",
+ "HeaderEmailSettings": "Postavke e-pošte",
+ "HeaderEpisodes": "Nastavci",
+ "HeaderEreaderDevices": "E-čitači",
+ "HeaderEreaderSettings": "Postavke e-čitača",
"HeaderFiles": "Datoteke",
"HeaderFindChapters": "Pronađi poglavlja",
"HeaderIgnoredFiles": "Zanemarene datoteke",
- "HeaderItemFiles": "Item Files",
- "HeaderItemMetadataUtils": "Item Metadata Utils",
- "HeaderLastListeningSession": "Posljednja Listening Session",
- "HeaderLatestEpisodes": "Najnovije epizode",
- "HeaderLibraries": "Biblioteke",
- "HeaderLibraryFiles": "Library Files",
- "HeaderLibraryStats": "Library Stats",
- "HeaderListeningSessions": "Listening Sessions",
- "HeaderListeningStats": "Listening Stats",
- "HeaderLogin": "Prijavljivanje",
- "HeaderLogs": "Logs",
- "HeaderManageGenres": "Manage Genres",
- "HeaderManageTags": "Manage Tags",
- "HeaderMapDetails": "Map details",
- "HeaderMatch": "Match",
- "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
- "HeaderMetadataToEmbed": "Metapodatci za ugradnju",
+ "HeaderItemFiles": "Datoteke stavke",
+ "HeaderItemMetadataUtils": "Alati za meta-podatke",
+ "HeaderLastListeningSession": "Posljednja sesija slušanja",
+ "HeaderLatestEpisodes": "Najnoviji nastavci",
+ "HeaderLibraries": "Knjižnice",
+ "HeaderLibraryFiles": "Datoteke knjižnice",
+ "HeaderLibraryStats": "Statistika knjižnice",
+ "HeaderListeningSessions": "Sesije slušanja",
+ "HeaderListeningStats": "Statistika slušanja",
+ "HeaderLogin": "Prijava",
+ "HeaderLogs": "Zapisnici",
+ "HeaderManageGenres": "Upravljanje žanrovima",
+ "HeaderManageTags": "Upravljanje oznakama",
+ "HeaderMapDetails": "Mapiranje pojedinosti",
+ "HeaderMatch": "Prepoznavanje",
+ "HeaderMetadataOrderOfPrecedence": "Redoslijed prihvaćanja meta-podataka",
+ "HeaderMetadataToEmbed": "Meta-podatci za ugradnju",
"HeaderNewAccount": "Novi korisnički račun",
- "HeaderNewLibrary": "Nova biblioteka",
+ "HeaderNewLibrary": "Nova knjižnica",
+ "HeaderNotificationCreate": "Izradi obavijest",
+ "HeaderNotificationUpdate": "Ažuriraj obavijest",
"HeaderNotifications": "Obavijesti",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
- "HeaderOpenRSSFeed": "Otvori RSS Feed",
+ "HeaderOpenIDConnectAuthentication": "Prijava na OpenID Connect",
+ "HeaderOpenRSSFeed": "Otvori RSS izvor",
"HeaderOtherFiles": "Druge datoteke",
- "HeaderPasswordAuthentication": "Password Authentication",
+ "HeaderPasswordAuthentication": "Provjera autentičnosti zaporkom",
"HeaderPermissions": "Dozvole",
- "HeaderPlayerQueue": "Player Queue",
- "HeaderPlaylist": "Playlist",
- "HeaderPlaylistItems": "Playlist Items",
- "HeaderPodcastsToAdd": "Podcasti za dodati",
- "HeaderPreviewCover": "Pregledaj Cover",
- "HeaderRSSFeedGeneral": "RSS Details",
- "HeaderRSSFeedIsOpen": "RSS Feed je otvoren",
- "HeaderRSSFeeds": "RSS Feeds",
- "HeaderRemoveEpisode": "Ukloni epizodu",
- "HeaderRemoveEpisodes": "Ukloni {0} epizoda/-e",
- "HeaderSavedMediaProgress": "Spremljen Media Progress",
- "HeaderSchedule": "Schedule",
- "HeaderScheduleLibraryScans": "Zakaži automatsko skeniranje biblioteke",
+ "HeaderPlayerQueue": "Redoslijed izvođenja",
+ "HeaderPlayerSettings": "Postavke reproduktora",
+ "HeaderPlaylist": "Popis za izvođenje",
+ "HeaderPlaylistItems": "Stavke popisa za izvođenje",
+ "HeaderPodcastsToAdd": "Podcasti za dodavanje",
+ "HeaderPreviewCover": "Pretpregled naslovnice",
+ "HeaderRSSFeedGeneral": "RSS pojedinosti",
+ "HeaderRSSFeedIsOpen": "RSS izvor je otvoren",
+ "HeaderRSSFeeds": "RSS izvori",
+ "HeaderRemoveEpisode": "Ukloni nastavak",
+ "HeaderRemoveEpisodes": "Ukloni {0} nastavaka",
+ "HeaderSavedMediaProgress": "Spremljen napredak medija",
+ "HeaderSchedule": "Zakazivanje",
+ "HeaderScheduleLibraryScans": "Zakaži automatsko skeniranje knjižnice",
"HeaderSession": "Sesija",
- "HeaderSetBackupSchedule": "Set Backup Schedule",
+ "HeaderSetBackupSchedule": "Zakazivanje sigurnosne pohrane",
"HeaderSettings": "Postavke",
- "HeaderSettingsDisplay": "Zaslon",
- "HeaderSettingsExperimental": "Eksperimentalni Features",
- "HeaderSettingsGeneral": "Opčenito",
- "HeaderSettingsScanner": "Scanner",
- "HeaderSleepTimer": "merač vremena spavanja",
- "HeaderStatsLargestItems": "Largest Items",
+ "HeaderSettingsDisplay": "Prikaz",
+ "HeaderSettingsExperimental": "Eksperimentalne značajke",
+ "HeaderSettingsGeneral": "Općenito",
+ "HeaderSettingsScanner": "Skener",
+ "HeaderSleepTimer": "Timer za spavanje",
+ "HeaderStatsLargestItems": "Najveće stavke",
"HeaderStatsLongestItems": "Najduže stavke (sati)",
- "HeaderStatsMinutesListeningChart": "Minuta odslušanih (posljednjih 7 dana)",
+ "HeaderStatsMinutesListeningChart": "Odslušanih minuta (posljednjih 7 dana)",
"HeaderStatsRecentSessions": "Nedavne sesije",
"HeaderStatsTop10Authors": "Top 10 autora",
"HeaderStatsTop5Genres": "Top 5 žanrova",
- "HeaderTableOfContents": "tabela kontenta",
+ "HeaderTableOfContents": "Sadržaj",
"HeaderTools": "Alati",
- "HeaderUpdateAccount": "Aktualiziraj Korisnički račun",
- "HeaderUpdateAuthor": "Aktualiziraj autora",
- "HeaderUpdateDetails": "Aktualiziraj detalje",
- "HeaderUpdateLibrary": "Aktualiziraj biblioteku",
- "HeaderUsers": "Korinici",
- "HeaderYearReview": "Year {0} in Review",
- "HeaderYourStats": "Tvoja statistika",
- "LabelAbridged": "Abridged",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
+ "HeaderUpdateAccount": "Ažuriraj korisnički račun",
+ "HeaderUpdateAuthor": "Ažuriraj autora",
+ "HeaderUpdateDetails": "Ažuriraj pojedinosti",
+ "HeaderUpdateLibrary": "Ažuriraj knjižnicu",
+ "HeaderUsers": "Korisnici",
+ "HeaderYearReview": "Pregled godine {0}",
+ "HeaderYourStats": "Vaša statistika",
+ "LabelAbridged": "Skraćeno",
+ "LabelAbridgedChecked": "Skraćeno (označeno)",
+ "LabelAbridgedUnchecked": "Neskraćeno (neoznačeno)",
+ "LabelAccessibleBy": "Dostupno",
"LabelAccountType": "Vrsta korisničkog računa",
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gost",
"LabelAccountTypeUser": "Korisnik",
"LabelActivity": "Aktivnost",
- "LabelAddToCollection": "Dodaj u kolekciju",
- "LabelAddToCollectionBatch": "Add {0} Books to Collection",
- "LabelAddToPlaylist": "Add to Playlist",
- "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
- "LabelAdded": "dodato",
- "LabelAddedAt": "dodato u",
- "LabelAdminUsersOnly": "Admin users only",
- "LabelAll": "sve",
+ "LabelAddToCollection": "Dodaj u zbirku",
+ "LabelAddToCollectionBatch": "Dodaj {0} knjiga u zbirku",
+ "LabelAddToPlaylist": "Dodaj na popis za izvođenje",
+ "LabelAddToPlaylistBatch": "Dodaj {0} stavki u popis za izvođenje",
+ "LabelAddedAt": "Dodano",
+ "LabelAddedDate": "Dodano {0}",
+ "LabelAdminUsersOnly": "Samo korisnici administratori",
+ "LabelAll": "Sve",
"LabelAllUsers": "Svi korisnici",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
- "LabelAlreadyInYourLibrary": "Already in your library",
- "LabelAppend": "Append",
+ "LabelAllUsersExcludingGuests": "Svi korisnici osim gostiju",
+ "LabelAllUsersIncludingGuests": "Svi korisnici uključujući i goste",
+ "LabelAlreadyInYourLibrary": "Već u vašoj knjižnici",
+ "LabelAppend": "Pridodaj",
"LabelAuthor": "Autor",
- "LabelAuthorFirstLast": "autor (prvi zadnji)",
- "LabelAuthorLastFirst": "autor (zadnji, prvi)",
+ "LabelAuthorFirstLast": "Autor (Ime Prezime)",
+ "LabelAuthorLastFirst": "Autor (Prezime, Ime)",
"LabelAuthors": "Autori",
- "LabelAutoDownloadEpisodes": "Automatski preuzmi epizode",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
- "LabelBackToUser": "Nazad k korisniku",
- "LabelBackupLocation": "Backup Location",
- "LabelBackupsEnableAutomaticBackups": "Uključi automatski backup",
- "LabelBackupsEnableAutomaticBackupsHelp": "Backups spremljeni u /metadata/backups",
- "LabelBackupsMaxBackupSize": "Maksimalna količina backupa (u GB)",
- "LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
- "LabelBackupsNumberToKeep": "Broj backupa zadržati",
- "LabelBackupsNumberToKeepHelp": "Samo 1 backup će biti odjednom obrisan. Ako koristite više njih, morati ćete ih ručno ukloniti.",
- "LabelBitrate": "Bitrate",
- "LabelBooks": "Knjige",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
+ "LabelAutoDownloadEpisodes": "Automatski preuzmi nastavke",
+ "LabelAutoFetchMetadata": "Automatski dohvati meta-podatke",
+ "LabelAutoFetchMetadataHelp": "Dohvaća meta-podatke o naslovu, autoru i serijalu kako bi pojednostavnio učitavanje. Dodatni meta-podatci će se možda morati dohvatiti nakon učitavanja.",
+ "LabelAutoLaunch": "Automatsko pokretanje",
+ "LabelAutoLaunchDescription": "Automatski preusmjeri na pružatelja autentifikacijskih usluga prilikom otvaranja stranice za prijavu (putanja za ručno zaobilaženje opcije
/login?autoLaunch=0
)",
+ "LabelAutoRegister": "Automatska registracija",
+ "LabelAutoRegisterDescription": "Automatski izradi nove korisnike nakon prijave",
+ "LabelBackToUser": "Povratak na korisnika",
+ "LabelBackupLocation": "Lokacija sigurnosnih kopija",
+ "LabelBackupsEnableAutomaticBackups": "Uključ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",
+ "LabelButtonText": "Tekst gumba",
+ "LabelByAuthor": "po {0}",
"LabelChangePassword": "Promijeni lozinku",
- "LabelChannels": "Channels",
- "LabelChapterTitle": "Ime poglavlja",
- "LabelChapters": "poglavlja",
- "LabelChaptersFound": "poglavlja pronađena",
- "LabelClickForMoreInfo": "Click for more info",
- "LabelClosePlayer": "izaberi igrača",
- "LabelCodec": "Codec",
- "LabelCollapseSeries": "Collapse Series",
- "LabelCollection": "Collection",
- "LabelCollections": "Kolekcije",
- "LabelComplete": "završeno",
+ "LabelChannels": "Kanali",
+ "LabelChapterTitle": "Naslov poglavlja",
+ "LabelChapters": "Poglavlja",
+ "LabelChaptersFound": "poglavlja pronađeno",
+ "LabelClickForMoreInfo": "Kliknite za više informacija",
+ "LabelClosePlayer": "Zatvori reproduktor",
+ "LabelCodec": "Kodek",
+ "LabelCollapseSeries": "Serijale prikaži sažeto",
+ "LabelCollapseSubSeries": "Podserijale prikaži sažeto",
+ "LabelCollection": "Zbirka",
+ "LabelCollections": "Zbirka/i",
+ "LabelComplete": "Dovršeno",
"LabelConfirmPassword": "Potvrdi lozinku",
- "LabelContinueListening": "nastavi slušati",
- "LabelContinueReading": "nastavi čitati",
- "LabelContinueSeries": "nastavi serije",
- "LabelCover": "Cover",
- "LabelCoverImageURL": "URL od covera",
- "LabelCreatedAt": "Napravljeno",
- "LabelCronExpression": "Cron Expression",
+ "LabelContinueListening": "Nastavi slušati",
+ "LabelContinueReading": "Nastavi čitati",
+ "LabelContinueSeries": "Nastavi serijal",
+ "LabelCover": "Naslovnica",
+ "LabelCoverImageURL": "URL naslovnice",
+ "LabelCreatedAt": "Stvoreno",
+ "LabelCronExpression": "Cron izraz",
"LabelCurrent": "Trenutan",
"LabelCurrently": "Trenutno:",
- "LabelCustomCronExpression": "Custom Cron Expression:",
- "LabelDatetime": "Datetime",
- "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
+ "LabelCustomCronExpression": "Prilagođeni CRON izraz:",
+ "LabelDatetime": "Datum i vrijeme",
+ "LabelDays": "Dani",
+ "LabelDeleteFromFileSystemCheckbox": "Izbriši datoteke (uklonite oznaku ako stavku želite izbrisati samo iz baze podataka)",
"LabelDescription": "Opis",
"LabelDeselectAll": "Odznači sve",
"LabelDevice": "Uređaj",
"LabelDeviceInfo": "O uređaju",
- "LabelDeviceIsAvailableTo": "Device is available to...",
+ "LabelDeviceIsAvailableTo": "Uređaj je dostupan...",
"LabelDirectory": "Direktorij",
- "LabelDiscFromFilename": "CD iz imena datoteke",
- "LabelDiscFromMetadata": "CD iz metapodataka",
- "LabelDiscover": "otkriti",
+ "LabelDiscFromFilename": "Disk iz imena datoteke",
+ "LabelDiscFromMetadata": "Disk iz metapodataka",
+ "LabelDiscover": "Otkrij",
"LabelDownload": "Preuzmi",
- "LabelDownloadNEpisodes": "Download {0} episodes",
+ "LabelDownloadNEpisodes": "Preuzmi {0} nastavak/a",
"LabelDuration": "Trajanje",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
+ "LabelDurationComparisonExactMatch": "(točno podudaranje)",
+ "LabelDurationComparisonLonger": "({0} duže)",
+ "LabelDurationComparisonShorter": "({0} kraće)",
"LabelDurationFound": "Pronađeno trajanje:",
- "LabelEbook": "elektronska knjiga",
- "LabelEbooks": "elektronske knjige",
+ "LabelEbook": "E-knjiga",
+ "LabelEbooks": "E-knjige",
"LabelEdit": "Uredi",
- "LabelEmail": "Email",
- "LabelEmailSettingsFromAddress": "From Address",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
- "LabelEmailSettingsSecure": "Secure",
- "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
- "LabelEmailSettingsTestAddress": "Test Address",
- "LabelEmbeddedCover": "Embedded Cover",
- "LabelEnable": "Uključi",
+ "LabelEmail": "E-pošta",
+ "LabelEmailSettingsFromAddress": "Adresa pošiljatelja",
+ "LabelEmailSettingsRejectUnauthorized": "Odbij neovjerene certifikate",
+ "LabelEmailSettingsRejectUnauthorizedHelp": "Onemogućavanjem ovjere SSL certifikata izlažete vezu sigurnosnim rizicima, poput MITM napada. Ovu opciju isključite samo ukoliko razumijete što ona znači i vjerujete poslužitelju e-pošte s kojim se povezujete.",
+ "LabelEmailSettingsSecure": "Sigurno",
+ "LabelEmailSettingsSecureHelp": "Ako je uključeno, prilikom spajanja na poslužitelj upotrebljavat će se TLS. Ako je isključeno, TLS se upotrebljava samo ako poslužitelj podržava STARTTLS proširenje. U većini slučajeva, ovu ćete vrijednost uključiti ako se spajate na priključak 465. Za priključke 587 ili 25 ostavite je isključenom. (Izvor: nodemailer.com/smtp/#authentication)",
+ "LabelEmailSettingsTestAddress": "Probna adresa",
+ "LabelEmbeddedCover": "Ugrađena naslovnica",
+ "LabelEnable": "Omogući",
"LabelEnd": "Kraj",
- "LabelEpisode": "Epizoda",
- "LabelEpisodeTitle": "Naslov epizode",
- "LabelEpisodeType": "Vrsta epizode",
- "LabelExample": "Example",
- "LabelExplicit": "Explicit",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
- "LabelFeedURL": "Feed URL",
- "LabelFetchingMetadata": "Fetching Metadata",
+ "LabelEndOfChapter": "Kraj poglavlja",
+ "LabelEpisode": "Nastavak",
+ "LabelEpisodeTitle": "Naslov nastavka",
+ "LabelEpisodeType": "Vrsta nastavka",
+ "LabelEpisodes": "Nastavci",
+ "LabelExample": "Primjer",
+ "LabelExpandSeries": "Serijal prikaži prošireno",
+ "LabelExpandSubSeries": "Podserijal prikaži prošireno",
+ "LabelExplicit": "Eksplicitni sadržaj",
+ "LabelExplicitChecked": "Eksplicitni sadržaj (označeno)",
+ "LabelExplicitUnchecked": "Nije eksplicitni sadržaj (odznačeno)",
+ "LabelExportOPML": "Izvoz OPML-a",
+ "LabelFeedURL": "URL izvora",
+ "LabelFetchingMetadata": "Dohvaćanje meta-podataka",
"LabelFile": "Datoteka",
- "LabelFileBirthtime": "File Birthtime",
- "LabelFileModified": "fajl izmenjen",
- "LabelFilename": "Ime datoteke",
+ "LabelFileBirthtime": "Datoteka stvorena",
+ "LabelFileBornDate": "Stvoreno {0}",
+ "LabelFileModified": "Datoteka izmijenjena",
+ "LabelFileModifiedDate": "Izmijenjeno {0}",
+ "LabelFilename": "Naziv datoteke",
"LabelFilterByUser": "Filtriraj po korisniku",
"LabelFindEpisodes": "Pronađi epizode",
- "LabelFinished": "završen",
- "LabelFolder": "folder",
- "LabelFolders": "Folderi",
- "LabelFontBold": "Bold",
- "LabelFontBoldness": "Font Boldness",
- "LabelFontFamily": "Font family",
- "LabelFontItalic": "Italic",
- "LabelFontScale": "Font scale",
- "LabelFontStrikethrough": "Strikethrough",
+ "LabelFinished": "Dovršeno",
+ "LabelFolder": "Mapa",
+ "LabelFolders": "Mape",
+ "LabelFontBold": "Podebljano",
+ "LabelFontBoldness": "Debljina slova",
+ "LabelFontFamily": "Skupina fontova",
+ "LabelFontItalic": "Kurziv",
+ "LabelFontScale": "Veličina slova",
+ "LabelFontStrikethrough": "Precrtano",
"LabelFormat": "Format",
- "LabelGenre": "Genre",
+ "LabelGenre": "Žanr",
"LabelGenres": "Žanrovi",
"LabelHardDeleteFile": "Obriši datoteku zauvijek",
- "LabelHasEbook": "Has ebook",
- "LabelHasSupplementaryEbook": "Has supplementary ebook",
- "LabelHighestPriority": "Highest priority",
- "LabelHost": "Host",
+ "LabelHasEbook": "Ima e-knjigu",
+ "LabelHasSupplementaryEbook": "Ima dopunsku e-knjigu",
+ "LabelHideSubtitles": "Skrij podnaslove",
+ "LabelHighestPriority": "Najviši prioritet",
+ "LabelHost": "Poslužitelj",
"LabelHour": "Sat",
+ "LabelHours": "Sati",
"LabelIcon": "Ikona",
- "LabelImageURLFromTheWeb": "Image URL from the web",
+ "LabelImageURLFromTheWeb": "URL slike s weba",
"LabelInProgress": "U tijeku",
- "LabelIncludeInTracklist": "Dodaj u Tracklist",
+ "LabelIncludeInTracklist": "Uključi u popisu zvučnih zapisa",
"LabelIncomplete": "Nepotpuno",
"LabelInterval": "Interval",
- "LabelIntervalCustomDailyWeekly": "Custom daily/weekly",
- "LabelIntervalEvery12Hours": "Every 12 hours",
- "LabelIntervalEvery15Minutes": "Every 15 minutes",
- "LabelIntervalEvery2Hours": "Every 2 hours",
- "LabelIntervalEvery30Minutes": "Every 30 minutes",
- "LabelIntervalEvery6Hours": "Every 6 hours",
- "LabelIntervalEveryDay": "Every day",
- "LabelIntervalEveryHour": "Every hour",
- "LabelInvert": "Invert",
+ "LabelIntervalCustomDailyWeekly": "Prilagođeno dnevno/tjedno",
+ "LabelIntervalEvery12Hours": "Svakih 12 sati",
+ "LabelIntervalEvery15Minutes": "Svakih 15 minuta",
+ "LabelIntervalEvery2Hours": "Svaka 2 sata",
+ "LabelIntervalEvery30Minutes": "Svakih 30 minuta",
+ "LabelIntervalEvery6Hours": "Svakih 6 sati",
+ "LabelIntervalEveryDay": "Svaki dan",
+ "LabelIntervalEveryHour": "Svaki sat",
+ "LabelInvert": "Obrni",
"LabelItem": "Stavka",
+ "LabelJumpBackwardAmount": "Dužina skoka unatrag",
+ "LabelJumpForwardAmount": "Dužina skoka unaprijed",
"LabelLanguage": "Jezik",
- "LabelLanguageDefaultServer": "Default jezik servera",
- "LabelLanguages": "Languages",
- "LabelLastBookAdded": "Last Book Added",
- "LabelLastBookUpdated": "Last Book Updated",
+ "LabelLanguageDefaultServer": "Zadani jezik poslužitelja",
+ "LabelLanguages": "Jezici",
+ "LabelLastBookAdded": "Zadnja dodana knjiga",
+ "LabelLastBookUpdated": "Zadnja ažurirana knjiga",
"LabelLastSeen": "Zadnje pogledano",
- "LabelLastTime": "Prošli put",
- "LabelLastUpdate": "Zadnja aktualizacija",
- "LabelLayout": "Layout",
- "LabelLayoutSinglePage": "Single page",
- "LabelLayoutSplitPage": "Split page",
+ "LabelLastTime": "Zadnji puta",
+ "LabelLastUpdate": "Zadnje ažuriranje",
+ "LabelLayout": "Prikaz",
+ "LabelLayoutSinglePage": "Jedna stranica",
+ "LabelLayoutSplitPage": "Podijeli stranicu",
"LabelLess": "Manje",
- "LabelLibrariesAccessibleToUser": "Biblioteke pristupačne korisniku",
- "LabelLibrary": "Biblioteka",
- "LabelLibraryFilterSublistEmpty": "No {0}",
- "LabelLibraryItem": "Stavka biblioteke",
- "LabelLibraryName": "Ime biblioteke",
- "LabelLimit": "Limit",
- "LabelLineSpacing": "Line spacing",
- "LabelListenAgain": "Slušaj ponovno",
+ "LabelLibrariesAccessibleToUser": "Knjižnice dostupne korisniku",
+ "LabelLibrary": "Knjižnica",
+ "LabelLibraryFilterSublistEmpty": "Br {0}",
+ "LabelLibraryItem": "Stavka knjižnice",
+ "LabelLibraryName": "Ime knjižnice",
+ "LabelLimit": "Ograničenje",
+ "LabelLineSpacing": "Razmak između redaka",
+ "LabelListenAgain": "Ponovno poslušaj",
"LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
- "LabelMediaPlayer": "Media Player",
- "LabelMediaType": "Media Type",
- "LabelMetaTag": "Meta Tag",
- "LabelMetaTags": "Meta Tags",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
- "LabelMetadataProvider": "Poslužitelj metapodataka ",
+ "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",
+ "LabelMediaPlayer": "Reproduktor medijskih sadržaja",
+ "LabelMediaType": "Vrsta medija",
+ "LabelMetaTag": "Meta oznaka",
+ "LabelMetaTags": "Meta oznake",
+ "LabelMetadataOrderOfPrecedenceDescription": "Izvori meta-podataka višeg prioriteta nadjačat će izvore nižeg prioriteta",
+ "LabelMetadataProvider": "Pružatelj meta-podataka",
"LabelMinute": "Minuta",
+ "LabelMinutes": "Minute",
"LabelMissing": "Nedostaje",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
+ "LabelMissingEbook": "Nema e-knjigu",
+ "LabelMissingSupplementaryEbook": "Nema dopunsku e-knjigu",
+ "LabelMobileRedirectURIs": "Dopušteni URI-ji za preusmjeravanje mobilne aplikacije",
+ "LabelMobileRedirectURIsDescription": "Ovo je popis dopuštenih važećih URI-ja za preusmjeravanje mobilne aplikacije. Zadana vrijednost je
audiobookshelf://oauth
, nju možete ukloniti ili dopuniti dodatnim URI-jima za integraciju aplikacija trećih strana. Upisom zvjezdice (
*
) kao jedinim unosom možete dozvoliti bilo koji URI.",
"LabelMore": "Više",
- "LabelMoreInfo": "More Info",
+ "LabelMoreInfo": "Više informacija",
"LabelName": "Ime",
- "LabelNarrator": "Narrator",
- "LabelNarrators": "Naratori",
+ "LabelNarrator": "Pripovjedač",
+ "LabelNarrators": "Pripovjedači",
"LabelNew": "Novo",
"LabelNewPassword": "Nova lozinka",
"LabelNewestAuthors": "Najnoviji autori",
- "LabelNewestEpisodes": "Najnovije epizode",
- "LabelNextBackupDate": "Next backup date",
- "LabelNextScheduledRun": "Next scheduled run",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
- "LabelNoEpisodesSelected": "No episodes selected",
- "LabelNotFinished": "Nedovršeno",
- "LabelNotStarted": "Not Started",
+ "LabelNewestEpisodes": "Najnoviji nastavci",
+ "LabelNextBackupDate": "Sljedeće izrada sigurnosne kopije",
+ "LabelNextScheduledRun": "Sljedeće zakazano izvođenje",
+ "LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka",
+ "LabelNoEpisodesSelected": "Nema odabranih nastavaka",
+ "LabelNotFinished": "Nije dovršeno",
+ "LabelNotStarted": "Nije započeto",
"LabelNotes": "Bilješke",
- "LabelNotificationAppriseURL": "Apprise URL(s)",
+ "LabelNotificationAppriseURL": "Apprise URL(ovi)",
"LabelNotificationAvailableVariables": "Dostupne varijable",
- "LabelNotificationBodyTemplate": "Body Template",
- "LabelNotificationEvent": "Notification Event",
- "LabelNotificationTitleTemplate": "Title Template",
+ "LabelNotificationBodyTemplate": "Predložak sadržaja",
+ "LabelNotificationEvent": "Događaj za obavijest",
+ "LabelNotificationTitleTemplate": "Predložak naslova",
"LabelNotificationsMaxFailedAttempts": "Maksimalan broj neuspjelih pokušaja",
- "LabelNotificationsMaxFailedAttemptsHelp": "Obavijesti će biti isključene ako par puta budu neuspješno poslane.",
- "LabelNotificationsMaxQueueSize": "Maksimalna veličina queuea za notification events",
- "LabelNotificationsMaxQueueSizeHelp": "Samo 1 event po sekundi može biti pokrenut. Eventi će biti ignorirani ako je queue na maksimalnoj veličini. To spriječava spammanje s obavijestima.",
- "LabelNumberOfBooks": "Number of Books",
- "LabelNumberOfEpisodes": "# of Episodes",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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.",
+ "LabelNotificationsMaxFailedAttemptsHelp": "Obavijesti će biti isključene ako slanje ne uspije nakon ovoliko pokušaja",
+ "LabelNotificationsMaxQueueSize": "Najveći broj događaja za obavijest u redu čekanja",
+ "LabelNotificationsMaxQueueSizeHelp": "Događaji se mogu okinuti samo jednom u sekundi. Događaji će se zanemariti ako je red čekanja pun. Ovo sprečava prekomjerno slanje obavijesti.",
+ "LabelNumberOfBooks": "Broj knjiga",
+ "LabelNumberOfEpisodes": "broj nastavaka",
+ "LabelOpenIDAdvancedPermsClaimDescription": "Naziv OpenID zahtjeva koji sadrži napredna dopuštenja za korisničke radnje u aplikaciji koje će se primijeniti na ne-administratorske uloge (
ako su konfigurirane ). Ako zahtjev nedostaje u odgovoru, pristup ABS-u neće se odobriti. Ako i jedna opcija nedostaje, smatrat će se da je
false
. Pripazite da zahtjev pružatelja identiteta uvijek odgovara očekivanoj strukturi:",
+ "LabelOpenIDClaims": "Sljedeće opcije ostavite praznima ako želite onemogućiti napredno dodjeljivanje grupa i dozvola, odnosno ako želite automatski dodijeliti grupu 'korisnik'.",
+ "LabelOpenIDGroupClaimDescription": "Naziv OpenID zahtjeva koji sadrži popis korisnikovih grupa. Često se naziva
groups
.
Ako se konfigurira , aplikacija će automatski dodijeliti uloge temeljem korisnikovih članstava u grupama, pod uvjetom da se iste zovu 'admin', 'user' ili 'guest' u zahtjevu (ne razlikuju se velika i mala slova). Zahtjev treba sadržavati popis i ako je korisnik član više grupa, aplikacija će dodijeliti ulogu koja odgovara najvišoj razini pristupa. Ukoliko se niti jedna grupa ne podudara, pristup će biti onemogućen.",
"LabelOpenRSSFeed": "Otvori RSS Feed",
- "LabelOverwrite": "Overwrite",
- "LabelPassword": "Lozinka",
+ "LabelOverwrite": "Prepiši",
+ "LabelPassword": "Zaporka",
"LabelPath": "Putanja",
- "LabelPermissionsAccessAllLibraries": "Ima pristup svim bibliotekama",
- "LabelPermissionsAccessAllTags": "Ima pristup svim tagovima",
+ "LabelPermanent": "Trajno",
+ "LabelPermissionsAccessAllLibraries": "Ima pristup svim knjižnicama",
+ "LabelPermissionsAccessAllTags": "Ima pristup svim oznakama",
"LabelPermissionsAccessExplicitContent": "Ima pristup eksplicitnom sadržzaju",
"LabelPermissionsDelete": "Smije brisati",
"LabelPermissionsDownload": "Smije preuzimati",
- "LabelPermissionsUpdate": "Smije aktualizirati",
- "LabelPermissionsUpload": "Smije uploadati",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
- "LabelPhotoPathURL": "Slika putanja/URL",
- "LabelPlayMethod": "Vrsta reprodukcije",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
- "LabelPlaylists": "Playlists",
+ "LabelPermissionsUpdate": "Smije ažurirati",
+ "LabelPermissionsUpload": "Smije učitavati",
+ "LabelPersonalYearReview": "Vaš godišnji pregled ({0})",
+ "LabelPhotoPathURL": "Putanja ili URL fotografije",
+ "LabelPlayMethod": "Način reprodukcije",
+ "LabelPlayerChapterNumberMarker": "{0} od {1}",
+ "LabelPlaylists": "Popisi za izvođenje",
"LabelPodcast": "Podcast",
- "LabelPodcastSearchRegion": "Područje pretrage podcasta",
- "LabelPodcastType": "Podcast Type",
- "LabelPodcasts": "Podcasts",
- "LabelPort": "Port",
- "LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
- "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
- "LabelPrimaryEbook": "Primary ebook",
+ "LabelPodcastSearchRegion": "Zemljopisno područje kod pretraživanja podcasta",
+ "LabelPodcastType": "Vrsta podcasta",
+ "LabelPodcasts": "Podcasti",
+ "LabelPort": "Priključak",
+ "LabelPrefixesToIgnore": "Prefiksi koji se zanemaruju (mala i velika slova nisu bitna)",
+ "LabelPreventIndexing": "Spriječite da iTunes i Google indeksiraju vaš feed za svoje popise podcasta",
+ "LabelPrimaryEbook": "Primarna e-knjiga",
"LabelProgress": "Napredak",
"LabelProvider": "Dobavljač",
- "LabelPubDate": "Datam izdavanja",
+ "LabelProviderAuthorizationValue": "Vrijednost autorizacijskog zaglavlja",
+ "LabelPubDate": "Datum izdavanja",
"LabelPublishYear": "Godina izdavanja",
+ "LabelPublishedDate": "Objavljeno {0}",
"LabelPublisher": "Izdavač",
- "LabelPublishers": "Publishers",
- "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
- "LabelRSSFeedCustomOwnerName": "Custom owner Name",
- "LabelRSSFeedOpen": "RSS Feed Open",
- "LabelRSSFeedPreventIndexing": "Prevent Indexing",
- "LabelRSSFeedSlug": "RSS Feed Slug",
- "LabelRSSFeedURL": "RSS Feed URL",
- "LabelRead": "Read",
- "LabelReadAgain": "Read Again",
- "LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
- "LabelRecentSeries": "Nedavne serije",
+ "LabelPublishers": "Izdavači",
+ "LabelRSSFeedCustomOwnerEmail": "Prilagođena adresa e-pošte vlasnika",
+ "LabelRSSFeedCustomOwnerName": "Prilagođeno ime vlasnika",
+ "LabelRSSFeedOpen": "RSS izvor otvoren",
+ "LabelRSSFeedPreventIndexing": "Onemogući indeksiranje",
+ "LabelRSSFeedSlug": "Slug RSS izvora",
+ "LabelRSSFeedURL": "URL RSS izvora",
+ "LabelRandomly": "Nasumično",
+ "LabelReAddSeriesToContinueListening": "Ponovno dodaj serijal u Nastavi slušati",
+ "LabelRead": "Čitaj",
+ "LabelReadAgain": "Ponovno čitaj",
+ "LabelReadEbookWithoutProgress": "Čitaj e-knjige bez praćenja napretka",
+ "LabelRecentSeries": "Najnoviji serijali",
"LabelRecentlyAdded": "Nedavno dodano",
- "LabelRecommended": "Recommended",
- "LabelRedo": "Redo",
+ "LabelRecommended": "Preporučeno",
+ "LabelRedo": "Ponovi",
"LabelRegion": "Regija",
"LabelReleaseDate": "Datum izlaska",
- "LabelRemoveCover": "Remove cover",
- "LabelRowsPerPage": "Rows per page",
+ "LabelRemoveCover": "Ukloni naslovnicu",
+ "LabelRowsPerPage": "Redaka po stranici",
"LabelSearchTerm": "Traži pojam",
"LabelSearchTitle": "Traži naslov",
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
"LabelSeason": "Sezona",
- "LabelSelectAll": "Select all",
- "LabelSelectAllEpisodes": "Select all episodes",
- "LabelSelectEpisodesShowing": "Select {0} episodes showing",
- "LabelSelectUsers": "Select users",
- "LabelSendEbookToDevice": "Send Ebook to...",
- "LabelSequence": "Sekvenca",
- "LabelSeries": "Serije",
- "LabelSeriesName": "Ime serije",
- "LabelSeriesProgress": "Series Progress",
- "LabelServerYearReview": "Server Year in Review ({0})",
- "LabelSetEbookAsPrimary": "Set as primary",
- "LabelSetEbookAsSupplementary": "Set as supplementary",
- "LabelSettingsAudiobooksOnly": "Audiobooks only",
- "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
- "LabelSettingsBookshelfViewHelp": "Skeumorfski (što god to bilo) dizajn sa drvenim policama",
- "LabelSettingsChromecastSupport": "Chromecast podrška",
+ "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",
+ "LabelSeriesName": "Ime serijala",
+ "LabelSeriesProgress": "Napredak u serijalu",
+ "LabelServerYearReview": "Godišnji pregled poslužitelja ({0})",
+ "LabelSetEbookAsPrimary": "Postavi kao primarno",
+ "LabelSetEbookAsSupplementary": "Postavi kao dopunsko",
+ "LabelSettingsAudiobooksOnly": "Samo zvučne knjige",
+ "LabelSettingsAudiobooksOnlyHelp": "Ako uključite ovu mogućnost, sustav će zanemariti datoteke e-knjiga ukoliko se ne nalaze u mapi zvučne knjige, gdje će se smatrati dopunskim e-knjigama",
+ "LabelSettingsBookshelfViewHelp": "Skeumorfni dizajn sa drvenim policama",
+ "LabelSettingsChromecastSupport": "Podrška za Chromecast",
"LabelSettingsDateFormat": "Format datuma",
- "LabelSettingsDisableWatcher": "Isključi Watchera",
- "LabelSettingsDisableWatcherForLibrary": "Isključi folder watchera za biblioteku",
- "LabelSettingsDisableWatcherHelp": "Isključi automatsko dodavanje/aktualiziranje stavci ako su promjene prepoznate. *Potreban restart servera",
- "LabelSettingsEnableWatcher": "Enable Watcher",
- "LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
- "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
- "LabelSettingsExperimentalFeatures": "Eksperimentalni features",
- "LabelSettingsExperimentalFeaturesHelp": "Features u razvoju trebaju vaš feedback i pomoć pri testiranju. Klikni da odeš to Github discussionsa.",
- "LabelSettingsFindCovers": "Pronađi covers",
- "LabelSettingsFindCoversHelp": "Ako audiobook nema embedani cover or a cover sliku unutar foldera, skener će probati pronaći cover.
Bilješka: Ovo će produžiti trjanje skeniranja",
- "LabelSettingsHideSingleBookSeries": "Hide single book series",
- "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
- "LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu",
- "LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku",
- "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": "Parsaj podnapise",
- "LabelSettingsParseSubtitlesHelp": "Izvadi podnapise iz imena od audiobook foldera.
Podnapis mora biti odvojen sa \" - \"
npr. \"Ime knjige - Podnapis ovdje\" ima podnapis \"Podnapis ovdje\"",
- "LabelSettingsPreferMatchedMetadata": "Preferiraj matchane metapodatke",
- "LabelSettingsPreferMatchedMetadataHelp": "Matchani podatci će biti korišteni kada se koristi Quick Match. Po defaultu Quick Match će ispuniti samo prazne detalje.",
- "LabelSettingsSkipMatchingBooksWithASIN": "Preskoči matchanje knjiga koje već imaju ASIN",
- "LabelSettingsSkipMatchingBooksWithISBN": "SPreskoči matchanje knjiga koje već imaju ISBN",
- "LabelSettingsSortingIgnorePrefixes": "Zanemari prefikse tokom sortiranja",
- "LabelSettingsSortingIgnorePrefixesHelp": "npr. za prefiks \"the\" book title \"The Ime Knjige\" će sortirati kao \"Ime Knjige, The\"",
- "LabelSettingsSquareBookCovers": "Kockasti cover knjige",
- "LabelSettingsSquareBookCoversHelp": "Koristi kockasti cover knjige umjesto klasičnog 1.6:1.",
- "LabelSettingsStoreCoversWithItem": "Spremi cover uz stakvu",
- "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
+ "LabelSettingsDisableWatcher": "Isključi praćenje datotečnog sustava",
+ "LabelSettingsDisableWatcherForLibrary": "Onemogući praćenje datotečnog sustava za ovu knjižnicu",
+ "LabelSettingsDisableWatcherHelp": "Onemogućuje automatsko dodavanje ili ažuriranje stavki kod uočenih promjena datoteka. *Potrebno je ponovno pokrenuti poslužitelj",
+ "LabelSettingsEnableWatcher": "Omogući praćenje promjena",
+ "LabelSettingsEnableWatcherForLibrary": "Omogući praćenje promjena u mapi knjižnice",
+ "LabelSettingsEnableWatcherHelp": "Omogućuje automatsko dodavanje/ažuriranje stavki kada se uoče izmjene datoteka. *Potrebno je ponovno pokretanje poslužitelja",
+ "LabelSettingsEpubsAllowScriptedContent": "Omogući skripte u epub datotekama",
+ "LabelSettingsEpubsAllowScriptedContentHelp": "Omogućuje epub datotekama izvođenje skripti. Preporučamo isključiti ovu mogućnost ukoliko nemate povjerenja u izvore epub datoteka.",
+ "LabelSettingsExperimentalFeatures": "Eksperimentalne značajke",
+ "LabelSettingsExperimentalFeaturesHelp": "Značajke u razvoju za koje trebamo vaše povratne informacije i pomoć u testiranju. Kliknite za otvaranje rasprave na githubu.",
+ "LabelSettingsFindCovers": "Pronađi naslovnice",
+ "LabelSettingsFindCoversHelp": "Ako vaša zvučna knjiga nema ugrađenu naslovnicu ili sliku naslovnice u mapi, skener će pokušati pronaći naslovnicu.
Napomena: ovo će produžiti trajanje skeniranja",
+ "LabelSettingsHideSingleBookSeries": "Skrij serijale sa samo jednom knjigom",
+ "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",
+ "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",
+ "LabelSettingsParseSubtitlesHelp": "Iz naziva mape zvučne knjige raščlanjuje podnaslov.
Podnaslov mora biti odvojen s \" - \"
npr. \"Naslov knjige - Ovo je podnaslov\" imat će podnaslov \"Ovo je podnaslov\"",
+ "LabelSettingsPreferMatchedMetadata": "Daj prednost meta-podatcima prepoznatih stavki",
+ "LabelSettingsPreferMatchedMetadataHelp": "Podatci prepoznatog naslova nadjačat će postojeće informacije kod korištenja funkcije Brzog prepoznavanja. Zadana funkcionalnost je da Brzo prepoznavanje samo dopuni podatke koji nedostaju.",
+ "LabelSettingsSkipMatchingBooksWithASIN": "Preskoči prepoznavanje knjiga koje već imaju ASIN",
+ "LabelSettingsSkipMatchingBooksWithISBN": "Preskoči prepoznavanje knjiga koje već imaju ISBN",
+ "LabelSettingsSortingIgnorePrefixes": "Zanemari prefikse kod sortiranja",
+ "LabelSettingsSortingIgnorePrefixesHelp": "npr. za prefiks \"the\" naslov knjige \"The Book Title\" sortirat će se \"Book Title, The\"",
+ "LabelSettingsSquareBookCovers": "Koristi pravokutne naslovnice knjiga",
+ "LabelSettingsSquareBookCoversHelp": "Koristi pravokutne naslovnice umjesto uobičajenih naslovnica omjera 1,6:1",
+ "LabelSettingsStoreCoversWithItem": "Spremi naslovnice uz stavke",
+ "LabelSettingsStoreCoversWithItemHelp": "Naslovnice se obično spremaju u /metadata/items, ako uključite ovu opciju naslovnice će se spremati u mapu knjižničke stavke. Čuva se samo jedna datoteka naziva \"cover\"",
"LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku",
- "LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke",
- "LabelSettingsTimeFormat": "Time Format",
+ "LabelSettingsStoreMetadataWithItemHelp": "Meta-podatci se obično spremaju u /metadata/items; ako uključite ovu postavku meta-podatci će se čuvati u mapama knjižničkih stavki",
+ "LabelSettingsTimeFormat": "Format vremena",
+ "LabelShare": "Podijeli",
+ "LabelShareOpen": "Dijeljenje otvoreno",
+ "LabelShareURL": "URL za dijeljenje",
"LabelShowAll": "Prikaži sve",
- "LabelShowSeconds": "Show seconds",
+ "LabelShowSeconds": "Prikaži sekunde",
+ "LabelShowSubtitles": "Prikaži podnaslove",
"LabelSize": "Veličina",
- "LabelSleepTimer": "Sleep timer",
+ "LabelSleepTimer": "Timer za spavanje",
"LabelSlug": "Slug",
- "LabelStart": "Pokreni",
- "LabelStartTime": "Vrijeme pokretanja",
- "LabelStarted": "Pokrenuto",
- "LabelStartedAt": "Pokrenuto",
- "LabelStatsAudioTracks": "Audio Tracks",
+ "LabelStart": "Početak",
+ "LabelStartTime": "Vrijeme početka",
+ "LabelStarted": "Započeto",
+ "LabelStartedAt": "Započeto",
+ "LabelStatsAudioTracks": "Zvučni zapisi",
"LabelStatsAuthors": "Autori",
"LabelStatsBestDay": "Najbolji dan",
"LabelStatsDailyAverage": "Dnevni prosjek",
"LabelStatsDays": "Dani",
- "LabelStatsDaysListened": "Dana slušao",
+ "LabelStatsDaysListened": "Dana slušano",
"LabelStatsHours": "Sati",
- "LabelStatsInARow": "u redu",
- "LabelStatsItemsFinished": "Završenih stavki",
- "LabelStatsItemsInLibrary": "Stavke u biblioteki",
+ "LabelStatsInARow": "uzastopno",
+ "LabelStatsItemsFinished": "Dovršenih stavki",
+ "LabelStatsItemsInLibrary": "Stavke u knjižnici",
"LabelStatsMinutes": "minute",
"LabelStatsMinutesListening": "Minuta odslušano",
- "LabelStatsOverallDays": "Overall Days",
- "LabelStatsOverallHours": "Overall Hours",
+ "LabelStatsOverallDays": "Ukupno dana",
+ "LabelStatsOverallHours": "Ukupno sati",
"LabelStatsWeekListening": "Tjedno slušanje",
- "LabelSubtitle": "Podnapis",
- "LabelSupportedFileTypes": "Podržtani tip datoteke",
- "LabelTag": "Tag",
- "LabelTags": "Tags",
- "LabelTagsAccessibleToUser": "Tags dostupni korisniku",
- "LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
- "LabelTasks": "Tasks Running",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
- "LabelTheme": "Theme",
- "LabelThemeDark": "Dark",
- "LabelThemeLight": "Light",
- "LabelTimeBase": "Time Base",
+ "LabelSubtitle": "Podnaslov",
+ "LabelSupportedFileTypes": "Podržane vrste datoteka",
+ "LabelTag": "Oznaka",
+ "LabelTags": "Oznake",
+ "LabelTagsAccessibleToUser": "Oznake dostupne korisniku",
+ "LabelTagsNotAccessibleToUser": "Oznake nedostupne korisniku",
+ "LabelTasks": "Zadatci koji se izvode",
+ "LabelTextEditorBulletedList": "Popis s grafičkim oznakama",
+ "LabelTextEditorLink": "Poveznica",
+ "LabelTextEditorNumberedList": "Numerirani popis",
+ "LabelTextEditorUnlink": "Prekini vezu",
+ "LabelTheme": "Tema",
+ "LabelThemeDark": "Tamna",
+ "LabelThemeLight": "Svijetla",
+ "LabelTimeBase": "Baza vremena",
+ "LabelTimeDurationXHours": "{0} sati",
+ "LabelTimeDurationXMinutes": "{0} minuta",
+ "LabelTimeDurationXSeconds": "{0} sekundi",
+ "LabelTimeInMinutes": "Vrijeme u minutama",
"LabelTimeListened": "Vremena odslušano",
"LabelTimeListenedToday": "Vremena odslušano danas",
"LabelTimeRemaining": "{0} preostalo",
"LabelTimeToShift": "Vrijeme za pomjeriti u sekundama",
"LabelTitle": "Naslov",
- "LabelToolsEmbedMetadata": "Embed Metadata",
- "LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
- "LabelToolsMakeM4b": "Make M4B Audiobook File",
- "LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
- "LabelToolsSplitM4b": "Split M4B to MP3's",
- "LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
- "LabelTotalDuration": "Total Duration",
+ "LabelToolsEmbedMetadata": "Ugradi meta-podatke",
+ "LabelToolsEmbedMetadataDescription": "Ugradi meta-podatke u zvučne datoteke zajedno s naslovnicom i poglavljima.",
+ "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",
+ "LabelToolsSplitM4bDescription": "Stvara MP3 datoteke dijeljenjem M4B datoteke po poglavljima, s ugrađenim meta-podatcima, slikom naslovnice i poglavljima.",
+ "LabelTotalDuration": "Ukupno trajanje",
"LabelTotalTimeListened": "Sveukupno vrijeme slušanja",
- "LabelTrackFromFilename": "Track iz imena datoteke",
- "LabelTrackFromMetadata": "Track iz metapodataka",
- "LabelTracks": "Tracks",
- "LabelTracksMultiTrack": "Multi-track",
- "LabelTracksNone": "No tracks",
- "LabelTracksSingleTrack": "Single-track",
- "LabelType": "Tip",
- "LabelUnabridged": "Unabridged",
- "LabelUndo": "Undo",
+ "LabelTrackFromFilename": "Naslov iz imena datoteke",
+ "LabelTrackFromMetadata": "Naslov iz meta-podataka",
+ "LabelTracks": "Naslovi",
+ "LabelTracksMultiTrack": "Više zvučnih zapisa",
+ "LabelTracksNone": "Nema zapisa",
+ "LabelTracksSingleTrack": "Jedan zvučni zapis",
+ "LabelType": "Vrsta",
+ "LabelUnabridged": "Neskraćeno",
+ "LabelUndo": "Vrati",
"LabelUnknown": "Nepoznato",
- "LabelUpdateCover": "Aktualiziraj Cover",
- "LabelUpdateCoverHelp": "Dozvoli postavljanje novog covera za odabrane knjige nakon što je match pronađen.",
- "LabelUpdateDetails": "Aktualiziraj detalje",
- "LabelUpdateDetailsHelp": "Dozvoli postavljanje novih detalja za odabrane knjige nakon što je match pronađen",
- "LabelUpdatedAt": "Aktualizirano",
- "LabelUploaderDragAndDrop": "Drag & Drop datoteke ili foldere",
- "LabelUploaderDropFiles": "Ubaci datoteke",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
- "LabelUseChapterTrack": "Koristi poglavlja track",
- "LabelUseFullTrack": "Koristi cijeli track",
+ "LabelUnknownPublishDate": "Nepoznat datum objavljivanja",
+ "LabelUpdateCover": "Ažuriraj naslovnicu",
+ "LabelUpdateCoverHelp": "Dozvoli prepisivanje postojećih naslovnica za odabrane knjige kada se prepoznaju",
+ "LabelUpdateDetails": "Ažuriraj pojedinosti",
+ "LabelUpdateDetailsHelp": "Dopusti prepisivanje postojećih podataka za odabrane knjige kada se prepoznaju",
+ "LabelUpdatedAt": "Ažurirano",
+ "LabelUploaderDragAndDrop": "Pritisni i prevuci datoteke ili mape",
+ "LabelUploaderDropFiles": "Ispusti datoteke",
+ "LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
+ "LabelUseChapterTrack": "Koristi zvučni zapis poglavlja",
+ "LabelUseFullTrack": "Koristi cijeli zvučni zapis",
"LabelUser": "Korisnik",
"LabelUsername": "Korisničko ime",
"LabelValue": "Vrijednost",
"LabelVersion": "Verzija",
- "LabelViewBookmarks": "View bookmarks",
- "LabelViewChapters": "View chapters",
- "LabelViewQueue": "View player queue",
- "LabelVolume": "Volume",
- "LabelWeekdaysToRun": "Radnih dana da radi",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
- "LabelYourAudiobookDuration": "Tvoje trajanje audiobooka",
- "LabelYourBookmarks": "Tvoje knjižne oznake",
- "LabelYourPlaylists": "Your Playlists",
- "LabelYourProgress": "Tvoj napredak",
- "MessageAddToPlayerQueue": "Add to player queue",
- "MessageAppriseDescription": "To use this feature you will need to have an instance of
Apprise API running or an api that will handle those same requests.
The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at
http://192.168.1.1:8337
then you would put
http://192.168.1.1:8337/notify
.",
+ "LabelViewBookmarks": "Pogledaj knjižne oznake",
+ "LabelViewChapters": "Pogledaj poglavlja",
+ "LabelViewPlayerSettings": "Pogledaj postavke reproduktora",
+ "LabelViewQueue": "Pogledaj redoslijed izvođenja reproduktora",
+ "LabelVolume": "Glasnoća",
+ "LabelWeekdaysToRun": "Dani u tjednu za pokretanje",
+ "LabelXBooks": "{0} knjiga",
+ "LabelXItems": "{0} stavki",
+ "LabelYearReviewHide": "Ne prikazuj Godišnji pregled",
+ "LabelYearReviewShow": "Pogledaj Godišnji pregled",
+ "LabelYourAudiobookDuration": "Trajanje vaših zvučnih knjiga",
+ "LabelYourBookmarks": "Vaše knjižne oznake",
+ "LabelYourPlaylists": "Vaši popisi za izvođenje",
+ "LabelYourProgress": "Vaš napredak",
+ "MessageAddToPlayerQueue": "Dodaj u redoslijed izvođenja",
+ "MessageAppriseDescription": "Da biste se koristili ovom značajkom, treba vam instanca
Apprise API-ja ili API koji može rukovati istom vrstom zahtjeva.
The Adresa Apprise API-ja treba biti puna URL putanja za slanje obavijesti, npr. ako vam se API instanca poslužuje na adresi
http://192.168.1.1:8337
trebate upisati
http://192.168.1.1:8337/notify
.",
"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.",
- "MessageBatchQuickMatchDescription": "Quick Match će probati dodati nedostale covere i metapodatke za odabrane stavke. Uključi postavke ispod da omočutie Quick Mathchu da zamijeni postojeće covere i/ili metapodatke.",
- "MessageBookshelfNoCollections": "You haven't made any collections yet",
- "MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
- "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
- "MessageBookshelfNoSeries": "You have no series",
- "MessageChapterEndIsAfter": "Kraj poglavlja je nakon kraja audioknjige.",
- "MessageChapterErrorFirstNotZero": "First chapter must start at 0",
- "MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
- "MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
- "MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja audioknjige.",
+ "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",
+ "MessageBatchQuickMatchDescription": "Brzo prepoznavanje za odabrane će stavke pokušati dodati naslovnice i meta-podatke koji nedostaju. Uključite donje opcije ako želite da Brzo prepoznavanje prepiše postojeće naslovnice i/ili meta-podatke.",
+ "MessageBookshelfNoCollections": "Niste izradili niti jednu zbirku",
+ "MessageBookshelfNoRSSFeeds": "Nema otvorenih RSS izvora",
+ "MessageBookshelfNoResultsForFilter": "Nema rezultata za filter \"{0}: {1}\"",
+ "MessageBookshelfNoResultsForQuery": "Vaš upit nema rezultata",
+ "MessageBookshelfNoSeries": "Nemate niti jedan serijal",
+ "MessageChapterEndIsAfter": "Kraj poglavlja je nakon kraja zvučne knjige",
+ "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.",
"MessageCheckingCron": "Provjeravam cron...",
- "MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
+ "MessageConfirmCloseFeed": "Sigurno želite zatvoriti ovaj izvor?",
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
- "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
- "MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
- "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
- "MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
- "MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
- "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?",
- "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
- "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
- "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?",
- "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}\"?",
- "MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
- "MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
- "MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
- "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
- "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?",
- "MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
- "MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
- "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
- "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
- "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
- "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
- "MessageDownloadingEpisode": "Preuzimam epizodu",
- "MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
- "MessageEmbedFinished": "Embed završen!",
- "MessageEpisodesQueuedForDownload": "{0} Epizoda/-e u redu za preuzimanje",
- "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 će biti {0}",
- "MessageFetching": "Dobavljam...",
- "MessageForceReScanDescription": "će skenirati sve datoteke ponovno kao svježi sken. ID3 tagovi od audio datoteka, OPF datoteke i tekst datoteke će biti skenirane kao da su nove.",
+ "MessageConfirmDeleteDevice": "Sigurno želite izbrisati e-čitač \"{0}\"?",
+ "MessageConfirmDeleteFile": "Ovo će izbrisati datoteke s datotečnog sustava. Jeste li sigurni?",
+ "MessageConfirmDeleteLibrary": "Sigurno želite trajno obrisati knjižnicu \"{0}\"?",
+ "MessageConfirmDeleteLibraryItem": "Ovo će izbrisati knjižničku stavku iz datoteke i vašeg datotečnog sustava. Jeste li sigurni?",
+ "MessageConfirmDeleteLibraryItems": "Ovo će izbrisati {0} knjižničkih stavki iz baze podataka i datotečnog sustava. Jeste li sigurni?",
+ "MessageConfirmDeleteMetadataProvider": "Sigurno želite izbrisati prilagođenog pružatelja meta-podataka \"{0}\"?",
+ "MessageConfirmDeleteNotification": "Sigurno želite izbrisati ovu obavijest?",
+ "MessageConfirmDeleteSession": "Sigurno želite obrisati ovu sesiju?",
+ "MessageConfirmForceReScan": "Sigurno želite ponovno pokrenuti skeniranje?",
+ "MessageConfirmMarkAllEpisodesFinished": "Sigurno želite označiti sve nastavke dovršenima?",
+ "MessageConfirmMarkAllEpisodesNotFinished": "Sigurno želite označiti sve nastavke nedovršenima?",
+ "MessageConfirmMarkItemFinished": "Sigurno želite označiti \"{0}\" dovršenim?",
+ "MessageConfirmMarkItemNotFinished": "Sigurno želite označiti \"{0}\" nedovršenim?",
+ "MessageConfirmMarkSeriesFinished": "Sigurno želite označiti sve knjige u ovom serijalu dovršenima?",
+ "MessageConfirmMarkSeriesNotFinished": "Sigurno želite označiti sve knjige u ovom serijalu nedovršenima?",
+ "MessageConfirmNotificationTestTrigger": "Želite li okinuti ovu obavijest s probnim podatcima?",
+ "MessageConfirmPurgeCache": "Brisanje predmemorije izbrisat će cijelu mapu
/metadata/cache
.
Sigurno želite izbrisati mapu predmemorije?",
+ "MessageConfirmPurgeItemsCache": "Brisanje predmemorije stavki izbrisat će cijelu mapu
/metadata/cache/items
.
Jeste li sigurni?",
+ "MessageConfirmQuickEmbed": "Pažnja! Funkcija brzog ugrađivanja ne stvara sigurnosne kopije vaših zvučnih datoteka. Provjerite imate li sigurnosnu kopiju.
Želite li nastaviti?",
+ "MessageConfirmReScanLibraryItems": "Sigurno želite ponovno skenirati {0} stavki?",
+ "MessageConfirmRemoveAllChapters": "Sigurno želite ukloniti sva poglavlja?",
+ "MessageConfirmRemoveAuthor": "Sigurno želite ukloniti autora \"{0}\"?",
+ "MessageConfirmRemoveCollection": "Sigurno želite obrisati kolekciju \"{0}\"?",
+ "MessageConfirmRemoveEpisode": "Sigurno želite ukloniti nastavak \"{0}\"?",
+ "MessageConfirmRemoveEpisodes": "Sigurno želite ukloniti {0} nastavaka?",
+ "MessageConfirmRemoveListeningSessions": "Sigurno želite ukloniti {0} sesija slušanja?",
+ "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?",
+ "MessageConfirmRenameGenreMergeNote": "Napomena: Ovaj žanr već postoji, stoga će biti pripojen.",
+ "MessageConfirmRenameGenreWarning": "Pažnja! Sličan žanr s drugačijim velikim i malim slovima već postoji \"{0}\".",
+ "MessageConfirmRenameTag": "Sigurno želite preimenovati oznaku \"{0}\" u \"{1}\" za sve stavke?",
+ "MessageConfirmRenameTagMergeNote": "Napomena: Ova oznaka već postoji, stoga će biti pripojena.",
+ "MessageConfirmRenameTagWarning": "Pažnja! Slična oznaka s drugačijim velikim i malim slovima već postoji \"{0}\".",
+ "MessageConfirmResetProgress": "Sigurno želite resetirati napredak?",
+ "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",
+ "MessageEmbedFailed": "Ugrađivanje nije uspjelo!",
+ "MessageEmbedFinished": "Ugrađivanje je dovršeno!",
+ "MessageEpisodesQueuedForDownload": "{0} nastavak(a) u redu za preuzimanje",
+ "MessageEreaderDevices": "Da biste osigurali isporuku e-knjiga, možda ćete morati gornju adresu e-pošte dodati kao dopuštenog pošiljatelja za svaki od donjih uređaja.",
+ "MessageFeedURLWillBe": "URL izvora bit će {0}",
+ "MessageFetching": "Dohvaćam...",
+ "MessageForceReScanDescription": "će ponovno skenirati sve datoteke kao nove datoteke. ID3 tagovi zvučnih datoteka, OPF datoteke i tekstualne datoteke skenirat će se kao da su nove.",
"MessageImportantNotice": "Važna obavijest!",
"MessageInsertChapterBelow": "Unesi poglavlje ispod",
"MessageItemsSelected": "{0} odabranih stavki",
- "MessageItemsUpdated": "{0} Items Updated",
+ "MessageItemsUpdated": "{0} stavki ažurirano",
"MessageJoinUsOn": "Pridruži nam se na",
"MessageListeningSessionsInTheLastYear": "{0} slušanja u prošloj godini",
"MessageLoading": "Učitavam...",
- "MessageLoadingFolders": "Učitavam foldere...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
+ "MessageLoadingFolders": "Učitavam mape...",
+ "MessageLogsDescription": "Zapisnici se čuvaju u
/metadata/logs
u obliku JSON datoteka. Zapisnici pada sustava čuvaju se u datoteci
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B neuspješan!",
"MessageM4BFinished": "M4B završio!",
- "MessageMapChapterTitles": "Mapiraj imena poglavlja u postoječa poglavlja bez izmijene timestampova.",
- "MessageMarkAllEpisodesFinished": "Mark all episodes finished",
- "MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
- "MessageMarkAsFinished": "Označi kao završeno",
- "MessageMarkAsNotFinished": "Označi kao nezavršeno",
- "MessageMatchBooksDescription": "će probati matchati knjige iz biblioteke sa knjigom od odabranog poslužitelja i popuniti prazne detalje i cover. Ne briše postojeće detalje.",
- "MessageNoAudioTracks": "Nema audio tracks",
+ "MessageMapChapterTitles": "Mapiraj nazive poglavlja postojećim poglavljima zvučne knjige bez uređivanja vremenskih identifikatora",
+ "MessageMarkAllEpisodesFinished": "Označi sve nastavke dovršenima",
+ "MessageMarkAllEpisodesNotFinished": "Označi sve nastavke nedovršenima",
+ "MessageMarkAsFinished": "Označi kao dovršeno",
+ "MessageMarkAsNotFinished": "Označi kao nedovršeno",
+ "MessageMatchBooksDescription": "će pokušati prepoznati knjige iz knjižnice u katalogu odabranog pružatelja podatka te nadopuniti podatke koji nedostaju i naslovnice. Ne prepisuje preko postojećih podataka.",
+ "MessageNoAudioTracks": "Nema zvučnih zapisa",
"MessageNoAuthors": "Nema autora",
- "MessageNoBackups": "Nema backupa",
- "MessageNoBookmarks": "Nema knjižnih bilješki",
+ "MessageNoBackups": "Nema sigurnosnih kopija",
+ "MessageNoBookmarks": "Nema knjižnih oznaka",
"MessageNoChapters": "Nema poglavlja",
- "MessageNoCollections": "Nema kolekcija",
- "MessageNoCoversFound": "Covers nisu pronađeni",
+ "MessageNoCollections": "Nema zbirki",
+ "MessageNoCoversFound": "Naslovnice nisu pronađene",
"MessageNoDescription": "Nema opisa",
- "MessageNoDownloadsInProgress": "No downloads currently in progress",
- "MessageNoDownloadsQueued": "No downloads queued",
- "MessageNoEpisodeMatchesFound": "Nijedna epizoda pronađena",
- "MessageNoEpisodes": "Nema epizoda",
- "MessageNoFoldersAvailable": "Nema dostupnih foldera",
+ "MessageNoDevices": "Nema uređaja",
+ "MessageNoDownloadsInProgress": "Nema preuzimanja u tijeku",
+ "MessageNoDownloadsQueued": "Nema preuzimanja u redu",
+ "MessageNoEpisodeMatchesFound": "Nije pronađen ni jedan odgovarajući nastavak",
+ "MessageNoEpisodes": "Nema nastavaka",
+ "MessageNoFoldersAvailable": "Nema dostupnih mapa",
"MessageNoGenres": "Nema žanrova",
- "MessageNoIssues": "No Issues",
+ "MessageNoIssues": "Nema problema",
"MessageNoItems": "Nema stavki",
- "MessageNoItemsFound": "Nijedna stavka pronađena",
- "MessageNoListeningSessions": "Nema Listening Sessions",
- "MessageNoLogs": "Nema Logs",
- "MessageNoMediaProgress": "Nema Media napredka",
+ "MessageNoItemsFound": "Nema pronađenih stavki",
+ "MessageNoListeningSessions": "Nema sesija slušanja",
+ "MessageNoLogs": "Nema zapisnika",
+ "MessageNoMediaProgress": "Nema podataka o započetim medijima",
"MessageNoNotifications": "Nema obavijesti",
- "MessageNoPodcastsFound": "Nijedan podcast pronađen",
+ "MessageNoPodcastsFound": "Nije pronađen niti jedan podcast",
"MessageNoResults": "Nema rezultata",
- "MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
- "MessageNoSeries": "No Series",
- "MessageNoTags": "No Tags",
- "MessageNoTasksRunning": "No Tasks Running",
- "MessageNoUpdateNecessary": "Aktualiziranje nije potrebno",
- "MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno",
- "MessageNoUserPlaylists": "You have no playlists",
- "MessageNotYetImplemented": "Not yet implemented",
- "MessageOr": "or",
- "MessagePauseChapter": "Pause chapter playback",
- "MessagePlayChapter": "Listen to beginning of chapter",
- "MessagePlaylistCreateFromCollection": "Create playlist from collection",
- "MessagePodcastHasNoRSSFeedForMatching": "Podcast nema RSS feed url za matchanje",
- "MessageQuickMatchDescription": "Popuni prazne detalje stavki i cover sa prvim match rezultato iz '{0}'. Ne briše detalje osim ako 'Prefer matched metadata' server postavka nije uključena.",
- "MessageRemoveChapter": "Remove chapter",
- "MessageRemoveEpisodes": "ukloni {0} epizoda/-e",
- "MessageRemoveFromPlayerQueue": "Remove from player queue",
- "MessageRemoveUserWarning": "Jeste li sigurni da želite trajno obrisati korisnika \"{0}\"?",
- "MessageReportBugsAndContribute": "Prijavte bugove, zatržite featurese i doprinosite na",
- "MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
- "MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran",
- "MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.
Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.
Svi klijenti koji koriste tvoj server će biti automatski osvježeni.",
- "MessageSearchResultsFor": "Traži rezultate za",
- "MessageSelected": "{0} selected",
- "MessageServerCouldNotBeReached": "Server ne može biti kontaktiran",
- "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
+ "MessageNoSearchResultsFor": "Nema rezultata pretrage za \"{0}\"",
+ "MessageNoSeries": "Nema serijala",
+ "MessageNoTags": "Nema oznaka",
+ "MessageNoTasksRunning": "Nema zadataka koji se izvode",
+ "MessageNoUpdatesWereNecessary": "Ažuriranje nije bilo potrebno",
+ "MessageNoUserPlaylists": "Nemate popisa za izvođenje",
+ "MessageNotYetImplemented": "Još nije implementirano",
+ "MessageOpmlPreviewNote": "Napomena: Ovo je pretpregled raščlanjene OPML datoteke. Stvarni naslov podcasta preuzet će se iz RSS izvora.",
+ "MessageOr": "ili",
+ "MessagePauseChapter": "Pauziraj reprodukciju poglavlja",
+ "MessagePlayChapter": "Slušaj početak poglavlja",
+ "MessagePlaylistCreateFromCollection": "Stvori popis za izvođenje od zbirke",
+ "MessagePleaseWait": "Molimo pričekajte...",
+ "MessagePodcastHasNoRSSFeedForMatching": "Podcast nema adresu RSS izvora za prepoznavanje",
+ "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",
+ "MessageRemoveFromPlayerQueue": "Ukloni iz redoslijeda izvođenja",
+ "MessageRemoveUserWarning": "Sigurno želite trajno obrisati korisnika \"{0}\"?",
+ "MessageReportBugsAndContribute": "Prijavite pogreške, zatražite funkcionalnosti i doprinesite na",
+ "MessageResetChaptersConfirm": "Sigurno želite vratiti poglavlja na prethodno stanje i poništiti učinjene promjene?",
+ "MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu",
+ "MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.
Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati.
Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.",
+ "MessageSearchResultsFor": "Rezultati pretrage za",
+ "MessageSelected": "{0} odabrano",
+ "MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju",
+ "MessageSetChaptersFromTracksDescription": "Postavi poglavlja koristeći se zvučnom datotekom kao poglavljem i nazivom datoteke kao naslovom poglavlja",
+ "MessageShareExpirationWillBe": "Vrijeme isteka će biti
{0} ",
+ "MessageShareExpiresIn": "Istječe za {0}",
+ "MessageShareURLWillBe": "URL za dijeljenje bit će
{0} ",
"MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?",
"MessageThinking": "Razmišljam...",
- "MessageUploaderItemFailed": "Upload neuspješan",
- "MessageUploaderItemSuccess": "Upload uspješan!",
- "MessageUploading": "Uploadam...",
- "MessageValidCronExpression": "Ispravan cron expression",
- "MessageWatcherIsDisabledGlobally": "Watcher je globalno isključen u postavkama servera",
- "MessageXLibraryIsEmpty": "{0} Library is empty!",
- "MessageYourAudiobookDurationIsLonger": "Trajanje audio knjige je duže nego pronadeđna duljina trajanja",
- "MessageYourAudiobookDurationIsShorter": "Trajanje audio knjige je kraća nego pronadeđna duljina trajanja",
- "NoteChangeRootPassword": "Root korisnik je jedini korisnik koji može imati praznu lozinku",
- "NoteChapterEditorTimes": "Bilješka: Prvo početno vrijeme poglavlja mora ostati na 0:00 i posljednje vrijeme poglavlja ne smije preći vrijeme trajanja ove audio knjige.",
- "NoteFolderPicker": "Bilješka: več mapirani folderi neće biti prikazani",
- "NoteRSSFeedPodcastAppsHttps": "Upozorenje: Večina podcasta će trebati RSS feed URL koji koristi HTTPS",
- "NoteRSSFeedPodcastAppsPubDate": "Upozorenje: 1 ili više vaših epizoda nemaju datum objavljivanja. Neke podcast aplikacije zahtjevaju to.",
- "NoteUploaderFoldersWithMediaFiles": "Folderi sa media datotekama će biti tretirane kao odvojene stavke u biblioteki.",
- "NoteUploaderOnlyAudioFiles": "Ako uploadate samo audio datoteke onda će audio datoteka biti tretirana kao odvojena audioknjiga.",
- "NoteUploaderUnsupportedFiles": "Nepodržane datoteke su ignorirane. Kada birate ili ubacujete folder, ostale datoteke koje nisu folder će biti ignorirane.",
- "PlaceholderNewCollection": "Ime nove kolekcije",
- "PlaceholderNewFolderPath": "Nova folder putanja",
- "PlaceholderNewPlaylist": "New playlist name",
+ "MessageUploaderItemFailed": "Učitavanje nije uspjelo",
+ "MessageUploaderItemSuccess": "Uspješno učitano!",
+ "MessageUploading": "Učitavam...",
+ "MessageValidCronExpression": "Ispravan cron izraz",
+ "MessageWatcherIsDisabledGlobally": "Praćenje datotečnog sustava globalno je isključen u postavkama poslužitelja",
+ "MessageXLibraryIsEmpty": "{0} Knjižnica je prazna!",
+ "MessageYourAudiobookDurationIsLonger": "Vaše trajanje zvučne knjige duže je od pronađenog trajanja",
+ "MessageYourAudiobookDurationIsShorter": "Vaše trajanje zvučne knjige kraće je od pronađenog trajanja",
+ "NoteChangeRootPassword": "Samo root korisnik može imati praznu zaporku",
+ "NoteChapterEditorTimes": "Napomena: Vrijeme početka prvog poglavlja mora ostati 0:00, a vrijeme početka zadnjeg poglavlja ne može premašiti ukupno trajanje ove zvučne knjige.",
+ "NoteFolderPicker": "Napomena: mape koje su već mapirane neće se prikazati",
+ "NoteRSSFeedPodcastAppsHttps": "Pripazite: Većina aplikacija za podcaste iziskuje URL RSS izvora koji se koristi HTTPS protokolom",
+ "NoteRSSFeedPodcastAppsPubDate": "Upozorenje: jedan ili više vaših nastavaka nemaju datum objavljivanja. To je obavezno kod nekih aplikacija za podcaste.",
+ "NoteUploaderFoldersWithMediaFiles": "Mape s medijskim datotekama smatrat će se zasebnim stavkama knjižnice.",
+ "NoteUploaderOnlyAudioFiles": "Ako učitavate samo zvučne datoteke svaka će se zvučna datoteka uvesti kao zasebna zvučna knjiga.",
+ "NoteUploaderUnsupportedFiles": "Nepodržane vrste datoteka zanemaruju se. Kada odabirete datoteke ili ispuštate mapu, sve datoteke koje nisu u mapi stavke zanemarit će se.",
+ "PlaceholderNewCollection": "Ime nove zbirke",
+ "PlaceholderNewFolderPath": "Nova putanja mape",
+ "PlaceholderNewPlaylist": "Naziv novog popisa za izvođenje",
"PlaceholderSearch": "Traži...",
- "PlaceholderSearchEpisode": "Search episode...",
- "ToastAccountUpdateFailed": "Neuspješno aktualiziranje korisničkog računa",
- "ToastAccountUpdateSuccess": "Korisnički račun aktualiziran",
- "ToastAuthorImageRemoveFailed": "Neuspješno uklanjanje slike",
+ "PlaceholderSearchEpisode": "Traži nastavak...",
+ "StatsAuthorsAdded": "autora dodano",
+ "StatsBooksAdded": "knjiga dodano",
+ "StatsBooksAdditional": "Novi naslovi uključuju…",
+ "StatsBooksFinished": "knjiga dovršeno",
+ "StatsBooksFinishedThisYear": "Neke knjige dovršene ove godine…",
+ "StatsBooksListenedTo": "knjiga slušano",
+ "StatsCollectionGrewTo": "Vaša zbirka knjiga narasla je na…",
+ "StatsSessions": "sesija",
+ "StatsSpentListening": "provedeno u slušanju",
+ "StatsTopAuthor": "NAJPOPULARNIJI AUTOR",
+ "StatsTopAuthors": "NAJPOPULARNIJI AUTORI",
+ "StatsTopGenre": "NAJPOPULARNIJI ŽANR",
+ "StatsTopGenres": "NAJPOPULARNIJI ŽANROVI",
+ "StatsTopMonth": "NAJJAČI MJESEC",
+ "StatsTopNarrator": "NAJPOPULARNIJI PRIPOVJEDAČ",
+ "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",
"ToastAuthorImageRemoveSuccess": "Slika autora uklonjena",
- "ToastAuthorUpdateFailed": "Neuspješno aktualiziranje autora",
- "ToastAuthorUpdateMerged": "Autor spojen",
- "ToastAuthorUpdateSuccess": "Autor aktualiziran ",
- "ToastAuthorUpdateSuccessNoImageFound": "Autor aktualiziran (slika nije pronađena)",
+ "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)",
+ "ToastBackupAppliedSuccess": "Sigurnosna kopija vraćena",
"ToastBackupCreateFailed": "Neuspješno kreiranje backupa",
- "ToastBackupCreateSuccess": "Backup kreiran",
- "ToastBackupDeleteFailed": "Neuspješno brisanje backupa",
- "ToastBackupDeleteSuccess": "Backup obrisan",
- "ToastBackupRestoreFailed": "Povračanje backupa neuspješno",
- "ToastBackupUploadFailed": "Uploadanje backupa neuspješno",
- "ToastBackupUploadSuccess": "Backup uploadan",
- "ToastBatchUpdateFailed": "Batch update neuspješan",
- "ToastBatchUpdateSuccess": "Batch update uspješan",
- "ToastBookmarkCreateFailed": "Kreiranje knjižne bilješke neuspješno",
- "ToastBookmarkCreateSuccess": "Knjižna bilješka dodana",
- "ToastBookmarkRemoveFailed": "Brisanje knjižne bilješke nauspješno",
- "ToastBookmarkRemoveSuccess": "Knjižnja bilješka uklonjena",
- "ToastBookmarkUpdateFailed": "Aktualizacija knjižne bilješke neuspješna",
- "ToastBookmarkUpdateSuccess": "Knjižna bilješka aktualizirana",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
- "ToastChaptersHaveErrors": "Chapters have errors",
- "ToastChaptersMustHaveTitles": "Chapters must have titles",
- "ToastCollectionItemsRemoveFailed": "Neuspješno brisanje stavke/-i iz kolekcije",
- "ToastCollectionItemsRemoveSuccess": "Stavka/-e obrisane iz kolekcije",
- "ToastCollectionRemoveFailed": "Brisanje kolekcije neuspješno",
- "ToastCollectionRemoveSuccess": "Kolekcija obrisana",
- "ToastCollectionUpdateFailed": "Aktualiziranje kolekcije neuspješno",
- "ToastCollectionUpdateSuccess": "Kolekcija aktualizirana",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
- "ToastItemCoverUpdateFailed": "Aktualiziranje covera stavke neuspješna",
- "ToastItemCoverUpdateSuccess": "Cover stavke aktualiziran",
- "ToastItemDetailsUpdateFailed": "Aktualiziranje detalja stavke neuspješno",
- "ToastItemDetailsUpdateSuccess": "Detalji stavke aktualizirani",
- "ToastItemDetailsUpdateUnneeded": "Aktualiziranje detalja stavke nepotrebno",
- "ToastItemMarkedAsFinishedFailed": "Označi kao Završeno neuspješno",
- "ToastItemMarkedAsFinishedSuccess": "Stavka označena kao Završeno",
- "ToastItemMarkedAsNotFinishedFailed": "Označi kao Nezavršeno neuspješno",
- "ToastItemMarkedAsNotFinishedSuccess": "Stavka oznaečena kao Nezavršeno",
- "ToastLibraryCreateFailed": "Kreiranje biblioteke neuspješno",
- "ToastLibraryCreateSuccess": "Biblioteka \"{0}\" kreirana",
- "ToastLibraryDeleteFailed": "Brisanje biblioteke neuspješno",
- "ToastLibraryDeleteSuccess": "Biblioteka obrisana",
- "ToastLibraryScanFailedToStart": "Skeniranje neuspješno",
- "ToastLibraryScanStarted": "Sken biblioteke pokrenut",
- "ToastLibraryUpdateFailed": "Aktualiziranje biblioteke neuspješno",
- "ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" aktualizirana",
- "ToastPlaylistCreateFailed": "Failed to create playlist",
- "ToastPlaylistCreateSuccess": "Playlist created",
- "ToastPlaylistRemoveFailed": "Failed to remove playlist",
- "ToastPlaylistRemoveSuccess": "Playlist removed",
- "ToastPlaylistUpdateFailed": "Failed to update playlist",
- "ToastPlaylistUpdateSuccess": "Playlist updated",
- "ToastPodcastCreateFailed": "Neuspješno kreiranje podcasta",
- "ToastPodcastCreateSuccess": "Podcast uspješno kreiran",
- "ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
- "ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
- "ToastRemoveItemFromCollectionFailed": "Neuspješno uklanjanje stavke iz kolekcije",
- "ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
- "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
- "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
- "ToastSeriesUpdateFailed": "Series update failed",
- "ToastSeriesUpdateSuccess": "Series update success",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
+ "ToastBackupCreateSuccess": "Izrađena sigurnosna kopija",
+ "ToastBackupDeleteFailed": "Brisanje sigurnosne kopije nije uspjelo",
+ "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",
+ "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",
+ "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",
+ "ToastDeleteFileSuccess": "Datoteka izbrisana",
+ "ToastDeviceAddFailed": "Dodavanje uređaja nije uspjelo",
+ "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",
+ "ToastErrorCannotShare": "Dijeljenje na ovaj uređaj nije moguće",
+ "ToastFailedToLoadData": "Učitavanje podataka nije uspjelo",
+ "ToastFailedToShare": "Dijeljenje nije uspjelo",
+ "ToastFailedToUpdateAccount": "Ažuriranje računa nije uspjelo",
+ "ToastFailedToUpdateUser": "Ažuriranje korisnika nije uspjelo",
+ "ToastInvalidImageUrl": "Neispravan URL slike",
+ "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",
+ "ToastLibraryDeleteFailed": "Brisanje knjižnice nije uspjelo",
+ "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",
+ "ToastNameEmailRequired": "Ime i adresa e-pošte su obavezni",
+ "ToastNameRequired": "Ime je obavezno",
+ "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",
+ "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",
+ "ToastProviderCreatedFailed": "Dodavanje pružatelja nije uspjelo",
+ "ToastProviderCreatedSuccess": "Novi pružatelj dodan",
+ "ToastProviderNameAndUrlRequired": "Ime i URL su obavezni",
+ "ToastProviderRemoveSuccess": "Pružatelj uklonjen",
+ "ToastRSSFeedCloseFailed": "RSS izvor nije uspješno zatvoren",
+ "ToastRSSFeedCloseSuccess": "RSS izvor zatvoren",
+ "ToastRemoveFailed": "Uklanjanje nije uspjelo",
+ "ToastRemoveItemFromCollectionFailed": "Neuspješno uklanjanje stavke iz zbirke",
+ "ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz zbirke",
+ "ToastRemoveItemsWithIssuesFailed": "Uklanjanje knjižničkih stavki s problemima nije uspjelo",
+ "ToastRemoveItemsWithIssuesSuccess": "Uspješno uklonjene knjižničke stavke s problemima",
+ "ToastRenameFailed": "Preimenovanje nije uspjelo",
+ "ToastRescanFailed": "Ponovno skeniranje {0} nije uspjelo",
+ "ToastRescanRemoved": "Ponovno skeniranje dovršene stavke je uklonjeno",
+ "ToastRescanUpToDate": "Ponovno skeniranje dovršene stavke bilo je ažurno",
+ "ToastRescanUpdated": "Ponovno skeniranje dovršene stavke je ažurirano",
+ "ToastScanFailed": "Skeniranje knjižničke stavke nije uspjelo",
+ "ToastSelectAtLeastOneUser": "Odaberite najmanje jednog korisnika",
+ "ToastSendEbookToDeviceFailed": "Slanje e-knjige na uređaj nije uspjelo",
+ "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",
- "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)",
+ "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",
+ "ToastUnlinkOpenIdFailed": "Uklanjanje OpenID veze korisnika nije uspjelo",
+ "ToastUnlinkOpenIdSuccess": "Korisnik odspojen od OpenID-ja",
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
- "ToastUserDeleteSuccess": "Korisnik obrisan"
+ "ToastUserDeleteSuccess": "Korisnik obrisan",
+ "ToastUserPasswordChangeSuccess": "Zaporka je uspješno promijenjena",
+ "ToastUserPasswordMismatch": "Zaporke se ne podudaraju",
+ "ToastUserPasswordMustChange": "Nova zaporka ne smije biti jednaka staroj",
+ "ToastUserRootRequireName": "Obavezan je unos korisničkog imena root korisnika"
}
diff --git a/client/strings/hu.json b/client/strings/hu.json
index cc12227a59..a50fce1b3b 100644
--- a/client/strings/hu.json
+++ b/client/strings/hu.json
@@ -9,7 +9,6 @@
"ButtonApply": "Alkalmaz",
"ButtonApplyChapters": "Fejezetek alkalmazása",
"ButtonAuthors": "Szerzők",
- "ButtonBack": "Back",
"ButtonBrowseForFolder": "Mappa keresése",
"ButtonCancel": "Mégse",
"ButtonCancelEncode": "Kódolás megszakítása",
@@ -44,7 +43,6 @@
"ButtonMatchAllAuthors": "Minden szerző egyeztetése",
"ButtonMatchBooks": "Könyvek egyeztetése",
"ButtonNevermind": "Mindegy",
- "ButtonNext": "Next",
"ButtonNextChapter": "Következő fejezet",
"ButtonOk": "Oké",
"ButtonOpenFeed": "Hírcsatorna megnyitása",
@@ -53,7 +51,6 @@
"ButtonPlay": "Lejátszás",
"ButtonPlaying": "Lejátszás folyamatban",
"ButtonPlaylists": "Lejátszási listák",
- "ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Előző fejezet",
"ButtonPurgeAllCache": "Összes gyorsítótár törlése",
"ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése",
@@ -62,9 +59,6 @@
"ButtonQuickMatch": "Gyors egyeztetés",
"ButtonReScan": "Újraszkennelés",
"ButtonRead": "Olvasás",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
"ButtonRemove": "Eltávolítás",
"ButtonRemoveAll": "Összes eltávolítása",
"ButtonRemoveAllLibraryItems": "Összes könyvtárelem eltávolítása",
@@ -83,7 +77,6 @@
"ButtonSelectFolderPath": "Mappa útvonalának kiválasztása",
"ButtonSeries": "Sorozatok",
"ButtonSetChaptersFromTracks": "Fejezetek beállítása sávokból",
- "ButtonShare": "Share",
"ButtonShiftTimes": "Idők eltolása",
"ButtonShow": "Megjelenítés",
"ButtonStartM4BEncode": "M4B kódolás indítása",
@@ -115,7 +108,6 @@
"HeaderCollectionItems": "Gyűjtemény elemek",
"HeaderCover": "Borító",
"HeaderCurrentDownloads": "Jelenlegi letöltések",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
"HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók",
"HeaderDetails": "Részletek",
"HeaderDownloadQueue": "Letöltési sor",
@@ -187,14 +179,9 @@
"HeaderUpdateDetails": "Részletek frissítése",
"HeaderUpdateLibrary": "Könyvtár frissítése",
"HeaderUsers": "Felhasználók",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Saját statisztikák",
"LabelAbridged": "Tömörített",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Fióktípus",
- "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Vendég",
"LabelAccountTypeUser": "Felhasználó",
"LabelActivity": "Tevékenység",
@@ -202,7 +189,6 @@
"LabelAddToCollectionBatch": "{0} könyv hozzáadása a gyűjteményhez",
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
"LabelAddToPlaylistBatch": "{0} elem hozzáadása a lejátszási listához",
- "LabelAdded": "Hozzáadva",
"LabelAddedAt": "Hozzáadás ideje",
"LabelAdminUsersOnly": "Csak admin felhasználók",
"LabelAll": "Minden",
@@ -233,7 +219,6 @@
"LabelBitrate": "Bitráta",
"LabelBooks": "Könyvek",
"LabelButtonText": "Gomb szövege",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Jelszó megváltoztatása",
"LabelChannels": "Csatornák",
"LabelChapterTitle": "Fejezet címe",
@@ -271,17 +256,12 @@
"LabelDownload": "Letöltés",
"LabelDownloadNEpisodes": "{0} epizód letöltése",
"LabelDuration": "Időtartam",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Megtalált időtartam:",
"LabelEbook": "E-könyv",
"LabelEbooks": "E-könyvek",
"LabelEdit": "Szerkesztés",
"LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Feladó címe",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"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",
@@ -292,9 +272,6 @@
"LabelEpisodeTitle": "Epizód címe",
"LabelEpisodeType": "Epizód típusa",
"LabelExample": "Példa",
- "LabelExplicit": "Explicit",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "Hírcsatorna URL",
"LabelFetchingMetadata": "Metaadatok lekérése",
"LabelFile": "Fájl",
@@ -307,7 +284,6 @@
"LabelFolder": "Mappa",
"LabelFolders": "Mappák",
"LabelFontBold": "Félkövér",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Betűtípus család",
"LabelFontItalic": "Dőlt",
"LabelFontScale": "Betűméret skála",
@@ -339,7 +315,6 @@
"LabelItem": "Elem",
"LabelLanguage": "Nyelv",
"LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "Utolsó hozzáadott könyv",
"LabelLastBookUpdated": "Utolsó frissített könyv",
"LabelLastSeen": "Utolsó látogatás",
@@ -351,13 +326,11 @@
"LabelLess": "Kevesebb",
"LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak",
"LabelLibrary": "Könyvtár",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Könyvtári elem",
"LabelLibraryName": "Könyvtár neve",
"LabelLimit": "Korlát",
"LabelLineSpacing": "Sorköz",
"LabelListenAgain": "Újrahallgatás",
- "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Információ",
"LabelLogLevelWarn": "Figyelmeztetés",
"LabelLookForNewEpisodesAfterDate": "Új epizódok keresése ezen a dátum után",
@@ -372,8 +345,6 @@
"LabelMetadataProvider": "Metaadat-szolgáltató",
"LabelMinute": "Perc",
"LabelMissing": "Hiányzó",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"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",
@@ -387,7 +358,6 @@
"LabelNewestEpisodes": "Legújabb epizódok",
"LabelNextBackupDate": "Következő biztonsági másolat dátuma",
"LabelNextScheduledRun": "Következő ütemezett futtatás",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok",
"LabelNotFinished": "Nem befejezett",
"LabelNotStarted": "Nem indult el",
@@ -403,9 +373,6 @@
"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": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "RSS hírcsatorna megnyitása",
"LabelOverwrite": "Felülírás",
"LabelPassword": "Jelszó",
@@ -417,16 +384,12 @@
"LabelPermissionsDownload": "Letölthet",
"LabelPermissionsUpdate": "Frissíthet",
"LabelPermissionsUpload": "Feltölthet",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Fénykép útvonal/URL",
"LabelPlayMethod": "Lejátszási módszer",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"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",
"LabelPrimaryEbook": "Elsődleges e-könyv",
@@ -435,7 +398,6 @@
"LabelPubDate": "Kiadás dátuma",
"LabelPublishYear": "Kiadás éve",
"LabelPublisher": "Kiadó",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
"LabelRSSFeedOpen": "RSS hírcsatorna nyitva",
@@ -457,7 +419,6 @@
"LabelSearchTitle": "Cím keresése",
"LabelSearchTitleOrASIN": "Cím vagy ASIN keresése",
"LabelSeason": "Évad",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Összes epizód kiválasztása",
"LabelSelectEpisodesShowing": "Kiválasztás {0} megjelenített epizód",
"LabelSelectUsers": "Felhasználók kiválasztása",
@@ -466,7 +427,6 @@
"LabelSeries": "Sorozat",
"LabelSeriesName": "Sorozat neve",
"LabelSeriesProgress": "Sorozat haladása",
- "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Beállítás elsődlegesként",
"LabelSetEbookAsSupplementary": "Beállítás kiegészítőként",
"LabelSettingsAudiobooksOnly": "Csak hangoskönyvek",
@@ -480,8 +440,6 @@
"LabelSettingsEnableWatcher": "Figyelő engedélyezése",
"LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban",
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Kísérleti funkciók",
"LabelSettingsExperimentalFeaturesHelp": "Fejlesztés alatt álló funkciók, amelyek visszajelzésre és tesztelésre szorulnak. Kattintson a github megbeszélés megnyitásához.",
"LabelSettingsFindCovers": "Borítók keresése",
@@ -490,8 +448,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "A csak egy könyvet tartalmazó sorozatok el lesznek rejtve a sorozatok oldalról és a kezdőlap polcairól.",
"LabelSettingsHomePageBookshelfView": "Kezdőlap használja a könyvespolc nézetet",
"LabelSettingsLibraryBookshelfView": "Könyvtár használja a könyvespolc nézetet",
- "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": "Feliratok elemzése",
"LabelSettingsParseSubtitlesHelp": "Feliratok kinyerése a hangoskönyv mappaneveiből.
A feliratnak el kell különülnie egy \" - \" jellel
például: \"Könyv címe - Egy felirat itt\" esetén a felirat \"Egy felirat itt\"",
"LabelSettingsPreferMatchedMetadata": "Preferált egyeztetett metaadatok",
@@ -508,7 +464,6 @@
"LabelSettingsStoreMetadataWithItemHelp": "Alapértelmezés szerint a metaadatfájlok a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a metaadatfájlokat a könyvtári elem mappáiban tárolja",
"LabelSettingsTimeFormat": "Időformátum",
"LabelShowAll": "Mindent mutat",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Méret",
"LabelSleepTimer": "Alvásidőzítő",
"LabelSlug": "Rövid cím",
@@ -539,7 +494,6 @@
"LabelTagsNotAccessibleToUser": "A felhasználó számára nem elérhető címkék",
"LabelTasks": "Futó feladatok",
"LabelTextEditorBulletedList": "Pontozott lista",
- "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Számozott lista",
"LabelTextEditorUnlink": "Link eltávolítása",
"LabelTheme": "Téma",
@@ -588,8 +542,6 @@
"LabelViewQueue": "Lejátszó sor megtekintése",
"LabelVolume": "Hangerő",
"LabelWeekdaysToRun": "Futás napjai",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Hangoskönyv időtartama",
"LabelYourBookmarks": "Könyvjelzőid",
"LabelYourPlaylists": "Lejátszási listáid",
@@ -601,7 +553,6 @@
"MessageBookshelfNoCollections": "Még nem készített gyűjteményeket",
"MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák",
"MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Nincsenek sorozatai",
"MessageChapterEndIsAfter": "A fejezet vége a hangoskönyv végét követi",
"MessageChapterErrorFirstNotZero": "Az első fejezetnek 0:00-kor kell kezdődnie",
@@ -621,8 +572,6 @@
"MessageConfirmMarkAllEpisodesNotFinished": "Biztosan meg szeretné jelölni az összes epizódot nem befejezettnek?",
"MessageConfirmMarkSeriesFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét befejezettnek?",
"MessageConfirmMarkSeriesNotFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét nem befejezettnek?",
- "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": "Figyelem! A Gyors beágyazás nem készít biztonsági másolatot az audiofájlokról. Győződjön meg arról, hogy van biztonsági másolata az audiofájlokról.
Szeretné folytatni?",
"MessageConfirmReScanLibraryItems": "Biztosan újra szeretné szkennelni a(z) {0} elemet?",
"MessageConfirmRemoveAllChapters": "Biztosan eltávolítja az összes fejezetet?",
@@ -644,7 +593,6 @@
"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",
- "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": "A hírcsatorna URL-je {0} lesz",
"MessageFetching": "Lekéré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.",
@@ -652,11 +600,10 @@
"MessageInsertChapterBelow": "Fejezet beszúrása alulra",
"MessageItemsSelected": "{0} kiválasztott elem",
"MessageItemsUpdated": "{0} frissített elem",
- "MessageJoinUsOn": "Csatlakozzon hozzánk: ",
+ "MessageJoinUsOn": "Csatlakozzon hozzánk",
"MessageListeningSessionsInTheLastYear": "{0} hallgatási munkamenet az elmúlt évben",
"MessageLoading": "Betöltés...",
"MessageLoadingFolders": "Mappák betöltése...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B sikertelen!",
"MessageM4BFinished": "M4B befejeződött!",
"MessageMapChapterTitles": "Fejezetcímek hozzárendelése a meglévő hangoskönyv fejezeteihez anélkül, hogy az időbélyegeket módosítaná",
@@ -692,7 +639,6 @@
"MessageNoSeries": "Nincsenek sorozatok",
"MessageNoTags": "Nincsenek címkék",
"MessageNoTasksRunning": "Nincsenek futó feladatok",
- "MessageNoUpdateNecessary": "Nincs szükség frissítésre",
"MessageNoUpdatesWereNecessary": "Nem volt szükség frissítésekre",
"MessageNoUserPlaylists": "Nincsenek felhasználói lejátszási listák",
"MessageNotYetImplemented": "Még nem implementált",
@@ -706,9 +652,9 @@
"MessageRemoveEpisodes": "Epizód(ok) eltávolítása: {0}",
"MessageRemoveFromPlayerQueue": "Eltávolítás a lejátszási sorból",
"MessageRemoveUserWarning": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" felhasználót?",
- "MessageReportBugsAndContribute": "Hibák jelentése, funkciók kérése és hozzájárulás itt:",
+ "MessageReportBugsAndContribute": "Hibák jelentése, funkciók kérése és hozzájárulás itt",
"MessageResetChaptersConfirm": "Biztosan alaphelyzetbe szeretné állítani a fejezeteket és visszavonni a módosításokat?",
- "MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült:",
+ "MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült",
"MessageRestoreBackupWarning": "A biztonsági mentés visszaállítása felülírja az egész adatbázist, amely a /config mappában található, valamint a borítóképeket a /metadata/items és /metadata/authors mappákban.
A biztonsági mentések nem módosítják a könyvtár mappáiban található fájlokat. Ha engedélyezte a szerverbeállításokat a borítóképek és a metaadatok könyvtármappákban való tárolására, akkor ezek nem kerülnek biztonsági mentésre vagy felülírásra.
A szerver használó összes kliens automatikusan frissül.",
"MessageSearchResultsFor": "Keresési eredmények",
"MessageSelected": "{0} kiválasztva",
@@ -739,7 +685,6 @@
"PlaceholderSearchEpisode": "Epizód keresése..",
"ToastAccountUpdateFailed": "A fiók frissítése sikertelen",
"ToastAccountUpdateSuccess": "Fiók frissítve",
- "ToastAuthorImageRemoveFailed": "A kép eltávolítása sikertelen",
"ToastAuthorImageRemoveSuccess": "Szerző képe eltávolítva",
"ToastAuthorUpdateFailed": "A szerző frissítése sikertelen",
"ToastAuthorUpdateMerged": "Szerző összevonva",
@@ -756,28 +701,19 @@
"ToastBatchUpdateSuccess": "Kötegelt frissítés sikeres",
"ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen",
"ToastBookmarkCreateSuccess": "Könyvjelző hozzáadva",
- "ToastBookmarkRemoveFailed": "Könyvjelző eltávolítása sikertelen",
"ToastBookmarkRemoveSuccess": "Könyvjelző eltávolítva",
"ToastBookmarkUpdateFailed": "Könyvjelző frissítése sikertelen",
"ToastBookmarkUpdateSuccess": "Könyvjelző frissítve",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak",
"ToastChaptersMustHaveTitles": "A fejezeteknek címekkel kell rendelkezniük",
- "ToastCollectionItemsRemoveFailed": "Elem(ek) eltávolítása a gyűjteményből sikertelen",
"ToastCollectionItemsRemoveSuccess": "Elem(ek) eltávolítva a gyűjteményből",
- "ToastCollectionRemoveFailed": "Gyűjtemény eltávolítása sikertelen",
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
"ToastCollectionUpdateFailed": "Gyűjtemény frissítése sikertelen",
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"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",
- "ToastItemDetailsUpdateUnneeded": "Nincsenek szükséges frissítések a tétel részletein",
"ToastItemMarkedAsFinishedFailed": "Megjelölés Befejezettként sikertelen",
"ToastItemMarkedAsFinishedSuccess": "Elem megjelölve Befejezettként",
"ToastItemMarkedAsNotFinishedFailed": "Nem sikerült Nem Befejezettként megjelölni az elemet",
@@ -792,7 +728,6 @@
"ToastLibraryUpdateSuccess": "\"{0}\" könyvtár frissítve",
"ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen",
"ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva",
- "ToastPlaylistRemoveFailed": "Lejátszási lista eltávolítása sikertelen",
"ToastPlaylistRemoveSuccess": "Lejátszási lista eltávolítva",
"ToastPlaylistUpdateFailed": "Lejátszási lista frissítése sikertelen",
"ToastPlaylistUpdateSuccess": "Lejátszási lista frissítve",
@@ -806,16 +741,11 @@
"ToastSendEbookToDeviceSuccess": "E-könyv elküldve az eszközre \"{0}\"",
"ToastSeriesUpdateFailed": "Sorozat frissítése sikertelen",
"ToastSeriesUpdateSuccess": "Sorozat frissítése sikeres",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Munkamenet törlése sikertelen",
"ToastSessionDeleteSuccess": "Munkamenet törölve",
"ToastSocketConnected": "Socket csatlakoztatva",
"ToastSocketDisconnected": "Socket lecsatlakoztatva",
"ToastSocketFailedToConnect": "A Socket csatlakoztatása sikertelen",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Felhasználó törlése sikertelen",
"ToastUserDeleteSuccess": "Felhasználó törölve"
}
diff --git a/client/strings/it.json b/client/strings/it.json
index c1251621f0..062b2a108c 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -46,7 +46,6 @@
"ButtonNevermind": "Ingora",
"ButtonNext": "Prossimo",
"ButtonNextChapter": "Prossimo Capitolo",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Apri il flusso",
"ButtonOpenManager": "Apri Manager",
"ButtonPause": "Pausa",
@@ -72,7 +71,6 @@
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
"ButtonRemoveFromContinueReading": "Rimuovi per proseguire la lettura",
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
- "ButtonReset": "Reset",
"ButtonResetToDefault": "Ripristino di default",
"ButtonRestore": "Ripristina",
"ButtonSave": "Salva",
@@ -91,7 +89,6 @@
"ButtonStartMetadataEmbed": "Inizia Incorporo Metadata",
"ButtonStats": "Statistische",
"ButtonSubmit": "Invia",
- "ButtonTest": "Test",
"ButtonUpload": "Carica",
"ButtonUploadBackup": "Carica Backup",
"ButtonUploadCover": "Carica Cover",
@@ -103,7 +100,6 @@
"ErrorUploadFetchMetadataAPI": "Errore Recupero metadati",
"ErrorUploadFetchMetadataNoResults": "Impossibile recuperare i metadati: prova a modificate il titolo e/o l'autore",
"ErrorUploadLacksTitle": "Deve avere un titolo",
- "HeaderAccount": "Account",
"HeaderAdvanced": "Avanzate",
"HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica",
"HeaderAudioTracks": "Tracce audio",
@@ -115,19 +111,16 @@
"HeaderChooseAFolder": "Seleziona la cartella",
"HeaderCollection": "Raccolta",
"HeaderCollectionItems": "Elementi della raccolta",
- "HeaderCover": "Cover",
"HeaderCurrentDownloads": "Download Correnti",
"HeaderCustomMessageOnLogin": "Messaggio personalizzato all'accesso",
"HeaderCustomMetadataProviders": "Metadata Providers Personalizzato",
"HeaderDetails": "Dettagli",
"HeaderDownloadQueue": "Download coda",
"HeaderEbookFiles": "File dei libri",
- "HeaderEmail": "Email",
"HeaderEmailSettings": "Impostazioni Email",
"HeaderEpisodes": "Episodi",
"HeaderEreaderDevices": "Dispositivo Ereader",
"HeaderEreaderSettings": "Impostazioni lettore",
- "HeaderFiles": "Files",
"HeaderFindChapters": "Trova Capitoli",
"HeaderIgnoredFiles": "File Ignorati",
"HeaderItemFiles": "Files",
@@ -139,8 +132,6 @@
"HeaderLibraryStats": "Statistiche Libreria",
"HeaderListeningSessions": "Sessioni di Ascolto",
"HeaderListeningStats": "Statistiche di Ascolto",
- "HeaderLogin": "Login",
- "HeaderLogs": "Logs",
"HeaderManageGenres": "Gestisci Generi",
"HeaderManageTags": "Gestisci Tags",
"HeaderMapDetails": "Mappa Dettagli",
@@ -153,17 +144,13 @@
"HeaderOpenIDConnectAuthentication": "Autenticazione OpenID Connect",
"HeaderOpenRSSFeed": "Apri il flusso RSS",
"HeaderOtherFiles": "Altri File",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Permessi",
"HeaderPlayerQueue": "Coda Riproduzione",
"HeaderPlayerSettings": "Impostazioni Player",
- "HeaderPlaylist": "Playlist",
"HeaderPlaylistItems": "Elementi della playlist",
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
"HeaderPreviewCover": "Anteprima Cover",
- "HeaderRSSFeedGeneral": "RSS Details",
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
- "HeaderRSSFeeds": "RSS Feeds",
"HeaderRemoveEpisode": "Rimuovi Episodi",
"HeaderRemoveEpisodes": "Rimuovi {0} Episodi",
"HeaderSavedMediaProgress": "Progressi salvati",
@@ -172,10 +159,8 @@
"HeaderSession": "Sessione",
"HeaderSetBackupSchedule": "Imposta programmazione Backup",
"HeaderSettings": "Impostazioni",
- "HeaderSettingsDisplay": "Display",
"HeaderSettingsExperimental": "Opzioni Sperimentali",
"HeaderSettingsGeneral": "Generale",
- "HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Sveglia",
"HeaderStatsLargestItems": "Oggetti Grandi",
"HeaderStatsLongestItems": "libri più lunghi (ore)",
@@ -197,7 +182,6 @@
"LabelAbridgedUnchecked": "Integrale (non selezionato)",
"LabelAccessibleBy": "Accessibile da",
"LabelAccountType": "Tipo di Account",
- "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Ospite",
"LabelAccountTypeUser": "Utente",
"LabelActivity": "Attività",
@@ -205,7 +189,6 @@
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
"LabelAddToPlaylist": "Aggiungi alla playlist",
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
- "LabelAdded": "Aggiunto",
"LabelAddedAt": "Aggiunto il",
"LabelAdminUsersOnly": "Solo utenti Amministratori",
"LabelAll": "Tutti",
@@ -233,10 +216,8 @@
"LabelBackupsMaxBackupSizeHelp": "Come protezione contro gli errori di config, i backup falliranno se superano la dimensione configurata.",
"LabelBackupsNumberToKeep": "Numero di backup da mantenere",
"LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.",
- "LabelBitrate": "Bitrate",
"LabelBooks": "Libri",
"LabelButtonText": "Buttone Testo",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Cambia Password",
"LabelChannels": "Canali",
"LabelChapterTitle": "Titoli dei Capitoli",
@@ -244,7 +225,6 @@
"LabelChaptersFound": "Capitoli Trovati",
"LabelClickForMoreInfo": "Click per altre Info",
"LabelClosePlayer": "Chiudi player",
- "LabelCodec": "Codec",
"LabelCollapseSeries": "Comprimi Serie",
"LabelCollection": "Raccolta",
"LabelCollections": "Raccolte",
@@ -253,7 +233,6 @@
"LabelContinueListening": "Continua ad Ascoltare",
"LabelContinueReading": "Continua la Lettura",
"LabelContinueSeries": "Continua serie",
- "LabelCover": "Cover",
"LabelCoverImageURL": "Indirizzo della cover URL",
"LabelCreatedAt": "Creato A",
"LabelCronExpression": "Espressione Cron",
@@ -282,7 +261,6 @@
"LabelEbook": "Libro digitale",
"LabelEbooks": "Libri digitali",
"LabelEdit": "Modifica",
- "LabelEmail": "Email",
"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.",
@@ -304,7 +282,6 @@
"LabelExportOPML": "Esposta OPML",
"LabelFeedURL": "URL del flusso",
"LabelFetchingMetadata": "Recupero dei metadati",
- "LabelFile": "File",
"LabelFileBirthtime": "Data di creazione",
"LabelFileModified": "Ultima modifica",
"LabelFilename": "Nome del file",
@@ -315,8 +292,6 @@
"LabelFolders": "Cartelle",
"LabelFontBold": "Grassetto",
"LabelFontBoldness": "Grassetto",
- "LabelFontFamily": "Font family",
- "LabelFontItalic": "Italic",
"LabelFontScale": "Dimensione font",
"LabelFontStrikethrough": "Barrato",
"LabelFormat": "Formato",
@@ -327,7 +302,6 @@
"LabelHasSupplementaryEbook": "Ha un libro supplementale",
"LabelHideSubtitles": "Nascondi Sottotitoli",
"LabelHighestPriority": "Priorità Massima",
- "LabelHost": "Host",
"LabelHour": "Ora",
"LabelHours": "Ore",
"LabelIcon": "Icona",
@@ -362,25 +336,18 @@
"LabelLess": "Poco",
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
"LabelLibrary": "Libreria",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Elementi della Library",
"LabelLibraryName": "Nome Libreria",
"LabelLimit": "Limiti",
"LabelLineSpacing": "Interlinea",
"LabelListenAgain": "Ascolta ancora",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Allarme",
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
"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",
- "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Tipo Media",
- "LabelMetaTag": "Meta Tag",
- "LabelMetaTags": "Meta Tags",
"LabelMetadataOrderOfPrecedenceDescription": "Le origini di metadati con priorità più alta sovrascriveranno le origini di metadati con priorità inferiore",
- "LabelMetadataProvider": "Metadata Provider",
"LabelMinute": "Minuto",
"LabelMinutes": "Minuti",
"LabelMissing": "Altro",
@@ -420,7 +387,6 @@
"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",
- "LabelPassword": "Password",
"LabelPath": "Percorso",
"LabelPermanent": "Permanente",
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
@@ -433,18 +399,12 @@
"LabelPersonalYearReview": "Il tuo anno in rassegna ({0})",
"LabelPhotoPathURL": "foto Path/URL",
"LabelPlayMethod": "Metodo di riproduzione",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
- "LabelPlaylists": "Playlists",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Area di ricerca podcast",
"LabelPodcastType": "Tipo di Podcast",
- "LabelPodcasts": "Podcasts",
- "LabelPort": "Port",
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
"LabelPrimaryEbook": "Libri Principlae",
"LabelProgress": "Cominciati",
- "LabelProvider": "Provider",
"LabelPubDate": "Data di pubblicazione",
"LabelPublishYear": "Anno di pubblicazione",
"LabelPublisher": "Editore",
@@ -454,7 +414,7 @@
"LabelRSSFeedOpen": "RSS Feed Aperto",
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
"LabelRSSFeedSlug": "Parole chiave del flusso RSS",
- "LabelRSSFeedURL": "RSS Feed URL",
+ "LabelRandomly": "Casualmente",
"LabelReAddSeriesToContinueListening": "Aggiungi di nuovo la serie per continuare ad ascoltare",
"LabelRead": "Leggi",
"LabelReadAgain": "Leggi ancora",
@@ -557,7 +517,6 @@
"LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
"LabelTasks": "Processi in esecuzione",
"LabelTextEditorBulletedList": "Elenco puntato",
- "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Elenco Numerato",
"LabelTextEditorUnlink": "Scollega",
"LabelTheme": "Tema",
@@ -609,7 +568,6 @@
"LabelViewChapters": "Visualizza i Capitoli",
"LabelViewPlayerSettings": "Mostra Impostazioni player",
"LabelViewQueue": "Visualizza coda",
- "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
"LabelXBooks": "{0} libri",
"LabelXItems": "{0} oggetti",
@@ -721,7 +679,6 @@
"MessageNoSeries": "Nessuna Serie",
"MessageNoTags": "Nessun Tags",
"MessageNoTasksRunning": "Nessun processo in esecuzione",
- "MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
"MessageNoUserPlaylists": "non hai nessuna Playlist",
"MessageNotYetImplemented": "Non Ancora Implementato",
@@ -770,9 +727,26 @@
"PlaceholderNewPlaylist": "Nome nuova playlist",
"PlaceholderSearch": "Cerca..",
"PlaceholderSearchEpisode": "Cerca Episodio..",
+ "StatsAuthorsAdded": "autori aggiunti",
+ "StatsBooksAdded": "Libri aggiunti",
+ "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",
+ "StatsTopAuthor": "MIGLIOR AUTORE",
+ "StatsTopAuthors": "MIGLIORI AUTORI",
+ "StatsTopGenre": "MIGLIOR GENERE",
+ "StatsTopGenres": "MIGLIORI GENERI",
+ "StatsTopMonth": "MIGLIOR MESE",
+ "StatsTopNarrator": "MIGLIOR NARRATORE",
+ "StatsTopNarrators": "MIGLIORI NARRATORI",
+ "StatsTotalDuration": "Con una durata totale di…",
+ "StatsYearInReview": "ANNO IN RASSEGNA",
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
"ToastAccountUpdateSuccess": "Account Aggiornato",
- "ToastAuthorImageRemoveFailed": "Rimozione immagine autore Fallita",
"ToastAuthorImageRemoveSuccess": "Immagine Autore Rimossa",
"ToastAuthorUpdateFailed": "Aggiornamento Autore Fallito",
"ToastAuthorUpdateMerged": "Autore unito",
@@ -789,7 +763,6 @@
"ToastBatchUpdateSuccess": "Batch di aggiornamento finito",
"ToastBookmarkCreateFailed": "Creazione segnalibro fallita",
"ToastBookmarkCreateSuccess": "Segnalibro creato",
- "ToastBookmarkRemoveFailed": "Rimozione segnalibro fallita",
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
"ToastBookmarkUpdateFailed": "Aggiornamento segnalibro fallito",
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
@@ -797,20 +770,18 @@
"ToastCachePurgeSuccess": "Cache eliminata correttamente",
"ToastChaptersHaveErrors": "I capitoli contengono errori",
"ToastChaptersMustHaveTitles": "I capitoli devono avere titoli",
- "ToastCollectionItemsRemoveFailed": "Rimozione oggetti dalla Raccolta fallita",
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla Raccolta",
- "ToastCollectionRemoveFailed": "Rimozione Raccolta fallita",
"ToastCollectionRemoveSuccess": "Collezione rimossa",
"ToastCollectionUpdateFailed": "Errore aggiornamento Raccolta",
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
"ToastDeleteFileFailed": "Impossibile eliminare il file",
"ToastDeleteFileSuccess": "File eliminato",
+ "ToastErrorCannotShare": "Impossibile condividere in modo nativo su questo dispositivo",
"ToastFailedToLoadData": "Impossibile caricare i dati",
"ToastItemCoverUpdateFailed": "Errore Aggiornamento cover",
"ToastItemCoverUpdateSuccess": "Cover aggiornata",
"ToastItemDetailsUpdateFailed": "Errore Aggiornamento dettagli file",
"ToastItemDetailsUpdateSuccess": "Dettagli file Aggiornata",
- "ToastItemDetailsUpdateUnneeded": "Nessun Aggiornamento necessario per il file",
"ToastItemMarkedAsFinishedFailed": "Errore nel segnare il file come finito",
"ToastItemMarkedAsFinishedSuccess": "File segnato come finito",
"ToastItemMarkedAsNotFinishedFailed": "Errore nel segnare il file come non completo",
@@ -825,7 +796,6 @@
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
"ToastPlaylistCreateFailed": "Errore creazione playlist",
"ToastPlaylistCreateSuccess": "Playlist creata",
- "ToastPlaylistRemoveFailed": "Rimozione Playlist Fallita",
"ToastPlaylistRemoveSuccess": "Playlist rimossa",
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
diff --git a/client/strings/lt.json b/client/strings/lt.json
index 2e064aff1a..f9b765d459 100644
--- a/client/strings/lt.json
+++ b/client/strings/lt.json
@@ -33,8 +33,6 @@
"ButtonHide": "Slėpti",
"ButtonHome": "Pradžia",
"ButtonIssues": "Problemos",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Naujausias",
"ButtonLibrary": "Biblioteka",
"ButtonLogout": "Atsijungti",
@@ -44,17 +42,12 @@
"ButtonMatchAllAuthors": "Pritaikyti visus autorius",
"ButtonMatchBooks": "Pritaikyti knygas",
"ButtonNevermind": "Nesvarbu",
- "ButtonNext": "Next",
"ButtonNextChapter": "Kitas Skyrius",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Atidaryti srautą",
"ButtonOpenManager": "Atidaryti tvarkyklę",
- "ButtonPause": "Pause",
"ButtonPlay": "Groti",
"ButtonPlaying": "Grojama",
"ButtonPlaylists": "Grojaraščiai",
- "ButtonPrevious": "Previous",
- "ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Valyti visą saugyklą",
"ButtonPurgeItemsCache": "Valyti elementų saugyklą",
"ButtonQueueAddItem": "Pridėti į eilę",
@@ -62,9 +55,6 @@
"ButtonQuickMatch": "Greitas pritaikymas",
"ButtonReScan": "Iš naujo nuskaityti",
"ButtonRead": "Skaityti",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
"ButtonRemove": "Pašalinti",
"ButtonRemoveAll": "Pašalinti viską",
"ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus",
@@ -72,7 +62,6 @@
"ButtonRemoveFromContinueReading": "Pašalinti iš Tęsti Skaitymą",
"ButtonRemoveSeriesFromContinueSeries": "Pašalinti seriją iš Tęsti Seriją",
"ButtonReset": "Atstatyti",
- "ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Atkurti",
"ButtonSave": "Išsaugoti",
"ButtonSaveAndClose": "Išsaugoti ir uždaryti",
@@ -83,7 +72,6 @@
"ButtonSelectFolderPath": "Pasirinkti aplanko kelią",
"ButtonSeries": "Serijos",
"ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių",
- "ButtonShare": "Share",
"ButtonShiftTimes": "Perstumti laikus",
"ButtonShow": "Rodyti",
"ButtonStartM4BEncode": "Pradėti M4B kodavimą",
@@ -98,15 +86,11 @@
"ButtonUserEdit": "Redaguoti naudotoją {0}",
"ButtonViewAll": "Peržiūrėti visus",
"ButtonYes": "Taip",
- "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Paskyra",
"HeaderAdvanced": "Papildomi",
"HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai",
"HeaderAudioTracks": "Garso takeliai",
"HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai",
- "HeaderAuthentication": "Authentication",
"HeaderBackups": "Atsarginės kopijos",
"HeaderChangePassword": "Pakeisti slaptažodį",
"HeaderChapters": "Skyriai",
@@ -115,8 +99,6 @@
"HeaderCollectionItems": "Kolekcijos elementai",
"HeaderCover": "Viršelis",
"HeaderCurrentDownloads": "Dabartiniai parsisiuntimai",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detalės",
"HeaderDownloadQueue": "Parsisiuntimo eilė",
"HeaderEbookFiles": "Eknygos failai",
@@ -143,15 +125,12 @@
"HeaderManageTags": "Tvarkyti žymas",
"HeaderMapDetails": "Susieti detales",
"HeaderMatch": "Atitaikyti",
- "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metaduomenys įterpimui",
"HeaderNewAccount": "Nauja paskyra",
"HeaderNewLibrary": "Nauja biblioteka",
"HeaderNotifications": "Pranešimai",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Atidaryti RSS srautą",
"HeaderOtherFiles": "Kiti failai",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Leidimai",
"HeaderPlayerQueue": "Grotuvo eilė",
"HeaderPlaylist": "Grojaraštis",
@@ -160,7 +139,6 @@
"HeaderPreviewCover": "Peržiūrėti viršelį",
"HeaderRSSFeedGeneral": "RSS informacija",
"HeaderRSSFeedIsOpen": "RSS srautas yra atidarytas",
- "HeaderRSSFeeds": "RSS Feeds",
"HeaderRemoveEpisode": "Pašalinti epizodą",
"HeaderRemoveEpisodes": "Pašalinti {0} epizodus",
"HeaderSavedMediaProgress": "Išsaugota medijos pažanga",
@@ -187,12 +165,8 @@
"HeaderUpdateDetails": "Atnaujinti informaciją",
"HeaderUpdateLibrary": "Atnaujinti biblioteką",
"HeaderUsers": "Naudotojai",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Jūsų statistika",
"LabelAbridged": "Santrauka",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Paskyros tipas",
"LabelAccountTypeAdmin": "Administratorius",
"LabelAccountTypeGuest": "Svečias",
@@ -202,13 +176,9 @@
"LabelAddToCollectionBatch": "Pridėti {0} knygas į kolekciją",
"LabelAddToPlaylist": "Pridėti į grojaraštį",
"LabelAddToPlaylistBatch": "Pridėti {0} elementus į grojaraštį",
- "LabelAdded": "Pridėta",
"LabelAddedAt": "Pridėta {0}",
- "LabelAdminUsersOnly": "Admin users only",
"LabelAll": "Visi",
"LabelAllUsers": "Visi naudotojai",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Jau yra jūsų bibliotekoje",
"LabelAppend": "Pridėti",
"LabelAuthor": "Autorius",
@@ -216,14 +186,7 @@
"LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)",
"LabelAuthors": "Autoriai",
"LabelAutoDownloadEpisodes": "Automatiškai atsisiųsti epizodus",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Grįžti į naudotoją",
- "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą",
"LabelBackupsEnableAutomaticBackupsHelp": "Atsarginės kopijos bus išsaugotos /metadata/backups aplanke",
"LabelBackupsMaxBackupSize": "Maksimalus atsarginių kopijų dydis (GB)",
@@ -232,18 +195,14 @@
"LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.",
"LabelBitrate": "Bitų sparta",
"LabelBooks": "Knygos",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Pakeisti slaptažodį",
"LabelChannels": "Kanalai",
"LabelChapterTitle": "Skyriaus pavadinimas",
"LabelChapters": "Skyriai",
"LabelChaptersFound": "rasti skyriai",
- "LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Uždaryti grotuvą",
"LabelCodec": "Kodekas",
"LabelCollapseSeries": "Suskleisti seriją",
- "LabelCollection": "Collection",
"LabelCollections": "Kolekcijos",
"LabelComplete": "Baigta",
"LabelConfirmPassword": "Patvirtinkite slaptažodį",
@@ -258,30 +217,22 @@
"LabelCurrently": "Šiuo metu:",
"LabelCustomCronExpression": "Nestandartinė Cron išraiška:",
"LabelDatetime": "Data ir laikas",
- "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Aprašymas",
"LabelDeselectAll": "Išvalyti pasirinktus",
"LabelDevice": "Įrenginys",
"LabelDeviceInfo": "Įrenginio informacija",
- "LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Katalogas",
"LabelDiscFromFilename": "Diskas pagal failo pavadinimą",
"LabelDiscFromMetadata": "Diskas pagal metaduomenis",
- "LabelDiscover": "Discover",
"LabelDownload": "Atsisiųsti",
"LabelDownloadNEpisodes": "Atsisiųsti {0} epizodų",
"LabelDuration": "Trukmė",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Rasta trukmė:",
"LabelEbook": "Elektroninė knyga",
"LabelEbooks": "Elektroninės knygos",
"LabelEdit": "Redaguoti",
"LabelEmail": "El. paštas",
"LabelEmailSettingsFromAddress": "Siuntėjo adresas",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "Apsaugota",
"LabelEmailSettingsSecureHelp": "Jei ši reikšmė yra \"true\", ryšys naudos TLS protokolą. Jei \"false\", TLS bus naudojamas tik tada, jei serveris palaiko STARTTLS plėtinį. Daugumos atveju, jei jungiamasi prie 465 prievado, šią reikšmę turėtumėte nustatyti kaip \"true\". Jei jungiamasi prie 587 arba 25 prievado, turi būti nustatyta \"false\". (iš nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testinis adresas",
@@ -293,10 +244,7 @@
"LabelEpisodeType": "Epizodo tipas",
"LabelExample": "Pavyzdys",
"LabelExplicit": "Suaugusiems",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "Srauto URL",
- "LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Failas",
"LabelFileBirthtime": "Failo kūrimo laikas",
"LabelFileModified": "Failo keitimo laikas",
@@ -306,23 +254,17 @@
"LabelFinished": "Baigta",
"LabelFolder": "Aplankas",
"LabelFolders": "Aplankai",
- "LabelFontBold": "Bold",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Famiglia di font",
- "LabelFontItalic": "Italic",
"LabelFontScale": "Šrifto mastelis",
- "LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Formatas",
"LabelGenre": "Žanras",
"LabelGenres": "Žanrai",
"LabelHardDeleteFile": "Galutinai ištrinti failą",
"LabelHasEbook": "Turi e-knygą",
"LabelHasSupplementaryEbook": "Turi papildomą e-knygą",
- "LabelHighestPriority": "Highest priority",
"LabelHost": "Serveris",
"LabelHour": "Valanda",
"LabelIcon": "Piktograma",
- "LabelImageURLFromTheWeb": "Image URL from the web",
"LabelInProgress": "Vyksta",
"LabelIncludeInTracklist": "Įtraukti į takelių sąrašą",
"LabelIncomplete": "Nebaigta",
@@ -339,7 +281,6 @@
"LabelItem": "Elementas",
"LabelLanguage": "Kalba",
"LabelLanguageDefaultServer": "Numatytoji serverio kalba",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "Paskutinė pridėta knyga",
"LabelLastBookUpdated": "Paskutinė atnaujinta knyga",
"LabelLastSeen": "Paskutinį kartą matyta",
@@ -351,31 +292,19 @@
"LabelLess": "Mažiau",
"LabelLibrariesAccessibleToUser": "Naudotojui pasiekiamos bibliotekos",
"LabelLibrary": "Biblioteka",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Bibliotekos elementas",
"LabelLibraryName": "Bibliotekos pavadinimas",
"LabelLimit": "Limitas",
"LabelLineSpacing": "Tarpas tarp eilučių",
"LabelListenAgain": "Klausytis iš naujo",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
- "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Grotuvas",
"LabelMediaType": "Medijos tipas",
"LabelMetaTag": "Meta žymė",
"LabelMetaTags": "Meta žymos",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metaduomenų tiekėjas",
"LabelMinute": "Minutė",
"LabelMissing": "Trūksta",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
"LabelMore": "Daugiau",
"LabelMoreInfo": "Daugiau informacijos",
"LabelName": "Pavadinimas",
@@ -387,7 +316,6 @@
"LabelNewestEpisodes": "Naujausi epizodai",
"LabelNextBackupDate": "Kitos atsarginės kopijos data",
"LabelNextScheduledRun": "Kito planuoto vykdymo data",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Nepasirinkti jokie epizodai",
"LabelNotFinished": "Nebaigta",
"LabelNotStarted": "Nepasileista",
@@ -403,9 +331,6 @@
"LabelNotificationsMaxQueueSizeHelp": "Įvykiai yra apriboti vienu įvykiu per sekundę. Įvykiai bus ignoruojami, jei eilė yra maksimalaus dydžio. Tai apsaugo nuo pranešimų šlamšto.",
"LabelNumberOfBooks": "Knygų skaičius",
"LabelNumberOfEpisodes": "Epizodų skaičius",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Atidaryti RSS srautą",
"LabelOverwrite": "Perrašyti",
"LabelPassword": "Slaptažodis",
@@ -417,10 +342,8 @@
"LabelPermissionsDownload": "Gali atsisiųsti",
"LabelPermissionsUpdate": "Gali atnaujinti",
"LabelPermissionsUpload": "Gali įkelti",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Nuotraukos kelias/URL",
"LabelPlayMethod": "Grojimo metodas",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Grojaraščiai",
"LabelPodcast": "Tinklalaidė",
"LabelPodcastSearchRegion": "Podcast paieškos regionas",
@@ -435,7 +358,6 @@
"LabelPubDate": "Publikavimo data",
"LabelPublishYear": "Leidimo metai",
"LabelPublisher": "Leidėjas",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
"LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas",
"LabelRSSFeedOpen": "Atidarytas RSS srautas",
@@ -448,25 +370,20 @@
"LabelRecentSeries": "Naujausios serijos",
"LabelRecentlyAdded": "Neseniai pridėta",
"LabelRecommended": "Rekomenduojama",
- "LabelRedo": "Redo",
"LabelRegion": "Regionas",
"LabelReleaseDate": "Išleidimo data",
"LabelRemoveCover": "Pašalinti viršelį",
- "LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Paieškos žodis",
"LabelSearchTitle": "Ieškoti pavadinimo",
"LabelSearchTitleOrASIN": "Ieškoti pavadinimo arba ASIN",
"LabelSeason": "Sezonas",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Pažymėti visus epizodus",
"LabelSelectEpisodesShowing": "Pažymėti {0} rodomus epizodus",
- "LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Siųsti e-knygą į...",
"LabelSequence": "Seka",
"LabelSeries": "Serija",
"LabelSeriesName": "Serijos pavadinimas",
"LabelSeriesProgress": "Serijos progresas",
- "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Nustatyti kaip pagrindinę",
"LabelSetEbookAsSupplementary": "Nustatyti kaip papildomą",
"LabelSettingsAudiobooksOnly": "Tik garso knygos",
@@ -477,11 +394,6 @@
"LabelSettingsDisableWatcher": "Išjungti stebėtoją",
"LabelSettingsDisableWatcherForLibrary": "Išjungti aplankų stebėtoją bibliotekai",
"LabelSettingsDisableWatcherHelp": "Išjungia automatinį elementų pridėjimą/atnaujinimą, jei pastebėti failų pokyčiai. *Reikalingas serverio paleidimas iš naujo",
- "LabelSettingsEnableWatcher": "Enable Watcher",
- "LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
- "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai",
"LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.",
"LabelSettingsFindCovers": "Rasti viršelius",
@@ -490,8 +402,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.",
"LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą",
"LabelSettingsLibraryBookshelfView": "Naudoti bibliotekos knygų lentynų vaizdą",
- "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": "Analizuoti subtitrus",
"LabelSettingsParseSubtitlesHelp": "Išskleisti subtitrus iš audioknygos aplanko pavadinimų.
Subtitrai turi būti atskirti brūkšniu \"-\"
pavyzdžiui, \"Knygos pavadinimas - Čia yra subtitrai\" turi subtitrą \"Čia yra subtitrai\"",
"LabelSettingsPreferMatchedMetadata": "Pirmenybė atitaikytiems metaduomenis",
@@ -508,10 +418,8 @@
"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",
"LabelShowAll": "Rodyti viską",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Dydis",
"LabelSleepTimer": "Miego laikmatis",
- "LabelSlug": "Slug",
"LabelStart": "Pradėti",
"LabelStartTime": "Pradžios laikas",
"LabelStarted": "Pradėta",
@@ -538,10 +446,6 @@
"LabelTagsAccessibleToUser": "Žymos, pasiekiamos vartotojui",
"LabelTagsNotAccessibleToUser": "Žymos, nepasiekiamos vartotojui",
"LabelTasks": "Vykdomos užduotys",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Tema",
"LabelThemeDark": "Tamsi",
"LabelThemeLight": "Šviesi",
@@ -563,11 +467,9 @@
"LabelTrackFromMetadata": "Takelis iš metaduomenų",
"LabelTracks": "Takeliai",
"LabelTracksMultiTrack": "Keli takeliai",
- "LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Vienas takelis",
"LabelType": "Tipas",
"LabelUnabridged": "Neprikurptas",
- "LabelUndo": "Undo",
"LabelUnknown": "Nežinoma",
"LabelUpdateCover": "Atnaujinti viršelį",
"LabelUpdateCoverHelp": "Leisti perrašyti esamus viršelius pasirinktoms knygoms, kai yra rasta atitikmenų",
@@ -576,7 +478,6 @@
"LabelUpdatedAt": "Atnaujinta",
"LabelUploaderDragAndDrop": "Tempkite ir paleiskite failus ar aplankus",
"LabelUploaderDropFiles": "Nutempti failus",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Naudoti skyrių takelį",
"LabelUseFullTrack": "Naudoti visą takelį",
"LabelUser": "Vartotojas",
@@ -588,8 +489,6 @@
"LabelViewQueue": "Peržiūrėti grotuvo eilę",
"LabelVolume": "Garsumas",
"LabelWeekdaysToRun": "Dienos, kuriomis vykdyti",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Jūsų garso knygos trukmė",
"LabelYourBookmarks": "Jūsų skirtukai",
"LabelYourPlaylists": "Jūsų grojaraščiai",
@@ -601,7 +500,6 @@
"MessageBookshelfNoCollections": "Dar nepridėjote jokių kolekcijų",
"MessageBookshelfNoRSSFeeds": "Nėra atvertų RSS srautų",
"MessageBookshelfNoResultsForFilter": "Rezultatų pagal filtrą \"{0}: {1}\" nėra",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Neturite jokių serijų",
"MessageChapterEndIsAfter": "Skyriaus pabaiga yra po jūsų garso knygos pabaigos",
"MessageChapterErrorFirstNotZero": "Pirmasis skyrius turi prasidėti nuo 0",
@@ -609,28 +507,19 @@
"MessageChapterErrorStartLtPrev": "Netinkamas pradžios laikas. Turi būti didesnis arba lygus ankstesnio skyriaus pradžios laikui",
"MessageChapterStartIsAfter": "Skyriaus pradžia yra po jūsų garso knygos pabaigos",
"MessageCheckingCron": "Tikrinamas cron...",
- "MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
"MessageConfirmDeleteBackup": "Ar tikrai norite ištrinti atsarginę kopiją, skirtą {0}?",
"MessageConfirmDeleteFile": "Tai ištrins failą iš jūsų failų sistemos. Ar tikrai?",
"MessageConfirmDeleteLibrary": "Ar tikrai norite visam laikui ištrinti biblioteką \"{0}\"?",
- "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Ar tikrai norite ištrinti šią sesiją?",
"MessageConfirmForceReScan": "Ar tikrai norite priversti perskenavimą?",
"MessageConfirmMarkAllEpisodesFinished": "Ar tikrai norite pažymėti visus epizodus kaip užbaigtus?",
"MessageConfirmMarkAllEpisodesNotFinished": "Ar tikrai norite pažymėti visus epizodus kaip nebaigtus?",
"MessageConfirmMarkSeriesFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip užbaigtas?",
"MessageConfirmMarkSeriesNotFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip nebaigtas?",
- "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?",
- "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmRemoveAllChapters": "Ar tikrai norite pašalinti visus skyrius?",
- "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Ar tikrai norite pašalinti kolekciją \"{0}\"?",
"MessageConfirmRemoveEpisode": "Ar tikrai norite pašalinti epizodą \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Ar tikrai norite pašalinti {0} epizodus?",
- "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Ar tikrai norite pašalinti skaitytoją \"{0}\"?",
"MessageConfirmRemovePlaylist": "Ar tikrai norite pašalinti savo grojaraštį \"{0}\"?",
"MessageConfirmRenameGenre": "Ar tikrai norite pervadinti žanrą \"{0}\" į \"{1}\" visiems elementams?",
@@ -644,7 +533,6 @@
"MessageDragFilesIntoTrackOrder": "Surikiuokite takelius vilkdami failus",
"MessageEmbedFinished": "Įterpimas baigtas!",
"MessageEpisodesQueuedForDownload": "{0} epizodai laukia atsisiuntimo",
- "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": "Srauto URL bus {0}",
"MessageFetching": "Surenkama...",
"MessageForceReScanDescription": "skenuos visus failus lyg iš naujo. Garsinių failų ID3 žymos, OPF failai ir tekstiniai failai bus nuskenuoti kaip nauji.",
@@ -656,7 +544,6 @@
"MessageListeningSessionsInTheLastYear": "{0} klausymo sesijų per paskutinius metus",
"MessageLoading": "Kraunama...",
"MessageLoadingFolders": "Kraunami aplankai...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B Nepavyko!",
"MessageM4BFinished": "M4B Baigta!",
"MessageMapChapterTitles": "Susieti skyriaus pavadinimus su jūsų esamais garso knygos skyriais, neredaguojant laiko žymų",
@@ -692,7 +579,6 @@
"MessageNoSeries": "Serijų nėra",
"MessageNoTags": "Žymų nėra",
"MessageNoTasksRunning": "Nėra vykstančių užduočių",
- "MessageNoUpdateNecessary": "Atnaujinimai nereikalingi",
"MessageNoUpdatesWereNecessary": "Nereikalingi jokie atnaujinimai",
"MessageNoUserPlaylists": "Neturite grojaraščių",
"MessageNotYetImplemented": "Dar neįgyvendinta",
@@ -711,7 +597,6 @@
"MessageRestoreBackupConfirm": "Ar tikrai norite atkurti atsarginę kopiją, sukurtą",
"MessageRestoreBackupWarning": "Atkurdami atsarginę kopiją perrašysite visą duomenų bazę, esančią /config ir viršelių vaizdus /metadata/items ir /metadata/authors.
Atsarginės kopijos nekeičia jokių failų jūsų bibliotekos aplankuose. Jei esate įgalinę serverio nustatymus, kad viršelio meną ir metaduomenis saugotumėte savo bibliotekos aplankuose, šie neperrašomi ar atkuriami.
Visi klientai, naudojantys jūsų serverį, bus automatiškai atnaujinti.",
"MessageSearchResultsFor": "Paieškos rezultatai „{0}“",
- "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Nepavyko pasiekti serverio",
"MessageSetChaptersFromTracksDescription": "Nustatyti skyrius, naudojant kiekvieną garso failą kaip skyrių ir skyriaus pavadinimą kaip garso failo pavadinimą",
"MessageStartPlaybackAtTime": "Paleisti klausymą „{0}“ nuo {1}?",
@@ -739,7 +624,6 @@
"PlaceholderSearchEpisode": "Ieškoti epizodo..",
"ToastAccountUpdateFailed": "Paskyros atnaujinimas nepavyko",
"ToastAccountUpdateSuccess": "Paskyra atnaujinta",
- "ToastAuthorImageRemoveFailed": "Nepavyko pašalinti autoriaus paveiksliuko",
"ToastAuthorImageRemoveSuccess": "Autoriaus paveiksliukas pašalintas",
"ToastAuthorUpdateFailed": "Nepavyko atnaujinti autoriaus",
"ToastAuthorUpdateMerged": "Autorius sujungtas",
@@ -756,28 +640,19 @@
"ToastBatchUpdateSuccess": "Masinis atnaujinimas sėkmingas",
"ToastBookmarkCreateFailed": "Žymos sukurti nepavyko",
"ToastBookmarkCreateSuccess": "Žyma pridėta",
- "ToastBookmarkRemoveFailed": "Žymos pašalinti nepavyko",
"ToastBookmarkRemoveSuccess": "Žyma pašalinta",
"ToastBookmarkUpdateFailed": "Žymos atnaujinti nepavyko",
"ToastBookmarkUpdateSuccess": "Žyma atnaujinta",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Skyriai turi klaidų",
"ToastChaptersMustHaveTitles": "Skyriai turi turėti pavadinimus",
- "ToastCollectionItemsRemoveFailed": "Elementų pašalinti iš kolekcijos nepavyko",
"ToastCollectionItemsRemoveSuccess": "Elementai pašalinti iš kolekcijos",
- "ToastCollectionRemoveFailed": "Kolekcijos pašalinti nepavyko",
"ToastCollectionRemoveSuccess": "Kolekcija pašalinta",
"ToastCollectionUpdateFailed": "Kolekcijos atnaujinti nepavyko",
"ToastCollectionUpdateSuccess": "Kolekcija atnaujinta",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Elemento viršelio atnaujinti nepavyko",
"ToastItemCoverUpdateSuccess": "Elemento viršelis atnaujintas",
"ToastItemDetailsUpdateFailed": "Elemento detalių atnaujinti nepavyko",
"ToastItemDetailsUpdateSuccess": "Elemento detalės atnaujintos",
- "ToastItemDetailsUpdateUnneeded": "Elemento detalės atnaujinimas nereikalingas",
"ToastItemMarkedAsFinishedFailed": "Pažymėti kaip Baigta nepavyko",
"ToastItemMarkedAsFinishedSuccess": "Elementas pažymėtas kaip Baigta",
"ToastItemMarkedAsNotFinishedFailed": "Pažymėti kaip Nebaigta nepavyko",
@@ -792,7 +667,6 @@
"ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" atnaujinta",
"ToastPlaylistCreateFailed": "Grojaraščio sukurti nepavyko",
"ToastPlaylistCreateSuccess": "Grojaraštis sukurtas",
- "ToastPlaylistRemoveFailed": "Grojaraščio pašalinti nepavyko",
"ToastPlaylistRemoveSuccess": "Grojaraštis pašalintas",
"ToastPlaylistUpdateFailed": "Grojaraščio atnaujinti nepavyko",
"ToastPlaylistUpdateSuccess": "Grojaraštis atnaujintas",
@@ -806,16 +680,11 @@
"ToastSendEbookToDeviceSuccess": "E-knyga išsiųsta į įrenginį \"{0}\"",
"ToastSeriesUpdateFailed": "Serijos atnaujinti nepavyko",
"ToastSeriesUpdateSuccess": "Serijos atnaujintos",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Sesijos ištrinti nepavyko",
"ToastSessionDeleteSuccess": "Sesija ištrinta",
"ToastSocketConnected": "Serveris prijungtas",
"ToastSocketDisconnected": "Severis atjungtas",
"ToastSocketFailedToConnect": "Nepavyko prisijungti prie serverio",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Nepavyko ištrinti naudotojo",
"ToastUserDeleteSuccess": "Naudotojas ištrintas"
}
diff --git a/client/strings/nl.json b/client/strings/nl.json
index e209c3a508..41fd8ef687 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -31,7 +31,6 @@
"ButtonForceReScan": "Forceer nieuwe scan",
"ButtonFullPath": "Volledig pad",
"ButtonHide": "Verberg",
- "ButtonHome": "Home",
"ButtonIssues": "Problemen",
"ButtonJumpBackward": "Spring achteruit",
"ButtonJumpForward": "Spring vooruit",
@@ -46,7 +45,6 @@
"ButtonNevermind": "Laat maar",
"ButtonNext": "Volgende",
"ButtonNextChapter": "Volgend hoofdstuk",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Feed openen",
"ButtonOpenManager": "Manager openen",
"ButtonPause": "Pauze",
@@ -71,17 +69,13 @@
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
"ButtonRemoveFromContinueReading": "Verwijder van Verder luisteren",
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
- "ButtonReset": "Reset",
- "ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Herstel",
"ButtonSave": "Opslaan",
"ButtonSaveAndClose": "Opslaan & sluiten",
"ButtonSaveTracklist": "Afspeellijst opslaan",
- "ButtonScan": "Scan",
"ButtonScanLibrary": "Scan bibliotheek",
"ButtonSearch": "Zoeken",
"ButtonSelectFolderPath": "Maplocatie selecteren",
- "ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
"ButtonShare": "Deel",
"ButtonShiftTimes": "Tijden verschuiven",
@@ -89,8 +83,6 @@
"ButtonStartM4BEncode": "Start M4B-encoding",
"ButtonStartMetadataEmbed": "Start insluiten metadata",
"ButtonSubmit": "Indienen",
- "ButtonTest": "Test",
- "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload back-up",
"ButtonUploadCover": "Upload cover",
"ButtonUploadOPMLFile": "Upload OPML-bestand",
@@ -101,12 +93,10 @@
"ErrorUploadFetchMetadataAPI": "Error metadata ophalen",
"ErrorUploadFetchMetadataNoResults": "Kan metadata niet ophalen - probeer de titel en/of auteur te updaten",
"ErrorUploadLacksTitle": "Moet een titel hebben",
- "HeaderAccount": "Account",
"HeaderAdvanced": "Geavanceerd",
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
"HeaderAudioTracks": "Audiotracks",
"HeaderAudiobookTools": "Audioboekbestandbeheer tools",
- "HeaderAuthentication": "Authentication",
"HeaderBackups": "Back-ups",
"HeaderChangePassword": "Wachtwoord wijzigen",
"HeaderChapters": "Hoofdstukken",
@@ -115,9 +105,6 @@
"HeaderCollectionItems": "Collectie-objecten",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Huidige downloads",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
- "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download-wachtrij",
"HeaderEbookFiles": "Ebook bestanden",
"HeaderEmail": "E-mail",
@@ -137,21 +124,14 @@
"HeaderLibraryStats": "Bibliotheekstatistieken",
"HeaderListeningSessions": "Luistersessies",
"HeaderListeningStats": "Luisterstatistieken",
- "HeaderLogin": "Login",
- "HeaderLogs": "Logs",
"HeaderManageGenres": "Genres beheren",
"HeaderManageTags": "Tags beheren",
- "HeaderMapDetails": "Map details",
- "HeaderMatch": "Match",
- "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "In te sluiten metadata",
"HeaderNewAccount": "Nieuwe account",
"HeaderNewLibrary": "Nieuwe bibliotheek",
"HeaderNotifications": "Notificaties",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Open RSS-feed",
"HeaderOtherFiles": "Andere bestanden",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Toestemmingen",
"HeaderPlayerQueue": "Afspeelwachtrij",
"HeaderPlaylist": "Afspeellijst",
@@ -172,7 +152,6 @@
"HeaderSettingsDisplay": "Toon",
"HeaderSettingsExperimental": "Experimentele functies",
"HeaderSettingsGeneral": "Algemeen",
- "HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Slaaptimer",
"HeaderStatsLargestItems": "Grootste items",
"HeaderStatsLongestItems": "Langste items (uren)",
@@ -181,18 +160,13 @@
"HeaderStatsTop10Authors": "Top 10 auteurs",
"HeaderStatsTop5Genres": "Top 5 genres",
"HeaderTableOfContents": "Inhoudsopgave",
- "HeaderTools": "Tools",
"HeaderUpdateAccount": "Account bijwerken",
"HeaderUpdateAuthor": "Auteur bijwerken",
"HeaderUpdateDetails": "Details bijwerken",
"HeaderUpdateLibrary": "Bibliotheek bijwerken",
"HeaderUsers": "Gebruikers",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Je statistieken",
"LabelAbridged": "Verkort",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Accounttype",
"LabelAccountTypeAdmin": "Beheerder",
"LabelAccountTypeGuest": "Gast",
@@ -202,13 +176,9 @@
"LabelAddToCollectionBatch": "{0} boeken toevoegen aan collectie",
"LabelAddToPlaylist": "Toevoegen aan afspeellijst",
"LabelAddToPlaylistBatch": "{0} onderdelen toevoegen aan afspeellijst",
- "LabelAdded": "Toegevoegd",
"LabelAddedAt": "Toegevoegd op",
- "LabelAdminUsersOnly": "Admin users only",
"LabelAll": "Alle",
"LabelAllUsers": "Alle gebruikers",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Reeds in je bibliotheek",
"LabelAppend": "Achteraan toevoegen",
"LabelAuthor": "Auteur",
@@ -216,12 +186,6 @@
"LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)",
"LabelAuthors": "Auteurs",
"LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Terug naar gebruiker",
"LabelBackupLocation": "Back-up locatie",
"LabelBackupsEnableAutomaticBackups": "Automatische back-ups inschakelen",
@@ -230,10 +194,7 @@
"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",
"LabelBooks": "Boeken",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Wachtwoord wijzigen",
"LabelChannels": "Kanalen",
"LabelChapterTitle": "Hoofdstuktitel",
@@ -241,7 +202,6 @@
"LabelChaptersFound": "Hoofdstukken gevonden",
"LabelClickForMoreInfo": "Klik voor meer informatie",
"LabelClosePlayer": "Sluit speler",
- "LabelCodec": "Codec",
"LabelCollapseSeries": "Series inklappen",
"LabelCollection": "Collectie",
"LabelCollections": "Collecties",
@@ -250,7 +210,6 @@
"LabelContinueListening": "Verder luisteren",
"LabelContinueReading": "Verder luisteren",
"LabelContinueSeries": "Ga verder met serie",
- "LabelCover": "Cover",
"LabelCoverImageURL": "Coverafbeelding URL",
"LabelCreatedAt": "Gecreëerd op",
"LabelCronExpression": "Cron-uitdrukking",
@@ -259,30 +218,18 @@
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
"LabelDatetime": "Datum-tijd",
"LabelDays": "Dagen",
- "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Beschrijving",
"LabelDeselectAll": "Deselecteer alle",
"LabelDevice": "Apparaat",
"LabelDeviceInfo": "Apparaat info",
- "LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Map",
"LabelDiscFromFilename": "Schijf uit bestandsnaam",
"LabelDiscFromMetadata": "Schijf uit metadata",
"LabelDiscover": "Ontdek",
- "LabelDownload": "Download",
- "LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duur",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Gevonden duur:",
- "LabelEbook": "Ebook",
- "LabelEbooks": "Ebooks",
"LabelEdit": "Wijzig",
- "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Van-adres",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"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",
@@ -294,9 +241,6 @@
"LabelEpisodeType": "Afleveringtype",
"LabelExample": "Voorbeeld",
"LabelExplicit": "Expliciet",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
- "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Metadata ophalen",
"LabelFile": "Bestand",
"LabelFileBirthtime": "Aanmaaktijd bestand",
@@ -308,27 +252,18 @@
"LabelFolder": "Map",
"LabelFolders": "Mappen",
"LabelFontBold": "Vetgedrukt",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Lettertypefamilie",
- "LabelFontItalic": "Italic",
"LabelFontScale": "Lettertype schaal",
- "LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Formaat",
- "LabelGenre": "Genre",
- "LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard-delete bestand",
"LabelHasEbook": "Heeft ebook",
"LabelHasSupplementaryEbook": "Heeft supplementair ebook",
- "LabelHighestPriority": "Highest priority",
- "LabelHost": "Host",
"LabelHour": "Uur",
"LabelHours": "Uren",
"LabelIcon": "Icoon",
- "LabelImageURLFromTheWeb": "Image URL from the web",
"LabelInProgress": "Bezig",
"LabelIncludeInTracklist": "Includeer in tracklijst",
"LabelIncomplete": "Incompleet",
- "LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Aangepast dagelijks/wekelijks",
"LabelIntervalEvery12Hours": "Iedere 12 uur",
"LabelIntervalEvery15Minutes": "Iedere 15 minuten",
@@ -341,43 +276,30 @@
"LabelItem": "Onderdeel",
"LabelLanguage": "Taal",
"LabelLanguageDefaultServer": "Standaard servertaal",
- "LabelLanguages": "Languages",
"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": "No {0}",
"LabelLibraryItem": "Bibliotheekonderdeel",
"LabelLibraryName": "Bibliotheeknaam",
"LabelLimit": "Limiet",
"LabelLineSpacing": "Regelruimte",
"LabelListenAgain": "Luister opnieuw",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Waarschuwing",
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Mediaspeler",
"LabelMediaType": "Mediatype",
"LabelMetaTag": "Meta-tag",
"LabelMetaTags": "Meta-tags",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadatabron",
"LabelMinute": "Minuut",
"LabelMissing": "Ontbrekend",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
"LabelMore": "Meer",
"LabelMoreInfo": "Meer info",
"LabelName": "Naam",
@@ -389,12 +311,10 @@
"LabelNewestEpisodes": "Nieuwste afleveringen",
"LabelNextBackupDate": "Volgende back-up datum",
"LabelNextScheduledRun": "Volgende geplande run",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Geen afleveringen geselecteerd",
"LabelNotFinished": "Niet Voltooid",
"LabelNotStarted": "Niet Gestart",
"LabelNotes": "Notities",
- "LabelNotificationAppriseURL": "Apprise URL(s)",
"LabelNotificationAvailableVariables": "Beschikbare variabelen",
"LabelNotificationBodyTemplate": "Body-template",
"LabelNotificationEvent": "Notificatie gebeurtenis",
@@ -405,9 +325,6 @@
"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": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Overschrijf",
"LabelPassword": "Wachtwoord",
@@ -419,15 +336,11 @@
"LabelPermissionsDownload": "Kan downloaden",
"LabelPermissionsUpdate": "Kan bijwerken",
"LabelPermissionsUpload": "Kan uploaden",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto pad/URL",
"LabelPlayMethod": "Afspeelwijze",
- "LabelPlayerChapterNumberMarker": "{0} of {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",
@@ -437,7 +350,6 @@
"LabelPubDate": "Publicatiedatum",
"LabelPublishYear": "Jaar van uitgave",
"LabelPublisher": "Uitgever",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
"LabelRSSFeedOpen": "RSS-feed open",
@@ -450,31 +362,25 @@
"LabelRecentSeries": "Recente series",
"LabelRecentlyAdded": "Recent toegevoegd",
"LabelRecommended": "Aangeraden",
- "LabelRedo": "Redo",
"LabelRegion": "Regio",
"LabelReleaseDate": "Verschijningsdatum",
"LabelRemoveCover": "Verwijder cover",
- "LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Zoekterm",
"LabelSearchTitle": "Zoek titel",
"LabelSearchTitleOrASIN": "Zoek titel of ASIN",
"LabelSeason": "Seizoen",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Selecteer alle afleveringen",
"LabelSelectEpisodesShowing": "Selecteer {0} afleveringen laten zien",
- "LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Stuur ebook naar...",
"LabelSequence": "Sequentie",
"LabelSeries": "Serie",
"LabelSeriesName": "Naam serie",
"LabelSeriesProgress": "Voortgang serie",
- "LabelServerYearReview": "Server Year 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 support",
"LabelSettingsDateFormat": "Datum format",
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
@@ -482,8 +388,6 @@
"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": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Experimentele functies",
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
"LabelSettingsFindCovers": "Zoek covers",
@@ -492,8 +396,6 @@
"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": "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": "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",
@@ -510,11 +412,8 @@
"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",
"LabelShowAll": "Toon alle",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Grootte",
"LabelSleepTimer": "Slaaptimer",
- "LabelSlug": "Slug",
- "LabelStart": "Start",
"LabelStartTime": "Starttijd",
"LabelStarted": "Gestart",
"LabelStartedAt": "Gestart op",
@@ -535,15 +434,9 @@
"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": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Thema",
"LabelThemeDark": "Donker",
"LabelThemeLight": "Licht",
@@ -563,11 +456,8 @@
"LabelTotalTimeListened": "Totale tijd geluisterd",
"LabelTrackFromFilename": "Track vanuit bestandsnaam",
"LabelTrackFromMetadata": "Track vanuit metadata",
- "LabelTracks": "Tracks",
- "LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "Geen tracks",
"LabelTracksSingleTrack": "Enkele track",
- "LabelType": "Type",
"LabelUnabridged": "Onverkort",
"LabelUndo": "Ongedaan maken",
"LabelUnknown": "Onbekend",
@@ -578,7 +468,6 @@
"LabelUpdatedAt": "Bijgewerkt op",
"LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen",
"LabelUploaderDropFiles": "Bestanden neerzetten",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Gebruik hoofdstuktrack",
"LabelUseFullTrack": "Gebruik volledige track",
"LabelUser": "Gebruiker",
@@ -588,10 +477,7 @@
"LabelViewBookmarks": "Bekijk boekwijzers",
"LabelViewChapters": "Bekijk hoofdstukken",
"LabelViewQueue": "Bekijk afspeelwachtrij",
- "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdagen om te draaien",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Je audioboekduur",
"LabelYourBookmarks": "Je boekwijzers",
"LabelYourPlaylists": "Je afspeellijsten",
@@ -603,7 +489,6 @@
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Je hebt geen series",
"MessageChapterEndIsAfter": "Hoofdstukeinde is na het einde van je audioboek",
"MessageChapterErrorFirstNotZero": "Eerste hoofdstuk moet starten op 0",
@@ -611,22 +496,15 @@
"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": "Are you sure you want to close this feed?",
"MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {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": "This will delete the library item from the database and your file system. Are you sure?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
"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?",
"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?",
- "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?",
- "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"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?",
@@ -646,7 +524,6 @@
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
"MessageEmbedFinished": "Insluiting voltooid!",
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
- "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 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.",
@@ -658,7 +535,6 @@
"MessageListeningSessionsInTheLastYear": "{0} luistersessies in het laatste jaar",
"MessageLoading": "Aan het laden...",
"MessageLoadingFolders": "Mappen aan het laden...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B mislukt!",
"MessageM4BFinished": "M4B voltooid!",
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
@@ -694,7 +570,6 @@
"MessageNoSeries": "Geen series",
"MessageNoTags": "Geen tags",
"MessageNoTasksRunning": "Geen lopende taken",
- "MessageNoUpdateNecessary": "Geen bijwerking noodzakelijk",
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
"MessageNotYetImplemented": "Nog niet geimplementeerd",
@@ -713,7 +588,6 @@
"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} selected",
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
"MessageShareExpiresIn": "Vervalt in {0}",
@@ -742,7 +616,6 @@
"PlaceholderSearchEpisode": "Aflevering zoeken..",
"ToastAccountUpdateFailed": "Bijwerken account mislukt",
"ToastAccountUpdateSuccess": "Account bijgewerkt",
- "ToastAuthorImageRemoveFailed": "Afbeelding verwijderen mislukt",
"ToastAuthorImageRemoveSuccess": "Afbeelding auteur verwijderd",
"ToastAuthorUpdateFailed": "Bijwerken auteur mislukt",
"ToastAuthorUpdateMerged": "Auteur samengevoegd",
@@ -759,28 +632,19 @@
"ToastBatchUpdateSuccess": "Bulk-bijwerking gelukt",
"ToastBookmarkCreateFailed": "Aanmaken boekwijzer mislukt",
"ToastBookmarkCreateSuccess": "boekwijzer toegevoegd",
- "ToastBookmarkRemoveFailed": "Verwijderen boekwijzer mislukt",
"ToastBookmarkRemoveSuccess": "Boekwijzer verwijderd",
"ToastBookmarkUpdateFailed": "Bijwerken boekwijzer mislukt",
"ToastBookmarkUpdateSuccess": "Boekwijzer bijgewerkt",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Hoofdstukken bevatten fouten",
"ToastChaptersMustHaveTitles": "Hoofdstukken moeten titels hebben",
- "ToastCollectionItemsRemoveFailed": "Verwijderen onderdeel (of onderdelen) uit collectie mislukt",
"ToastCollectionItemsRemoveSuccess": "Onderdeel (of onderdelen) verwijderd uit collectie",
- "ToastCollectionRemoveFailed": "Verwijderen collectie mislukt",
"ToastCollectionRemoveSuccess": "Collectie verwijderd",
"ToastCollectionUpdateFailed": "Bijwerken collectie mislukt",
"ToastCollectionUpdateSuccess": "Collectie bijgewerkt",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Bijwerken cover onderdeel mislukt",
"ToastItemCoverUpdateSuccess": "Cover onderdeel bijgewerkt",
"ToastItemDetailsUpdateFailed": "Bijwerken details onderdeel mislukt",
"ToastItemDetailsUpdateSuccess": "Details onderdeel bijgewerkt",
- "ToastItemDetailsUpdateUnneeded": "Geen bijwerking nodig voor details onderdeel",
"ToastItemMarkedAsFinishedFailed": "Markeren als Voltooid mislukt",
"ToastItemMarkedAsFinishedSuccess": "Onderdeel gemarkeerd als Voltooid",
"ToastItemMarkedAsNotFinishedFailed": "Markeren als Niet Voltooid mislukt",
@@ -795,7 +659,6 @@
"ToastLibraryUpdateSuccess": "Bibliotheek \"{0}\" bijgewerkt",
"ToastPlaylistCreateFailed": "Aanmaken afspeellijst mislukt",
"ToastPlaylistCreateSuccess": "Afspeellijst aangemaakt",
- "ToastPlaylistRemoveFailed": "Verwijderen afspeellijst mislukt",
"ToastPlaylistRemoveSuccess": "Afspeellijst verwijderd",
"ToastPlaylistUpdateFailed": "Afspeellijst bijwerken mislukt",
"ToastPlaylistUpdateSuccess": "Afspeellijst bijgewerkt",
@@ -809,16 +672,11 @@
"ToastSendEbookToDeviceSuccess": "Ebook verstuurd naar apparaat \"{0}\"",
"ToastSeriesUpdateFailed": "Bijwerken serie mislukt",
"ToastSeriesUpdateSuccess": "Bijwerken serie gelukt",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Verwijderen sessie mislukt",
"ToastSessionDeleteSuccess": "Sessie verwijderd",
"ToastSocketConnected": "Socket verbonden",
"ToastSocketDisconnected": "Socket niet verbonden",
"ToastSocketFailedToConnect": "Verbinding Socket mislukt",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Verwijderen gebruiker mislukt",
"ToastUserDeleteSuccess": "Gebruiker verwijderd"
}
diff --git a/client/strings/no.json b/client/strings/no.json
index 6db2d98fb6..ea2b8f0c53 100644
--- a/client/strings/no.json
+++ b/client/strings/no.json
@@ -4,7 +4,6 @@
"ButtonAddDevice": "Legg til enhet",
"ButtonAddLibrary": "Legg til bibliotek",
"ButtonAddPodcasts": "Legg til podcast",
- "ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Legg til ditt første bibliotek",
"ButtonApply": "Bruk",
"ButtonApplyChapters": "Bruk kapittel",
@@ -44,12 +43,9 @@
"ButtonMatchAllAuthors": "Søk opp alle forfattere",
"ButtonMatchBooks": "Søk opp bøker",
"ButtonNevermind": "Avbryt",
- "ButtonNext": "Next",
"ButtonNextChapter": "Neste Kapittel",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Åpne Feed",
"ButtonOpenManager": "Åpne behandler",
- "ButtonPause": "Pause",
"ButtonPlay": "Spill av",
"ButtonPlaying": "Spiller av",
"ButtonPlaylists": "Spillelister",
@@ -73,7 +69,6 @@
"ButtonRemoveFromContinueReading": "Fjern fra Fortsett å lese",
"ButtonRemoveSeriesFromContinueSeries": "Fjern serie fra Fortsett serie",
"ButtonReset": "Nullstill",
- "ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Gjenopprett",
"ButtonSave": "Lagre",
"ButtonSaveAndClose": "Lagre og lukk",
@@ -91,7 +86,6 @@
"ButtonStartMetadataEmbed": "Start Metadata innbaking",
"ButtonStats": "Statistikk",
"ButtonSubmit": "Send inn",
- "ButtonTest": "Test",
"ButtonUpload": "Last opp",
"ButtonUploadBackup": "Last opp sikkerhetskopi",
"ButtonUploadCover": "Last opp cover",
@@ -101,14 +95,11 @@
"ButtonViewAll": "Vis alt",
"ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Feil ved innhenting av metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Avansert",
"HeaderAppriseNotificationSettings": "Apprise notifikasjonsinstillinger",
"HeaderAudioTracks": "Lydspor",
"HeaderAudiobookTools": "Lydbok Filbehandlingsverktøy",
- "HeaderAuthentication": "Authentication",
"HeaderBackups": "Sikkerhetskopier",
"HeaderChangePassword": "Bytt passord",
"HeaderChapters": "Kapittel",
@@ -117,8 +108,6 @@
"HeaderCollectionItems": "Samlingsgjenstander",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Aktive nedlastinger",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Last ned kø",
"HeaderEbookFiles": "Ebook filer",
@@ -153,7 +142,6 @@
"HeaderOpenIDConnectAuthentication": "Autentisering med OpenID Connect",
"HeaderOpenRSSFeed": "Åpne RSS Feed",
"HeaderOtherFiles": "Andre filer",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Rettigheter",
"HeaderPlayerQueue": "Spiller kø",
"HeaderPlaylist": "Spilleliste",
@@ -189,14 +177,9 @@
"HeaderUpdateDetails": "Oppdater detaljer",
"HeaderUpdateLibrary": "Oppdater bibliotek",
"HeaderUsers": "Brukere",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Din statistikk",
"LabelAbridged": "Forkortet",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Kontotype",
- "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Gjest",
"LabelAccountTypeUser": "Bruker",
"LabelActivity": "Aktivitet",
@@ -204,13 +187,9 @@
"LabelAddToCollectionBatch": "Legg {0} bøker til samling",
"LabelAddToPlaylist": "Legg til i spilleliste",
"LabelAddToPlaylistBatch": "Legg {0} enheter til i spilleliste",
- "LabelAdded": "Lagt til",
"LabelAddedAt": "Lagt Til",
- "LabelAdminUsersOnly": "Admin users only",
"LabelAll": "Alle",
"LabelAllUsers": "Alle brukere",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Allerede i biblioteket",
"LabelAppend": "Legge til",
"LabelAuthor": "Forfatter",
@@ -218,14 +197,7 @@
"LabelAuthorLastFirst": "Forfatter (Etternavn Fornavn)",
"LabelAuthors": "Forfattere",
"LabelAutoDownloadEpisodes": "Last ned episoder automatisk",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Tilbake til bruker",
- "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Aktiver automatisk sikkerhetskopi",
"LabelBackupsEnableAutomaticBackupsHelp": "Sikkerhetskopier lagret under /metadata/backups",
"LabelBackupsMaxBackupSize": "Maks sikkerhetskopi størrelse (i GB)",
@@ -234,14 +206,11 @@
"LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhetskopi vil bli fjernet om gangen, hvis du allerede har flere sikkerhetskopier enn dette bør du fjerne de manuelt.",
"LabelBitrate": "Bithastighet",
"LabelBooks": "Bøker",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Endre passord",
"LabelChannels": "Kanaler",
"LabelChapterTitle": "Kapittel tittel",
"LabelChapters": "Kapitler",
"LabelChaptersFound": "kapitler funnet",
- "LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Lukk spiller",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Minimer serier",
@@ -260,12 +229,11 @@
"LabelCurrently": "Nåværende:",
"LabelCustomCronExpression": "Tilpasset Cron utrykk:",
"LabelDatetime": "Dato tid",
- "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
+ "LabelDays": "Dager",
"LabelDescription": "Beskrivelse",
"LabelDeselectAll": "Fjern valg",
"LabelDevice": "Enhet",
"LabelDeviceInfo": "Enhetsinformasjon",
- "LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Mappe",
"LabelDiscFromFilename": "Disk fra filnavn",
"LabelDiscFromMetadata": "Disk fra metadata",
@@ -273,32 +241,25 @@
"LabelDownload": "Last ned",
"LabelDownloadNEpisodes": "Last ned {0} episoder",
"LabelDuration": "Varighet",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Varighet funnet:",
"LabelEbook": "Ebok",
"LabelEbooks": "E-bøker",
"LabelEdit": "Rediger",
"LabelEmail": "Epost",
"LabelEmailSettingsFromAddress": "Fra Adresse",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "Sikker",
"LabelEmailSettingsSecureHelp": "Hvis aktivert, vil tilkoblingen bruke TLS under tilkobling til tjeneren. Ellers vil TLS bli brukt hvis tjeneren støtter STARTTLS utvidelsen. I de fleste tilfeller aktiver valget hvis du kobler til med port 465. Med port 587 eller 25 deaktiver valget. (fra nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Adresse",
"LabelEmbeddedCover": "Bak inn omslag",
"LabelEnable": "Aktiver",
"LabelEnd": "Slutt",
- "LabelEpisode": "Episode",
+ "LabelEndOfChapter": "Slutt på kapittel",
"LabelEpisodeTitle": "Episode tittel",
"LabelEpisodeType": "Episode type",
"LabelExample": "Eksempel",
"LabelExplicit": "Eksplisitt",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
+ "LabelExportOPML": "Eksporter OPML",
"LabelFeedURL": "Feed Adresse",
- "LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Fil",
"LabelFileBirthtime": "Fil Opprettelsesdato",
"LabelFileModified": "Fil Endret",
@@ -308,23 +269,19 @@
"LabelFinished": "Fullført",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
- "LabelFontBold": "Bold",
"LabelFontBoldness": "Skrifttykkelse",
"LabelFontFamily": "Fontfamilie",
- "LabelFontItalic": "Italic",
"LabelFontScale": "Font størrelse",
- "LabelFontStrikethrough": "Strikethrough",
- "LabelFormat": "Format",
"LabelGenre": "Sjanger",
"LabelGenres": "Sjangers",
"LabelHardDeleteFile": "Tving sletting av fil",
"LabelHasEbook": "Har e-bok",
"LabelHasSupplementaryEbook": "Har komplimentær e-bok",
- "LabelHighestPriority": "Highest priority",
+ "LabelHideSubtitles": "Skjul undertekster",
"LabelHost": "Tjener",
"LabelHour": "Time",
+ "LabelHours": "Timer",
"LabelIcon": "Ikon",
- "LabelImageURLFromTheWeb": "Image URL from the web",
"LabelInProgress": "I gang",
"LabelIncludeInTracklist": "Inkluder i sporliste",
"LabelIncomplete": "Ufullstendig",
@@ -341,7 +298,6 @@
"LabelItem": "Enhet",
"LabelLanguage": "Språk",
"LabelLanguageDefaultServer": "Standard tjener språk",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "Siste bok lagt til",
"LabelLastBookUpdated": "Siste bok oppdatert",
"LabelLastSeen": "Sist sett",
@@ -353,31 +309,17 @@
"LabelLess": "Mindre",
"LabelLibrariesAccessibleToUser": "Biblioteker tilgjengelig for bruker",
"LabelLibrary": "Bibliotek",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Bibliotek enhet",
"LabelLibraryName": "Bibliotek navn",
"LabelLimit": "Begrensning",
"LabelLineSpacing": "Linjemellomrom",
"LabelListenAgain": "Lytt igjen",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
- "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Mediespiller",
"LabelMediaType": "Medie type",
- "LabelMetaTag": "Meta Tag",
- "LabelMetaTags": "Meta Tags",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadata Leverandør",
"LabelMinute": "Minutt",
"LabelMissing": "Mangler",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
"LabelMore": "Mer",
"LabelMoreInfo": "Mer info",
"LabelName": "Navn",
@@ -389,7 +331,6 @@
"LabelNewestEpisodes": "Nyeste episoder",
"LabelNextBackupDate": "Neste sikkerhetskopi dato",
"LabelNextScheduledRun": "Neste planlagte kjøring",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Ingen episoder valgt",
"LabelNotFinished": "Ikke fullført",
"LabelNotStarted": "Ikke startet",
@@ -405,13 +346,11 @@
"LabelNotificationsMaxQueueSizeHelp": "Hendelser er begrenset til avfyre 1 gang per sekund. Hendelser vil bli ignorert om køen er full. Dette forhindrer Notifikasjon spam.",
"LabelNumberOfBooks": "Antall bøker",
"LabelNumberOfEpisodes": "Antall episoder",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Åpne RSS Feed",
"LabelOverwrite": "Overskriv",
"LabelPassword": "Passord",
"LabelPath": "Sti",
+ "LabelPermanent": "Fast",
"LabelPermissionsAccessAllLibraries": "Har tilgang til alle bibliotek",
"LabelPermissionsAccessAllTags": "Har til gang til alle tags",
"LabelPermissionsAccessExplicitContent": "Har tilgang til eksplisitt material",
@@ -419,16 +358,12 @@
"LabelPermissionsDownload": "Kan laste ned",
"LabelPermissionsUpdate": "Kan oppdatere",
"LabelPermissionsUpload": "Kan laste opp",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Bilde sti/URL",
"LabelPlayMethod": "Avspillingsmetode",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Spilleliste",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcast-søkeområde",
"LabelPodcastType": "Podcast type",
"LabelPodcasts": "Podcaster",
- "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefiks som skal ignoreres (skiller ikke mellom store og små bokstaver)",
"LabelPreventIndexing": "Forhindre at din feed fra å bli indeksert av iTunes og Google podcast kataloger",
"LabelPrimaryEbook": "Primær ebok",
@@ -437,38 +372,30 @@
"LabelPubDate": "Publiseringsdato",
"LabelPublishYear": "Publikasjonsår",
"LabelPublisher": "Forlegger",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier e-post",
"LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn",
"LabelRSSFeedOpen": "RSS Feed åpne",
"LabelRSSFeedPreventIndexing": "Forhindre indeksering",
"LabelRSSFeedSlug": "RSS-informasjonskanalunderadresse",
- "LabelRSSFeedURL": "RSS Feed URL",
"LabelRead": "Les",
"LabelReadAgain": "Les igjen",
"LabelReadEbookWithoutProgress": "Les ebok uten å beholde fremgang",
"LabelRecentSeries": "Nylige serier",
"LabelRecentlyAdded": "Nylig tillagt",
"LabelRecommended": "Anbefalte",
- "LabelRedo": "Redo",
- "LabelRegion": "Region",
"LabelReleaseDate": "Utgivelsesdato",
"LabelRemoveCover": "Fjern omslag",
- "LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Søkeord",
"LabelSearchTitle": "Søk tittel",
"LabelSearchTitleOrASIN": "Søk tittel eller ASIN",
"LabelSeason": "Sesong",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Velg alle episoder",
"LabelSelectEpisodesShowing": "Velg {0} episoder vist",
- "LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebok til...",
"LabelSequence": "Sekvens",
"LabelSeries": "Serier",
"LabelSeriesName": "Serier Navn",
"LabelSeriesProgress": "Serier fremgang",
- "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Sett som primær",
"LabelSetEbookAsSupplementary": "Sett som supplerende",
"LabelSettingsAudiobooksOnly": "Kun lydbøker",
@@ -482,8 +409,6 @@
"LabelSettingsEnableWatcher": "Aktiver overvåker",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappe overvåker for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Eksperimentelle funksjoner",
"LabelSettingsExperimentalFeaturesHelp": "Funksjoner under utvikling som kan trenge din tilbakemelding og hjelp med testing. Klikk for å åpne GitHub diskusjon.",
"LabelSettingsFindCovers": "Finn omslag",
@@ -492,8 +417,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier som har kun en bok vil bli gjemt på serie- og hjemmeside hyllen.",
"LabelSettingsHomePageBookshelfView": "Hjemmeside bruk bokhyllevisning",
"LabelSettingsLibraryBookshelfView": "Bibliotek bruk bokhyllevisning",
- "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": "Analyser undertekster",
"LabelSettingsParseSubtitlesHelp": "Trekk ut undertekster fra lydbok mappenavn.
undertekster må være separert med \" - \"
f.eks. \"Boktittel - Undertekst her\" har Undertekst \"Undertekst her\"",
"LabelSettingsPreferMatchedMetadata": "Foretrekk funnet metadata",
@@ -509,12 +432,11 @@
"LabelSettingsStoreMetadataWithItem": "Lagre metadata med gjenstand",
"LabelSettingsStoreMetadataWithItemHelp": "Som standard vil metadata bli lagret under /metadata/items, aktiveres dette valget vil metadata bli lagret i samme mappe som gjenstanden",
"LabelSettingsTimeFormat": "Tid format",
+ "LabelShare": "Dele",
+ "LabelShareURL": "Dele URL",
"LabelShowAll": "Vis alt",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Størrelse",
"LabelSleepTimer": "Sove-timer",
- "LabelSlug": "Slug",
- "LabelStart": "Start",
"LabelStartTime": "Start Tid",
"LabelStarted": "Startet",
"LabelStartedAt": "Startet",
@@ -535,19 +457,15 @@
"LabelStatsWeekListening": "Uker lyttet",
"LabelSubtitle": "undertekster",
"LabelSupportedFileTypes": "Støttede filtyper",
- "LabelTag": "Tag",
"LabelTags": "Tagger",
"LabelTagsAccessibleToUser": "Tagger tilgjengelig for bruker",
"LabelTagsNotAccessibleToUser": "Tagger ikke tilgjengelig for bruker",
"LabelTasks": "Oppgaver som kjører",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Tema",
"LabelThemeDark": "Mørk",
"LabelThemeLight": "Lys",
"LabelTimeBase": "Tidsbase",
+ "LabelTimeInMinutes": "Timer i minutter",
"LabelTimeListened": "Tid lyttet",
"LabelTimeListenedToday": "Tid lyttet idag",
"LabelTimeRemaining": "{0} gjennstående",
@@ -567,9 +485,7 @@
"LabelTracksMultiTrack": "Flerspor",
"LabelTracksNone": "Ingen spor",
"LabelTracksSingleTrack": "Enkelspor",
- "LabelType": "Type",
"LabelUnabridged": "Uavkortet",
- "LabelUndo": "Undo",
"LabelUnknown": "Ukjent",
"LabelUpdateCover": "Oppdater omslag",
"LabelUpdateCoverHelp": "Tillat overskriving av eksisterende omslag for de valgte bøkene når en lik bok er funnet",
@@ -578,7 +494,6 @@
"LabelUpdatedAt": "Oppdatert",
"LabelUploaderDragAndDrop": "Dra og slipp filer eller mapper",
"LabelUploaderDropFiles": "Slipp filer",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Bruk kapittelspor",
"LabelUseFullTrack": "Bruke hele sporet",
"LabelUser": "Bruker",
@@ -590,8 +505,6 @@
"LabelViewQueue": "Vis spillerkø",
"LabelVolume": "Volum",
"LabelWeekdaysToRun": "Ukedager å kjøre",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Din lydbok lengde",
"LabelYourBookmarks": "Dine bokmerker",
"LabelYourPlaylists": "Dine spillelister",
@@ -605,7 +518,6 @@
"MessageBookshelfNoCollections": "Du har ikke laget noen samlinger ennå",
"MessageBookshelfNoRSSFeeds": "Ingen RSS feed er åpen",
"MessageBookshelfNoResultsForFilter": "Ingen resultat for filter \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Du har ingen serier",
"MessageChapterEndIsAfter": "Kapittel slutt er etter slutt av lydboken",
"MessageChapterErrorFirstNotZero": "Første kapittel starter på 0",
@@ -617,24 +529,16 @@
"MessageConfirmDeleteBackup": "Er du sikker på at du vil slette sikkerhetskopi for {0}?",
"MessageConfirmDeleteFile": "Dette vil slette filen fra filsystemet. Er du sikker?",
"MessageConfirmDeleteLibrary": "Er du sikker på at du vil slette biblioteket \"{0}\" for godt?",
- "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Er du sikker på at du vil slette denne sesjonen?",
"MessageConfirmForceReScan": "Er du sikker på at du vil tvinge en ny skann?",
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på at du vil markere alle episodene som fullført?",
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på at du vil markere alle episodene som ikke fullført?",
"MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?",
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?",
- "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?",
- "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmRemoveAllChapters": "Er du sikker på at du vil fjerne alle kapitler?",
- "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?",
"MessageConfirmRemoveEpisode": "Er du sikker på at du vil fjerne episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Er du sikker på at du vil fjerne {0} episoder?",
- "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Er du sikker på at du vil fjerne forteller \"{0}\"?",
"MessageConfirmRemovePlaylist": "Er du sikker på at du vil fjerne spillelisten \"{0}\"?",
"MessageConfirmRenameGenre": "Er du sikker på at du vil endre sjanger \"{0}\" til \"{1}\" for alle gjenstandene?",
@@ -648,7 +552,6 @@
"MessageDragFilesIntoTrackOrder": "Dra filene i rett spor rekkefølge",
"MessageEmbedFinished": "Bak inn Fullført!",
"MessageEpisodesQueuedForDownload": "{0} Episode(r) lagt til i kø for nedlasting",
- "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 vil bli {0}",
"MessageFetching": "Henter...",
"MessageForceReScanDescription": "vil skanne alle filene igjen som en ny skann. Lyd fil ID3 tagger, OPF filer og tekstfiler vil bli skannet som nye.",
@@ -660,7 +563,6 @@
"MessageListeningSessionsInTheLastYear": "{0} Lyttesesjoner iløpet av siste året",
"MessageLoading": "Laster...",
"MessageLoadingFolders": "Laster mapper...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B mislykkes!",
"MessageM4BFinished": "M4B fullført!",
"MessageMapChapterTitles": "Bruk kapittel titler fra din eksisterende lydbok kapitler uten å justere tidsstempel",
@@ -696,7 +598,6 @@
"MessageNoSeries": "Ingen serier",
"MessageNoTags": "Ingen tags",
"MessageNoTasksRunning": "Ingen oppgaver kjører",
- "MessageNoUpdateNecessary": "Ingen oppdatering nødvendig",
"MessageNoUpdatesWereNecessary": "Ingen oppdatering var nødvendig",
"MessageNoUserPlaylists": "Du har ingen spillelister",
"MessageNotYetImplemented": "Ikke implementert ennå",
@@ -715,7 +616,6 @@
"MessageRestoreBackupConfirm": "Er du sikker på at du vil gjenopprette sikkerhetskopien som var laget",
"MessageRestoreBackupWarning": "gjenoppretting av sikkerhetskopi vil overskrive hele databasen under /config og omslagsbilde under /metadata/items og /metadata/authors.
Sikkerhetskopier endrer ikke noen filer under dine bibliotekmapper. Hvis du har aktivert tjenerinstillingen for å lagre omslagsbilder og metadata i bibliotekmapper så vil ikke de filene bli tatt sikkerhetskopi eller overskrevet.
Alle klientene som bruker din tjener vil bli fornyet automatisk.",
"MessageSearchResultsFor": "Søk resultat for",
- "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Tjener kunne ikke bli nådd",
"MessageSetChaptersFromTracksDescription": "Sett kapitler ved å bruke hver lydfil som kapittel og kapitteltittel som lydfilnavnet",
"MessageStartPlaybackAtTime": "Start avspilling av \"{0}\" ved {1}?",
@@ -743,7 +643,6 @@
"PlaceholderSearchEpisode": "Søk episode..",
"ToastAccountUpdateFailed": "Mislykkes å oppdatere konto",
"ToastAccountUpdateSuccess": "Konto oppdatert",
- "ToastAuthorImageRemoveFailed": "Mislykkes å fjerne bilde",
"ToastAuthorImageRemoveSuccess": "Forfatter bilde fjernet",
"ToastAuthorUpdateFailed": "Mislykkes å oppdatere forfatter",
"ToastAuthorUpdateMerged": "Forfatter slått sammen",
@@ -760,28 +659,19 @@
"ToastBatchUpdateSuccess": "Bulk oppdatering fullført",
"ToastBookmarkCreateFailed": "Misslykkes å opprette bokmerke",
"ToastBookmarkCreateSuccess": "Bokmerke lagt til",
- "ToastBookmarkRemoveFailed": "Misslykkes å fjerne bokmerke",
"ToastBookmarkRemoveSuccess": "Bokmerke fjernet",
"ToastBookmarkUpdateFailed": "Misslykkes å oppdatere bokmerke",
"ToastBookmarkUpdateSuccess": "Bokmerke oppdatert",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Kapittel har feil",
"ToastChaptersMustHaveTitles": "Kapittel må ha titler",
- "ToastCollectionItemsRemoveFailed": "Misslykkes å fjerne gjenstand(er) fra samling",
"ToastCollectionItemsRemoveSuccess": "Gjenstand(er) fjernet fra samling",
- "ToastCollectionRemoveFailed": "Misslykkes å fjerne samling",
"ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateFailed": "Misslykkes å oppdatere samling",
"ToastCollectionUpdateSuccess": "samlingupdated",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Misslykkes å oppdatere omslag",
"ToastItemCoverUpdateSuccess": "Omslag oppdatert",
"ToastItemDetailsUpdateFailed": "Misslykkes å oppdatere detaljer",
"ToastItemDetailsUpdateSuccess": "Detaljer oppdatert",
- "ToastItemDetailsUpdateUnneeded": "Ingen oppdateringer nødvendig for detaljer",
"ToastItemMarkedAsFinishedFailed": "Misslykkes å markere som Fullført",
"ToastItemMarkedAsFinishedSuccess": "Gjenstand marker som Fullført",
"ToastItemMarkedAsNotFinishedFailed": "Misslykkes å markere som Ikke Fullført",
@@ -796,7 +686,6 @@
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" oppdatert",
"ToastPlaylistCreateFailed": "Misslykkes å opprette spilleliste",
"ToastPlaylistCreateSuccess": "Spilleliste opprettet",
- "ToastPlaylistRemoveFailed": "Misslykkes å fjerne spilleliste",
"ToastPlaylistRemoveSuccess": "Spilleliste fjernet",
"ToastPlaylistUpdateFailed": "Misslykkes å oppdatere spilleliste",
"ToastPlaylistUpdateSuccess": "Spilleliste oppdatert",
@@ -810,16 +699,11 @@
"ToastSendEbookToDeviceSuccess": "Ebok sendt til \"{0}\"",
"ToastSeriesUpdateFailed": "Misslykkes å oppdatere serie",
"ToastSeriesUpdateSuccess": "Serie oppdatert",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Misslykkes å slette sesjon",
"ToastSessionDeleteSuccess": "Sesjon slettet",
"ToastSocketConnected": "Socket koblet til",
"ToastSocketDisconnected": "Socket koblet fra",
"ToastSocketFailedToConnect": "Misslykkes å koble til Socket",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Misslykkes å slette bruker",
"ToastUserDeleteSuccess": "Bruker slettet"
}
diff --git a/client/strings/pl.json b/client/strings/pl.json
index 0fe8535dda..c786a1188f 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Wybierz pliki",
"ButtonClearFilter": "Wyczyść filtr",
"ButtonCloseFeed": "Zamknij kanał",
+ "ButtonCloseSession": "Zamknij otwartą sesję",
"ButtonCollections": "Kolekcje",
"ButtonConfigureScanner": "Skonfiguruj skaner",
"ButtonCreate": "Utwórz",
@@ -28,6 +29,7 @@
"ButtonEdit": "Edycja",
"ButtonEditChapters": "Edytuj rozdziały",
"ButtonEditPodcast": "Edytuj podcast",
+ "ButtonEnable": "Włącz",
"ButtonForceReScan": "Wymuś ponowne skanowanie",
"ButtonFullPath": "Pełna ścieżka",
"ButtonHide": "Ukryj",
@@ -46,10 +48,11 @@
"ButtonNevermind": "Anuluj",
"ButtonNext": "Następny",
"ButtonNextChapter": "Następny rozdział",
+ "ButtonNextItemInQueue": "Następny element w kolejce",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Otwórz feed",
"ButtonOpenManager": "Otwórz menadżera",
- "ButtonPause": "Pause",
+ "ButtonPause": "Wstrzymaj",
"ButtonPlay": "Odtwarzaj",
"ButtonPlaying": "Odtwarzane",
"ButtonPlaylists": "Listy odtwarzania",
@@ -59,6 +62,7 @@
"ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji",
"ButtonQueueAddItem": "Dodaj do kolejki",
"ButtonQueueRemoveItem": "Usuń z kolejki",
+ "ButtonQuickEmbedMetadata": "Szybkie wstawianie metadanych",
"ButtonQuickMatch": "Szybkie dopasowanie",
"ButtonReScan": "Ponowne skanowanie",
"ButtonRead": "Czytaj",
@@ -103,6 +107,7 @@
"ErrorUploadFetchMetadataNoResults": "Nie można pobrać metadanych — spróbuj zaktualizować tytuł i/lub autora",
"ErrorUploadLacksTitle": "Musi mieć tytuł",
"HeaderAccount": "Konto",
+ "HeaderAddCustomMetadataProvider": "Dodaj niestandardowego dostawcę metadanych",
"HeaderAdvanced": "Zaawansowane",
"HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise",
"HeaderAudioTracks": "Ścieżki audio",
@@ -121,7 +126,7 @@
"HeaderDetails": "Szczegóły",
"HeaderDownloadQueue": "Kolejka do ściągania",
"HeaderEbookFiles": "Pliki Ebook",
- "HeaderEmail": "Email",
+ "HeaderEmail": "E-mail",
"HeaderEmailSettings": "Ustawienia e-mail",
"HeaderEpisodes": "Rozdziały",
"HeaderEreaderDevices": "Czytniki",
@@ -130,7 +135,6 @@
"HeaderFindChapters": "Wyszukaj rozdziały",
"HeaderIgnoredFiles": "Zignoruj pliki",
"HeaderItemFiles": "Pliki",
- "HeaderItemMetadataUtils": "Item Metadata Utils",
"HeaderLastListeningSession": "Ostatnia sesja słuchania",
"HeaderLatestEpisodes": "Najnowsze odcinki",
"HeaderLibraries": "Biblioteki",
@@ -148,6 +152,8 @@
"HeaderMetadataToEmbed": "Osadź metadane",
"HeaderNewAccount": "Nowe konto",
"HeaderNewLibrary": "Nowa biblioteka",
+ "HeaderNotificationCreate": "Utwórz powiadomienie",
+ "HeaderNotificationUpdate": "Zaktualizuj powiadomienie",
"HeaderNotifications": "Powiadomienia",
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
@@ -160,9 +166,9 @@
"HeaderPlaylistItems": "Pozycje listy odtwarzania",
"HeaderPodcastsToAdd": "Podcasty do dodania",
"HeaderPreviewCover": "Podgląd okładki",
- "HeaderRSSFeedGeneral": "RSS Details",
+ "HeaderRSSFeedGeneral": "Szczegóły RSS",
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
- "HeaderRSSFeeds": "RSS Feeds",
+ "HeaderRSSFeeds": "Kanały RSS",
"HeaderRemoveEpisode": "Usuń odcinek",
"HeaderRemoveEpisodes": "Usuń {0} odcinków",
"HeaderSavedMediaProgress": "Zapisany postęp",
@@ -204,8 +210,8 @@
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
"LabelAddToPlaylist": "Dodaj do playlisty",
"LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty",
- "LabelAdded": "Dodane",
"LabelAddedAt": "Dodano",
+ "LabelAddedDate": "Dodano {0}",
"LabelAdminUsersOnly": "Tylko użytkownicy administracyjni",
"LabelAll": "Wszystkie",
"LabelAllUsers": "Wszyscy użytkownicy",
@@ -221,20 +227,19 @@
"LabelAutoFetchMetadata": "Automatycznie pobierz metadane",
"LabelAutoFetchMetadataHelp": "Pobiera metadane dotyczące tytułu, autora i serii, aby usprawnić przesyłanie. Po przesłaniu może być konieczne dopasowanie dodatkowych metadanych.",
"LabelAutoLaunch": "Uruchom automatycznie",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
+ "LabelAutoRegister": "Automatyczna rejestracja",
+ "LabelAutoRegisterDescription": "Automatycznie utwórz nowych użytkowników po zalogowaniu",
"LabelBackToUser": "Powrót",
"LabelBackupLocation": "Lokalizacja kopii zapasowej",
"LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe",
"LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups",
- "LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB)",
+ "LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB) (0 oznacza nieograniczony)",
"LabelBackupsMaxBackupSizeHelp": "Jako zabezpieczenie przed błędną konfiguracją, kopie zapasowe nie będą wykonywane, jeśli przekroczą skonfigurowany rozmiar.",
"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",
"LabelBooks": "Książki",
- "LabelButtonText": "Button Text",
+ "LabelButtonText": "Tekst przycisku",
"LabelByAuthor": "autorstwa {0}",
"LabelChangePassword": "Zmień hasło",
"LabelChannels": "Kanały",
@@ -243,8 +248,9 @@
"LabelChaptersFound": "Znalezione rozdziały",
"LabelClickForMoreInfo": "Kliknij po więcej szczegółów",
"LabelClosePlayer": "Zamknij odtwarzacz",
- "LabelCodec": "Codec",
+ "LabelCodec": "Kodek",
"LabelCollapseSeries": "Podsumuj serię",
+ "LabelCollapseSubSeries": "Zwiń podserie",
"LabelCollection": "Kolekcja",
"LabelCollections": "Kolekcje",
"LabelComplete": "Ukończone",
@@ -258,7 +264,7 @@
"LabelCronExpression": "Wyrażenie CRON",
"LabelCurrent": "Aktualny",
"LabelCurrently": "Obecnie:",
- "LabelCustomCronExpression": "Custom Cron Expression:",
+ "LabelCustomCronExpression": "Niestandardowe wyrażenie Cron:",
"LabelDatetime": "Data i godzina",
"LabelDays": "Dni",
"LabelDeleteFromFileSystemCheckbox": "Usuń z systemu plików (odznacz, aby usunąć tylko z bazy danych)",
@@ -266,7 +272,7 @@
"LabelDeselectAll": "Odznacz wszystko",
"LabelDevice": "Urządzenie",
"LabelDeviceInfo": "Informacja o urządzeniu",
- "LabelDeviceIsAvailableTo": "Device is available to...",
+ "LabelDeviceIsAvailableTo": "Urządzenie jest dostępne do...",
"LabelDirectory": "Katalog",
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
@@ -274,35 +280,42 @@
"LabelDownload": "Pobierz",
"LabelDownloadNEpisodes": "Ściąganie {0} odcinków",
"LabelDuration": "Czas trwania",
- "LabelDurationComparisonExactMatch": "(exact match)",
+ "LabelDurationComparisonExactMatch": "(dokładne dopasowanie)",
"LabelDurationComparisonLonger": "({0} dłużej)",
"LabelDurationComparisonShorter": "({0} krócej)",
"LabelDurationFound": "Znaleziona długość:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooki",
"LabelEdit": "Edytuj",
- "LabelEmail": "Email",
+ "LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Z adresu",
"LabelEmailSettingsRejectUnauthorized": "Odrzuć nieautoryzowane certyfikaty",
"LabelEmailSettingsRejectUnauthorizedHelp": "Wyłączenie walidacji certyfikatów SSL może narazić cię na ryzyka bezpieczeństwa, takie jak ataki man-in-the-middle. Wyłącz tą opcję wyłącznie jeśli rozumiesz tego skutki i ufasz serwerowi pocztowemu, do którego się podłączasz.",
"LabelEmailSettingsSecure": "Bezpieczeństwo",
"LabelEmailSettingsSecureHelp": "Jeśli włączysz, połączenie będzie korzystać z TLS podczas łączenia do serwera. Jeśli wyłączysz, TLS będzie wykorzystane jeśli serwer wspiera rozszerzenie STARTTLS. W większości przypadków włącz to ustawienie jeśli łączysz się do portu 465. Dla portów 587 lub 25 pozostaw to ustawienie wyłączone. (na podstawie nodemailer.com/smtp/#authentication)",
- "LabelEmailSettingsTestAddress": "Test Address",
+ "LabelEmailSettingsTestAddress": "Adres testowy",
"LabelEmbeddedCover": "Wbudowana okładka",
"LabelEnable": "Włącz",
"LabelEnd": "Zakończ",
+ "LabelEndOfChapter": "Koniec rozdziału",
"LabelEpisode": "Odcinek",
"LabelEpisodeTitle": "Tytuł odcinka",
"LabelEpisodeType": "Typ odcinka",
+ "LabelEpisodes": "Epizody",
"LabelExample": "Przykład",
+ "LabelExpandSeries": "Rozwiń serie",
+ "LabelExpandSubSeries": "Rozwiń podserie",
"LabelExplicit": "Nieprzyzwoite",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
+ "LabelExplicitChecked": "Nieprzyzwoite (sprawdzone)",
+ "LabelExplicitUnchecked": "Przyzwoite (niesprawdzone)",
+ "LabelExportOPML": "Wyeksportuj OPML",
"LabelFeedURL": "URL kanału",
"LabelFetchingMetadata": "Pobieranie metadanych",
"LabelFile": "Plik",
"LabelFileBirthtime": "Data utworzenia pliku",
+ "LabelFileBornDate": "Utworzony {0}",
"LabelFileModified": "Data modyfikacji pliku",
+ "LabelFileModifiedDate": "Modyfikowany {0}",
"LabelFilename": "Nazwa pliku",
"LabelFilterByUser": "Filtruj według danego użytkownika",
"LabelFindEpisodes": "Znajdź odcinki",
@@ -312,7 +325,7 @@
"LabelFontBold": "Pogrubiony",
"LabelFontBoldness": "Grubość czcionki",
"LabelFontFamily": "Rodzina czcionek",
- "LabelFontItalic": "Italic",
+ "LabelFontItalic": "Kursywa",
"LabelFontScale": "Rozmiar czcionki",
"LabelFontStrikethrough": "Przekreślony",
"LabelFormat": "Format",
@@ -342,6 +355,7 @@
"LabelIntervalEveryHour": "Każdej godziny",
"LabelInvert": "Inversja",
"LabelItem": "Pozycja",
+ "LabelJumpBackwardAmount": "Rozmiar skoku do przodu",
"LabelLanguage": "Język",
"LabelLanguageDefaultServer": "Domyślny język serwera",
"LabelLanguages": "Języki",
@@ -356,13 +370,13 @@
"LabelLess": "Mniej",
"LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika",
"LabelLibrary": "Biblioteka",
- "LabelLibraryFilterSublistEmpty": "No {0}",
+ "LabelLibraryFilterSublistEmpty": "Brak {0}",
"LabelLibraryItem": "Element biblioteki",
"LabelLibraryName": "Nazwa biblioteki",
"LabelLimit": "Limit",
"LabelLineSpacing": "Odstęp między wierszami",
"LabelListenAgain": "Słuchaj ponownie",
- "LabelLogLevelDebug": "Debug",
+ "LabelLogLevelDebug": "Debugowanie",
"LabelLogLevelInfo": "Informacja",
"LabelLogLevelWarn": "Ostrzeżenie",
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
@@ -372,7 +386,7 @@
"LabelMediaPlayer": "Odtwarzacz",
"LabelMediaType": "Typ mediów",
"LabelMetaTag": "Tag",
- "LabelMetaTags": "Meta Tags",
+ "LabelMetaTags": "Meta Tagi",
"LabelMetadataOrderOfPrecedenceDescription": "Źródła metadanych o wyższym priorytecie będą zastępują źródła o niższym priorytecie",
"LabelMetadataProvider": "Dostawca metadanych",
"LabelMinute": "Minuta",
@@ -380,8 +394,7 @@
"LabelMissing": "Brakujący",
"LabelMissingEbook": "Nie posiada ebooka",
"LabelMissingSupplementaryEbook": "Nie posiada dodatkowego ebooka",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
+ "LabelMobileRedirectURIs": "Dozwolone URI przekierowań mobilnych",
"LabelMore": "Więcej",
"LabelMoreInfo": "Więcej informacji",
"LabelName": "Nazwa",
@@ -409,9 +422,6 @@
"LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.",
"LabelNumberOfBooks": "Liczba książek",
"LabelNumberOfEpisodes": "# odcinków",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Otwórz kanał RSS",
"LabelOverwrite": "Nadpisz",
"LabelPassword": "Hasło",
@@ -427,13 +437,9 @@
"LabelPersonalYearReview": "Podsumowanie twojego roku ({0})",
"LabelPhotoPathURL": "Scieżka/URL do zdjęcia",
"LabelPlayMethod": "Metoda odtwarzania",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Listy odtwarzania",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów",
- "LabelPodcastType": "Podcast Type",
"LabelPodcasts": "Podcasty",
- "LabelPort": "Port",
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
"LabelPreventIndexing": "Zapobiega indeksowaniu przez iTunes i Google",
"LabelPrimaryEbook": "Główny ebook",
@@ -443,12 +449,10 @@
"LabelPublishYear": "Rok publikacji",
"LabelPublisher": "Wydawca",
"LabelPublishers": "Wydawcy",
- "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
- "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed otwarty",
"LabelRSSFeedPreventIndexing": "Zapobiegaj indeksowaniu",
- "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "URL kanały RSS",
+ "LabelRandomly": "Losowo",
"LabelReAddSeriesToContinueListening": "Ponownie Dodaj Serię do sekcji Kontunuuj Odtwarzanie",
"LabelRead": "Czytaj",
"LabelReadAgain": "Czytaj ponownie",
@@ -457,7 +461,6 @@
"LabelRecentlyAdded": "Niedawno dodany",
"LabelRecommended": "Polecane",
"LabelRedo": "Wycofaj",
- "LabelRegion": "Region",
"LabelReleaseDate": "Data wydania",
"LabelRemoveCover": "Usuń okładkę",
"LabelRowsPerPage": "Wierszy na stronę",
@@ -467,7 +470,6 @@
"LabelSeason": "Sezon",
"LabelSelectAll": "Wybierz wszystko",
"LabelSelectAllEpisodes": "Wybierz wszystkie odcinki",
- "LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Wybór użytkowników",
"LabelSendEbookToDevice": "Wyślij ebook do...",
"LabelSequence": "Kolejność",
@@ -498,8 +500,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serie, które posiadają tylko jedną książkę, nie będą pokazywane na stronie z seriami i na stronie domowej z półkami.",
"LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej",
"LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki",
- "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": "Przetwarzaj podtytuły",
"LabelSettingsParseSubtitlesHelp": "Opcja pozwala na pobranie podtytułu z nazwy folderu z audiobookiem.
Podtytuł musi być rozdzielony za pomocą separatora \" - \"
Przykład: \"Book Title - A Subtitle Here\" podtytuł \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Preferowanie dopasowanych metadanych",
@@ -523,7 +523,6 @@
"LabelShowSubtitles": "Pokaż Napisy",
"LabelSize": "Rozmiar",
"LabelSleepTimer": "Wyłącznik czasowy",
- "LabelSlug": "Slug",
"LabelStart": "Rozpocznij",
"LabelStartTime": "Czas rozpoczęcia",
"LabelStarted": "Rozpoczęty",
@@ -545,19 +544,14 @@
"LabelStatsWeekListening": "Tydzień słuchania",
"LabelSubtitle": "Podtytuł",
"LabelSupportedFileTypes": "Obsługiwane typy plików",
- "LabelTag": "Tag",
"LabelTags": "Tagi",
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
- "LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
- "LabelTasks": "Tasks Running",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
- "LabelTheme": "Theme",
"LabelThemeDark": "Ciemny",
"LabelThemeLight": "Jasny",
- "LabelTimeBase": "Time Base",
+ "LabelTimeDurationXHours": "{0} godzin",
+ "LabelTimeDurationXMinutes": "{0} minuty",
+ "LabelTimeDurationXSeconds": "{0} sekundy",
+ "LabelTimeInMinutes": "Czas w minutach",
"LabelTimeListened": "Czas odtwarzania",
"LabelTimeListenedToday": "Czas odtwarzania dzisiaj",
"LabelTimeRemaining": "Pozostało {0}",
@@ -574,11 +568,7 @@
"LabelTrackFromFilename": "Ścieżka z nazwy pliku",
"LabelTrackFromMetadata": "Ścieżka z metadanych",
"LabelTracks": "Ścieżki",
- "LabelTracksMultiTrack": "Multi-track",
- "LabelTracksNone": "No tracks",
- "LabelTracksSingleTrack": "Single-track",
"LabelType": "Typ",
- "LabelUnabridged": "Unabridged",
"LabelUndo": "Wycofaj",
"LabelUnknown": "Nieznany",
"LabelUpdateCover": "Zaktalizuj odkładkę",
@@ -601,13 +591,14 @@
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
"LabelVolume": "Głośność",
"LabelWeekdaysToRun": "Dni tygodnia",
+ "LabelXBooks": "{0} książek",
+ "LabelXItems": "{0} elementów",
"LabelYearReviewHide": "Ukryj Podsumowanie Roku",
"LabelYearReviewShow": "Pokaż Podsumowanie Roku",
"LabelYourAudiobookDuration": "Czas trwania audiobooka",
"LabelYourBookmarks": "Twoje zakładki",
"LabelYourPlaylists": "Twoje playlisty",
"LabelYourProgress": "Twój postęp",
- "MessageAddToPlayerQueue": "Add to player queue",
"MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji
Apprise API albo innego rozwiązania, które obsługuje schemat zapytań Apprise.
URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem
http://192.168.1.1:8337
to wpisany tutaj URL powinien mieć postać:
http://192.168.1.1:8337/notify
.",
"MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w
/metadata/items
&
/metadata/authors
. Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.",
"MessageBackupsLocationEditNote": "Uwaga: Zmiana lokalizacji kopii zapasowej nie przenosi ani nie modyfikuje istniejących kopii zapasowych",
@@ -621,53 +612,34 @@
"MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii",
"MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka",
"MessageChapterErrorFirstNotZero": "Pierwszy rozdział musi rozpoczynać się na 0",
- "MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
- "MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
"MessageChapterStartIsAfter": "Początek rozdziału następuje po zakończeniu audiobooka",
"MessageCheckingCron": "Sprawdzanie cron...",
- "MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
"MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?",
"MessageConfirmDeleteFile": "Ta operacja usunie plik z twojego dysku. Jesteś pewien?",
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Ta operacja usunie pozycję biblioteki z bazy danych i z dysku. Czy jesteś pewien?",
- "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
"MessageConfirmMarkAllEpisodesFinished": "Czy na pewno chcesz oznaczyć wszystkie odcinki jako ukończone?",
"MessageConfirmMarkAllEpisodesNotFinished": "Czy na pewno chcesz oznaczyć wszystkie odcinki jako nieukończone?",
"MessageConfirmMarkSeriesFinished": "Czy na pewno chcesz oznaczyć wszystkie książki w tej serii jako ukończone?",
"MessageConfirmMarkSeriesNotFinished": "Czy na pewno chcesz oznaczyć wszystkie książki w tej serii jako nieukończone?",
- "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?",
- "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}\"?",
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
"MessageConfirmRemoveListeningSessions": "Czy na pewno chcesz usunąć {0} sesji słuchania?",
- "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Czy jesteś pewien, że chcesz usunąć twoją playlistę \"{0}\"?",
- "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
- "MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
- "MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
- "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
- "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
- "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
- "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Pobieranie odcinka",
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
+ "MessageEmbedFailed": "Niepowodzenie wstawiania!",
"MessageEmbedFinished": "Osadzanie zakończone!",
"MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania",
- "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": "URL kanału: {0}",
"MessageFetching": "Pobieranie...",
"MessageForceReScanDescription": "przeskanuje wszystkie pliki ponownie, jak przy świeżym skanowaniu. Tagi ID3 plików audio, pliki OPF i pliki tekstowe będą skanowane jak nowe.",
"MessageImportantNotice": "Ważna informacja!",
"MessageInsertChapterBelow": "Wstaw rozdział poniżej",
"MessageItemsSelected": "{0} zaznaczone elementy",
- "MessageItemsUpdated": "{0} Items Updated",
"MessageJoinUsOn": "Dołącz do nas na",
"MessageListeningSessionsInTheLastYear": "Sesje słuchania w ostatnim roku: {0}",
"MessageLoading": "Ładowanie...",
@@ -689,8 +661,6 @@
"MessageNoCollections": "Brak kolekcji",
"MessageNoCoversFound": "Okładki nieznalezione",
"MessageNoDescription": "Brak opisu",
- "MessageNoDownloadsInProgress": "No downloads currently in progress",
- "MessageNoDownloadsQueued": "No downloads queued",
"MessageNoEpisodeMatchesFound": "Nie znaleziono pasujących odcinków",
"MessageNoEpisodes": "Brak odcinków",
"MessageNoFoldersAvailable": "Brak dostępnych folderów",
@@ -705,13 +675,11 @@
"MessageNoPodcastsFound": "Nie znaleziono podcastów",
"MessageNoResults": "Brak wyników",
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
- "MessageNoSeries": "No Series",
- "MessageNoTags": "No Tags",
"MessageNoTasksRunning": "Brak uruchomionych zadań",
- "MessageNoUpdateNecessary": "Brak konieczności aktualizacji",
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
"MessageNoUserPlaylists": "Nie masz żadnych list odtwarzania",
"MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
+ "MessageOpmlPreviewNote": "Uwaga: To jest podgląd sparsowanego pliku OPML. Tytuł podcastu wzięty został z wątku RSS.",
"MessageOr": "lub",
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
@@ -723,13 +691,11 @@
"MessageRemoveFromPlayerQueue": "Usuń z kolejki odtwarzacza",
"MessageRemoveUserWarning": "Czy na pewno chcesz trwale usunąć użytkownika \"{0}\"?",
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
- "MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
"MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu",
"MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisanie bazy danych w folderze /config oraz okładek w folderze /metadata/items & /metadata/authors.
Kopie zapasowe nie modyfikują żadnego pliku w folderach z plikami audio. Jeśli włączyłeś ustawienia serwera, aby przechowywać okładki i metadane w folderach biblioteki, to nie są one zapisywane w kopii zapasowej lub nadpisywane
Wszyscy klienci korzystający z Twojego serwera będą automatycznie odświeżani.",
"MessageSearchResultsFor": "Wyniki wyszukiwania dla",
"MessageSelected": "{0} wybranych",
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
- "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageShareExpirationWillBe": "Czas udostępniania
{0} ",
"MessageShareExpiresIn": "Wygaśnie za {0}",
"MessageShareURLWillBe": "Udostępnione pod linkiem
{0} ",
@@ -756,9 +722,24 @@
"PlaceholderNewPlaylist": "Nowa nazwa playlisty",
"PlaceholderSearch": "Szukanie..",
"PlaceholderSearchEpisode": "Szukanie odcinka..",
+ "StatsAuthorsAdded": "dodano autorów",
+ "StatsBooksAdded": "dodano książki",
+ "StatsBooksFinished": "ukończone książki",
+ "StatsBooksFinishedThisYear": "Wybrane książki ukończone w tym roku…",
+ "StatsBooksListenedTo": "książki wysłuchane",
+ "StatsCollectionGrewTo": "Twoja kolekcja książek wzrosła do…",
+ "StatsSessions": "sesje",
+ "StatsSpentListening": "spędzono na słuchaniu",
+ "StatsTopAuthor": "TOPOWY AUTOR",
+ "StatsTopAuthors": "TOPOWI AUTORZY",
+ "StatsTopGenre": "TOPOWY GATUNEK",
+ "StatsTopGenres": "TOPOWE GATUNKI",
+ "StatsTopMonth": "TOPOWY MIESIĄC",
+ "StatsTopNarrator": "TOPOWY NARRATOR",
+ "StatsTopNarrators": "TOPOWI NARRATORZY",
+ "StatsYearInReview": "PRZEGLĄD ROKU",
"ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
"ToastAccountUpdateSuccess": "Zaktualizowano konto",
- "ToastAuthorImageRemoveFailed": "Nie udało się usunąć obrazu",
"ToastAuthorImageRemoveSuccess": "Zdjęcie autora usunięte",
"ToastAuthorUpdateFailed": "nie udało się zaktualizować autora",
"ToastAuthorUpdateMerged": "Autor scalony",
@@ -775,28 +756,17 @@
"ToastBatchUpdateSuccess": "Aktualizacja wsadowa powiodła się",
"ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki",
"ToastBookmarkCreateSuccess": "Dodano zakładkę",
- "ToastBookmarkRemoveFailed": "Nie udało się usunąć zakładki",
"ToastBookmarkRemoveSuccess": "Zakładka została usunięta",
"ToastBookmarkUpdateFailed": "Nie udało się zaktualizować zakładki",
"ToastBookmarkUpdateSuccess": "Zaktualizowano zakładkę",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
- "ToastChaptersHaveErrors": "Chapters have errors",
- "ToastChaptersMustHaveTitles": "Chapters must have titles",
- "ToastCollectionItemsRemoveFailed": "Nie udało się usunąć pozycji z kolekcji",
"ToastCollectionItemsRemoveSuccess": "Przedmiot(y) zostały usunięte z kolekcji",
- "ToastCollectionRemoveFailed": "Nie udało się usunąć kolekcji",
"ToastCollectionRemoveSuccess": "Kolekcja usunięta",
"ToastCollectionUpdateFailed": "Nie udało się zaktualizować kolekcji",
"ToastCollectionUpdateSuccess": "Zaktualizowano kolekcję",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Nie udało się zaktualizować okładki",
"ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę",
"ToastItemDetailsUpdateFailed": "Nie udało się zaktualizować szczegółów",
"ToastItemDetailsUpdateSuccess": "Zaktualizowano szczegóły",
- "ToastItemDetailsUpdateUnneeded": "Brak aktulizacji dla pozycji",
"ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako ukończone",
"ToastItemMarkedAsFinishedSuccess": "Pozycja oznaczona jako ukończona",
"ToastItemMarkedAsNotFinishedFailed": "Oznaczenie pozycji jako ukończonej nie powiodło się",
@@ -811,7 +781,6 @@
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
"ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
"ToastPlaylistCreateSuccess": "Playlista utworzona",
- "ToastPlaylistRemoveFailed": "Nie udało się usunąć playlisty",
"ToastPlaylistRemoveSuccess": "Playlista usunięta",
"ToastPlaylistUpdateFailed": "Nie udało się zaktualizować playlisty",
"ToastPlaylistUpdateSuccess": "Playlista zaktualizowana",
@@ -822,19 +791,11 @@
"ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji",
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
- "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
- "ToastSeriesUpdateFailed": "Series update failed",
- "ToastSeriesUpdateSuccess": "Series update success",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
"ToastSessionDeleteSuccess": "Sesja usunięta",
"ToastSocketConnected": "Nawiązano połączenie z serwerem",
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
"ToastUserDeleteSuccess": "Użytkownik usunięty"
}
diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json
index 027e511250..dba497d44e 100644
--- a/client/strings/pt-br.json
+++ b/client/strings/pt-br.json
@@ -37,7 +37,6 @@
"ButtonJumpForward": "Adiantar",
"ButtonLatest": "Mais Recentes",
"ButtonLibrary": "Biblioteca",
- "ButtonLogout": "Logout",
"ButtonLookup": "Procurar",
"ButtonManageTracks": "Gerenciar Faixas",
"ButtonMapChapterTitles": "Designar Títulos de Capítulos",
@@ -46,7 +45,6 @@
"ButtonNevermind": "Cancelar",
"ButtonNext": "Próximo",
"ButtonNextChapter": "Próximo Capítulo",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Abrir Feed",
"ButtonOpenManager": "Abrir Gerenciador",
"ButtonPause": "Pausar",
@@ -90,7 +88,6 @@
"ButtonStartMetadataEmbed": "Iniciar Inclusão de Metadados",
"ButtonSubmit": "Enviar",
"ButtonTest": "Testar",
- "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload de Backup",
"ButtonUploadCover": "Upload de Capa",
"ButtonUploadOPMLFile": "Upload Arquivo OPML",
@@ -107,7 +104,6 @@
"HeaderAudioTracks": "Trilhas de áudio",
"HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks",
"HeaderAuthentication": "Autenticação",
- "HeaderBackups": "Backups",
"HeaderChangePassword": "Trocar Senha",
"HeaderChapters": "Capítulos",
"HeaderChooseAFolder": "Escolha uma Pasta",
@@ -120,7 +116,6 @@
"HeaderDetails": "Detalhes",
"HeaderDownloadQueue": "Fila de Download",
"HeaderEbookFiles": "Arquivos Ebook",
- "HeaderEmail": "Email",
"HeaderEmailSettings": "Configurações de Email",
"HeaderEpisodes": "Episódios",
"HeaderEreaderDevices": "Dispositivos Ereader",
@@ -137,8 +132,6 @@
"HeaderLibraryStats": "Estatísticas da Biblioteca",
"HeaderListeningSessions": "Sessões",
"HeaderListeningStats": "Estatísticas",
- "HeaderLogin": "Login",
- "HeaderLogs": "Logs",
"HeaderManageGenres": "Gerenciar Gêneros",
"HeaderManageTags": "Gerenciar Etiquetas",
"HeaderMapDetails": "Designar Detalhes",
@@ -202,7 +195,6 @@
"LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção",
"LabelAddToPlaylist": "Adicionar à Lista de Reprodução",
"LabelAddToPlaylistBatch": "Adicionar {0} itens à Lista de Reprodução",
- "LabelAdded": "Acrescentado",
"LabelAddedAt": "Acrescentado em",
"LabelAdminUsersOnly": "Apenas usuários administradores",
"LabelAll": "Todos",
@@ -230,7 +222,6 @@
"LabelBackupsMaxBackupSizeHelp": "Como proteção contra uma configuração incorreta, backups darão erro se excederem o tamanho configurado.",
"LabelBackupsNumberToKeep": "Número de backups para guardar",
"LabelBackupsNumberToKeepHelp": "Apenas 1 backup será removido por vez, então, se já existem mais backups, você deve apagá-los manualmente.",
- "LabelBitrate": "Bitrate",
"LabelBooks": "Livros",
"LabelButtonText": "Texto do botão",
"LabelByAuthor": "por {0}",
@@ -241,7 +232,6 @@
"LabelChaptersFound": "capítulos encontrados",
"LabelClickForMoreInfo": "Clique para mais informações",
"LabelClosePlayer": "Fechar Reprodutor",
- "LabelCodec": "Codec",
"LabelCollapseSeries": "Fechar Série",
"LabelCollection": "Coleção",
"LabelCollections": "Coleções",
@@ -268,17 +258,13 @@
"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",
- "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Remetente",
"LabelEmailSettingsRejectUnauthorized": "Rejeitar certificados não autorizados",
"LabelEmailSettingsRejectUnauthorizedHelp": "Desativar a validação de certificados SSL pode expor sua conexão a riscos de segurança, como ataques \"man-in-the-middle\". Desative essa opção apenas se entender suas consequências e se puder confiar no servidor de email ao qual você está se conectando.",
@@ -319,7 +305,6 @@
"LabelHasEbook": "Tem ebook",
"LabelHasSupplementaryEbook": "Tem ebook complementar",
"LabelHighestPriority": "Prioridade mais alta",
- "LabelHost": "Host",
"LabelHour": "Hora",
"LabelIcon": "Ícone",
"LabelImageURLFromTheWeb": "URL da imagem na internet",
@@ -336,7 +321,6 @@
"LabelIntervalEveryDay": "Todo dia",
"LabelIntervalEveryHour": "Toda hora",
"LabelInvert": "Inverter",
- "LabelItem": "Item",
"LabelLanguage": "Idioma",
"LabelLanguageDefaultServer": "Idioma Padrão do Servidor",
"LabelLanguages": "Idiomas",
@@ -345,20 +329,16 @@
"LabelLastSeen": "Visto pela Última Vez",
"LabelLastTime": "Progresso",
"LabelLastUpdate": "Última Atualização",
- "LabelLayout": "Layout",
"LabelLayoutSinglePage": "Uma página",
"LabelLayoutSplitPage": "Página dividida",
"LabelLess": "Menos",
"LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário",
"LabelLibrary": "Biblioteca",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Item da Biblioteca",
"LabelLibraryName": "Nome da Biblioteca",
"LabelLimit": "Limite",
"LabelLineSpacing": "Espaçamento entre linhas",
"LabelListenAgain": "Escutar novamente",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Atenção",
"LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data",
"LabelLowestPriority": "Prioridade mais baixa",
@@ -420,12 +400,9 @@
"LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})",
"LabelPhotoPathURL": "Caminho/URL para Foto",
"LabelPlayMethod": "Método de Reprodução",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Listas de Reprodução",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Região de busca do podcast",
"LabelPodcastType": "Tipo de Podcast",
- "LabelPodcasts": "Podcasts",
"LabelPort": "Porta",
"LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)",
"LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes e Google",
@@ -511,7 +488,6 @@
"LabelShowSeconds": "Exibir segundos",
"LabelSize": "Tamanho",
"LabelSleepTimer": "Timer",
- "LabelSlug": "Slug",
"LabelStart": "Iniciar",
"LabelStartTime": "Horário do Início",
"LabelStarted": "Iniciado",
@@ -539,7 +515,6 @@
"LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário",
"LabelTasks": "Tarefas em Execuçào",
"LabelTextEditorBulletedList": "Lista com marcadores",
- "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Lista numerada",
"LabelTextEditorUnlink": "Remover link",
"LabelTheme": "Tema",
@@ -586,7 +561,6 @@
"LabelViewBookmarks": "Ver marcadores",
"LabelViewChapters": "Ver capítulos",
"LabelViewQueue": "Ver fila do reprodutor",
- "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Dias da semana para executar",
"LabelYearReviewHide": "Ocultar Retrospectiva Anual",
"LabelYearReviewShow": "Exibir Retrospectiva Anual",
@@ -692,7 +666,6 @@
"MessageNoSeries": "Sem Séries",
"MessageNoTags": "Sem etiquetas",
"MessageNoTasksRunning": "Sem Tarefas em Execução",
- "MessageNoUpdateNecessary": "Não é necessária a atualização",
"MessageNoUpdatesWereNecessary": "Nenhuma atualização é necessária",
"MessageNoUserPlaylists": "Você não tem listas de reprodução",
"MessageNotYetImplemented": "Ainda não implementado",
@@ -739,7 +712,6 @@
"PlaceholderSearchEpisode": "Buscar Episódio..",
"ToastAccountUpdateFailed": "Falha ao atualizar a conta",
"ToastAccountUpdateSuccess": "Conta atualizada",
- "ToastAuthorImageRemoveFailed": "Falha ao remover imagem",
"ToastAuthorImageRemoveSuccess": "Imagem do autor removida",
"ToastAuthorUpdateFailed": "Falha ao atualizar o autor",
"ToastAuthorUpdateMerged": "Autor combinado",
@@ -756,7 +728,6 @@
"ToastBatchUpdateSuccess": "Atualização em lote realizada",
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
"ToastBookmarkCreateSuccess": "Marcador adicionado",
- "ToastBookmarkRemoveFailed": "Falha ao remover marcador",
"ToastBookmarkRemoveSuccess": "Marcador removido",
"ToastBookmarkUpdateFailed": "Falha ao atualizar o marcador",
"ToastBookmarkUpdateSuccess": "Marcador atualizado",
@@ -764,9 +735,7 @@
"ToastCachePurgeSuccess": "Cache apagado com sucesso",
"ToastChaptersHaveErrors": "Capítulos com erro",
"ToastChaptersMustHaveTitles": "Capítulos precisam ter títulos",
- "ToastCollectionItemsRemoveFailed": "Falha ao remover item(ns) da coleção",
"ToastCollectionItemsRemoveSuccess": "Item(ns) removidos da coleção",
- "ToastCollectionRemoveFailed": "Falha ao remover coleção",
"ToastCollectionRemoveSuccess": "Coleção removida",
"ToastCollectionUpdateFailed": "Falha ao atualizar coleção",
"ToastCollectionUpdateSuccess": "Coleção atualizada",
@@ -777,7 +746,6 @@
"ToastItemCoverUpdateSuccess": "Capa do item atualizada",
"ToastItemDetailsUpdateFailed": "Falha ao atualizar detalhes do item",
"ToastItemDetailsUpdateSuccess": "Detalhes do item atualizados",
- "ToastItemDetailsUpdateUnneeded": "Nenhuma atualização necessária para os detalhes do item",
"ToastItemMarkedAsFinishedFailed": "Falha ao marcar como Concluído",
"ToastItemMarkedAsFinishedSuccess": "Item marcado como Concluído",
"ToastItemMarkedAsNotFinishedFailed": "Falha ao marcar como Não Concluído",
@@ -792,7 +760,6 @@
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada",
"ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução",
"ToastPlaylistCreateSuccess": "Lista de reprodução criada",
- "ToastPlaylistRemoveFailed": "Falha ao remover lista de reprodução",
"ToastPlaylistRemoveSuccess": "Lista de reprodução removida",
"ToastPlaylistUpdateFailed": "Falha ao atualizar lista de reprodução",
"ToastPlaylistUpdateSuccess": "Lista de reprodução atualizada",
diff --git a/client/strings/ru.json b/client/strings/ru.json
index f0cf560046..374ad87976 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.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": "Скрыть",
@@ -44,21 +48,24 @@
"ButtonMatchAllAuthors": "Найти всех авторов",
"ButtonMatchBooks": "Найти книги",
"ButtonNevermind": "Не важно",
- "ButtonNext": "Next",
+ "ButtonNext": "Следующий",
"ButtonNextChapter": "Следующая глава",
+ "ButtonNextItemInQueue": "Следующий элемент в очереди",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Открыть канал",
"ButtonOpenManager": "Открыть менеджер",
- "ButtonPause": "Pause",
+ "ButtonPause": "Пауза",
"ButtonPlay": "Слушать",
"ButtonPlaying": "Проигрывается",
"ButtonPlaylists": "Плейлисты",
"ButtonPrevious": "Предыдущий",
"ButtonPreviousChapter": "Предыдущая глава",
+ "ButtonProbeAudioFile": "Сканирование аудиофайла",
"ButtonPurgeAllCache": "Очистить весь кэш",
"ButtonPurgeItemsCache": "Очистить кэш элементов",
"ButtonQueueAddItem": "Добавить в очередь",
"ButtonQueueRemoveItem": "Удалить из очереди",
+ "ButtonQuickEmbedMetadata": "Быстрое встраивание метаданных",
"ButtonQuickMatch": "Быстрый поиск",
"ButtonReScan": "Пересканировать",
"ButtonRead": "Читать",
@@ -88,6 +95,7 @@
"ButtonShow": "Показать",
"ButtonStartM4BEncode": "Начать кодирование M4B",
"ButtonStartMetadataEmbed": "Начать встраивание метаданных",
+ "ButtonStats": "Статистика",
"ButtonSubmit": "Применить",
"ButtonTest": "Тест",
"ButtonUpload": "Загрузить",
@@ -102,6 +110,7 @@
"ErrorUploadFetchMetadataNoResults": "Не удалось получить метаданные - попробуйте обновить название и/или автора",
"ErrorUploadLacksTitle": "Название должно быть заполнено",
"HeaderAccount": "Учетная запись",
+ "HeaderAddCustomMetadataProvider": "Добавление пользовательского поставщика метаданных",
"HeaderAdvanced": "Дополнительно",
"HeaderAppriseNotificationSettings": "Настройки оповещений",
"HeaderAudioTracks": "Аудио треки",
@@ -120,7 +129,7 @@
"HeaderDetails": "Подробности",
"HeaderDownloadQueue": "Очередь скачивания",
"HeaderEbookFiles": "Файлы e-книг",
- "HeaderEmail": "Email",
+ "HeaderEmail": "E-mail",
"HeaderEmailSettings": "Настройки Email",
"HeaderEpisodes": "Эпизоды",
"HeaderEreaderDevices": "Устройства E-книга",
@@ -147,6 +156,8 @@
"HeaderMetadataToEmbed": "Метаинформация для встраивания",
"HeaderNewAccount": "Новая учетная запись",
"HeaderNewLibrary": "Новая библиотека",
+ "HeaderNotificationCreate": "Создать уведомление",
+ "HeaderNotificationUpdate": "Уведомление об обновлении",
"HeaderNotifications": "Уведомления",
"HeaderOpenIDConnectAuthentication": "Аутентификация OpenID Connect",
"HeaderOpenRSSFeed": "Открыть RSS-канал",
@@ -154,6 +165,7 @@
"HeaderPasswordAuthentication": "Аутентификация по паролю",
"HeaderPermissions": "Разрешения",
"HeaderPlayerQueue": "Очередь воспроизведения",
+ "HeaderPlayerSettings": "Настройки плеера",
"HeaderPlaylist": "Плейлист",
"HeaderPlaylistItems": "Элементы списка воспроизведения",
"HeaderPodcastsToAdd": "Подкасты для добавления",
@@ -202,8 +214,8 @@
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
"LabelAddToPlaylist": "Добавить в плейлист",
"LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист",
- "LabelAdded": "Добавили",
"LabelAddedAt": "Дата добавления",
+ "LabelAddedDate": "Добавлено {0}",
"LabelAdminUsersOnly": "Только для пользователей с правами администратора",
"LabelAll": "Все",
"LabelAllUsers": "Все пользователи",
@@ -226,14 +238,14 @@
"LabelBackupLocation": "Путь для бэкапов",
"LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование",
"LabelBackupsEnableAutomaticBackupsHelp": "Бэкапы сохраняются в /metadata/backups",
- "LabelBackupsMaxBackupSize": "Максимальный размер бэкапа (в GB)",
+ "LabelBackupsMaxBackupSize": "Максимальный размер бэкапа (в GB) (0 для неограниченного лимита)",
"LabelBackupsMaxBackupSizeHelp": "В качестве защиты процесс бэкапирования будет завершаться ошибкой, если будет превышен настроенный размер.",
"LabelBackupsNumberToKeep": "Сохранять бэкапов",
"LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.",
"LabelBitrate": "Битрейт",
"LabelBooks": "Книги",
"LabelButtonText": "Текст кнопки",
- "LabelByAuthor": "by {0}",
+ "LabelByAuthor": "{0}",
"LabelChangePassword": "Изменить пароль",
"LabelChannels": "Каналы",
"LabelChapterTitle": "Название главы",
@@ -243,6 +255,7 @@
"LabelClosePlayer": "Закрыть проигрыватель",
"LabelCodec": "Кодек",
"LabelCollapseSeries": "Свернуть серии",
+ "LabelCollapseSubSeries": "Свернуть подсерию",
"LabelCollection": "Коллекция",
"LabelCollections": "Коллекции",
"LabelComplete": "Завершить",
@@ -258,6 +271,7 @@
"LabelCurrently": "Текущее:",
"LabelCustomCronExpression": "Пользовательское выражение Cron:",
"LabelDatetime": "Дата и время",
+ "LabelDays": "Дней",
"LabelDeleteFromFileSystemCheckbox": "Удалить из файловой системы (снимите флажок, чтобы удалить только из базы данных)",
"LabelDescription": "Описание",
"LabelDeselectAll": "Снять выделение",
@@ -278,7 +292,7 @@
"LabelEbook": "E-книга",
"LabelEbooks": "E-книги",
"LabelEdit": "Редактировать",
- "LabelEmail": "Email",
+ "LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Адрес От",
"LabelEmailSettingsRejectUnauthorized": "Отклонение неавторизованных сертификатов",
"LabelEmailSettingsRejectUnauthorizedHelp": "Отключение проверки SSL-сертификата может подвергнуть ваше подключение рискам безопасности, таким как атаки типа \"man-in-the-middle\". Отключайте эту опцию только в том случае, если вы понимаете последствия и доверяете почтовому серверу, к которому подключаетесь.",
@@ -288,18 +302,25 @@
"LabelEmbeddedCover": "Встроенная обложка",
"LabelEnable": "Включить",
"LabelEnd": "Конец",
+ "LabelEndOfChapter": "Конец главы",
"LabelEpisode": "Эпизод",
"LabelEpisodeTitle": "Имя эпизода",
"LabelEpisodeType": "Тип эпизода",
+ "LabelEpisodes": "Эпизодов",
"LabelExample": "Пример",
+ "LabelExpandSeries": "Развернуть серию",
+ "LabelExpandSubSeries": "Развернуть подсерию",
"LabelExplicit": "Явный",
"LabelExplicitChecked": "Явный (отмечено)",
"LabelExplicitUnchecked": "Не явно (не отмечено)",
+ "LabelExportOPML": "Экспорт OPML",
"LabelFeedURL": "URL канала",
"LabelFetchingMetadata": "Извлечение метаданных",
"LabelFile": "Файл",
"LabelFileBirthtime": "Дата создания",
+ "LabelFileBornDate": "Родился {0}",
"LabelFileModified": "Дата модификации",
+ "LabelFileModifiedDate": "Изменено {0}",
"LabelFilename": "Имя файла",
"LabelFilterByUser": "Фильтр по пользователю",
"LabelFindEpisodes": "Найти эпизоды",
@@ -309,7 +330,7 @@
"LabelFontBold": "Жирный",
"LabelFontBoldness": "Жирность шрифта",
"LabelFontFamily": "Семейство шрифтов",
- "LabelFontItalic": "Italic",
+ "LabelFontItalic": "Курсив",
"LabelFontScale": "Масштаб шрифта",
"LabelFontStrikethrough": "Зачеркнутый",
"LabelFormat": "Формат",
@@ -318,9 +339,11 @@
"LabelHardDeleteFile": "Жесткое удаление файла",
"LabelHasEbook": "Есть e-книга",
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
+ "LabelHideSubtitles": "Скрыть субтитры",
"LabelHighestPriority": "Наивысший приоритет",
"LabelHost": "Хост",
"LabelHour": "Часы",
+ "LabelHours": "Часов",
"LabelIcon": "Иконка",
"LabelImageURLFromTheWeb": "URL-адрес изображения из Интернета",
"LabelInProgress": "В процессе",
@@ -337,6 +360,8 @@
"LabelIntervalEveryHour": "Каждый час",
"LabelInvert": "Инвертировать",
"LabelItem": "Элемент",
+ "LabelJumpBackwardAmount": "Прыжок назад на величину",
+ "LabelJumpForwardAmount": "Прыжок вперед на величину",
"LabelLanguage": "Язык",
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
"LabelLanguages": "Языки",
@@ -351,7 +376,7 @@
"LabelLess": "Менее",
"LabelLibrariesAccessibleToUser": "Библиотеки доступные для пользователя",
"LabelLibrary": "Библиотека",
- "LabelLibraryFilterSublistEmpty": "No {0}",
+ "LabelLibraryFilterSublistEmpty": "Нет {0}",
"LabelLibraryItem": "Элемент библиотеки",
"LabelLibraryName": "Имя библиотеки",
"LabelLimit": "Лимит",
@@ -371,6 +396,7 @@
"LabelMetadataOrderOfPrecedenceDescription": "Источники метаданных с более высоким приоритетом будут переопределять источники метаданных с более низким приоритетом",
"LabelMetadataProvider": "Провайдер",
"LabelMinute": "Минуты",
+ "LabelMinutes": "Минуты",
"LabelMissing": "Потеряно",
"LabelMissingEbook": "Нет e-книги",
"LabelMissingSupplementaryEbook": "Нет дополнительной e-книги",
@@ -410,6 +436,7 @@
"LabelOverwrite": "Перезаписать",
"LabelPassword": "Пароль",
"LabelPath": "Путь",
+ "LabelPermanent": "Постоянный",
"LabelPermissionsAccessAllLibraries": "Есть доступ ко всем библиотекам",
"LabelPermissionsAccessAllTags": "Есть доступ ко всем тегам",
"LabelPermissionsAccessExplicitContent": "Есть доступ к явному содержимому",
@@ -420,7 +447,7 @@
"LabelPersonalYearReview": "Итоги прошедшего года ({0})",
"LabelPhotoPathURL": "Путь к фото/URL",
"LabelPlayMethod": "Метод воспроизведения",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
+ "LabelPlayerChapterNumberMarker": "{0} из {1}",
"LabelPlaylists": "Плейлисты",
"LabelPodcast": "Подкаст",
"LabelPodcastSearchRegion": "Регион поиска подкастов",
@@ -432,8 +459,10 @@
"LabelPrimaryEbook": "Основная e-книга",
"LabelProgress": "Прогресс",
"LabelProvider": "Провайдер",
+ "LabelProviderAuthorizationValue": "Значение заголовка авторизации",
"LabelPubDate": "Дата публикации",
"LabelPublishYear": "Год публикации",
+ "LabelPublishedDate": "Опубликовано {0}",
"LabelPublisher": "Издатель",
"LabelPublishers": "Издатели",
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
@@ -442,6 +471,8 @@
"LabelRSSFeedPreventIndexing": "Запретить индексирование",
"LabelRSSFeedSlug": "Встроить RSS-канал",
"LabelRSSFeedURL": "URL RSS-канала",
+ "LabelRandomly": "Случайно",
+ "LabelReAddSeriesToContinueListening": "Повторно добавить серию в «Продолжить слушать»",
"LabelRead": "Читать",
"LabelReadAgain": "Читать снова",
"LabelReadEbookWithoutProgress": "Читать e-книгу без сохранения прогресса",
@@ -470,7 +501,7 @@
"LabelSetEbookAsPrimary": "Установить как основную",
"LabelSetEbookAsSupplementary": "Установить как дополнительную",
"LabelSettingsAudiobooksOnly": "Только аудиокниги",
- "LabelSettingsAudiobooksOnlyHelp": "Если включить эту настройку, файлы электронных книг будут игнорироваться, за исключением случаев, когда они находятся в папке с аудиокнигами, в этом случае они будут рассматриваться как дополнительные электронные книги.",
+ "LabelSettingsAudiobooksOnlyHelp": "Если включить эту настройку, файлы электронных книг будут игнорироваться, за исключением случаев, когда они находятся в папке с аудиокнигами, в этом случае они будут рассматриваться как дополнительные электронные книги",
"LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками",
"LabelSettingsChromecastSupport": "Поддержка Chromecast",
"LabelSettingsDateFormat": "Формат даты",
@@ -495,7 +526,7 @@
"LabelSettingsParseSubtitles": "Разбор подзаголовков",
"LabelSettingsParseSubtitlesHelp": "Извлечение подзаголовков из имен папок аудиокниг.
Подзаголовок должны быть отделен \" - \"
например \"Название Книги - Тут Подзаголовок\" подзаголовок будет \"Тут Подзаголовок\"",
"LabelSettingsPreferMatchedMetadata": "Предпочитать метаданные поиска",
- "LabelSettingsPreferMatchedMetadataHelp": "Данные поиска будут перезаписывать данные книг при использовании Быстрого Поиска. По умолчанию Быстрый Поиск будет использоваться только при отсутствии данных",
+ "LabelSettingsPreferMatchedMetadataHelp": "Данные поиска будут перезаписывать данные книг при использовании Быстрого Поиска. По умолчанию Быстрый Поиск будет использоваться только при отсутствии данных.",
"LabelSettingsSkipMatchingBooksWithASIN": "Пропускать Поиск книг у которых уже заполнен ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Пропускать Поиск книг у которых уже заполнен ISBN",
"LabelSettingsSortingIgnorePrefixes": "Игнорировать префиксы при сортировке",
@@ -507,8 +538,12 @@
"LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом",
"LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента",
"LabelSettingsTimeFormat": "Формат времени",
+ "LabelShare": "Поделиться",
+ "LabelShareOpen": "Общедоступно",
+ "LabelShareURL": "Общедоступный URL",
"LabelShowAll": "Показать все",
"LabelShowSeconds": "Отображать секунды",
+ "LabelShowSubtitles": "Показать субтитры",
"LabelSize": "Размер",
"LabelSleepTimer": "Таймер сна",
"LabelSlug": "Слизень",
@@ -539,13 +574,17 @@
"LabelTagsNotAccessibleToUser": "Теги не доступные для пользователя",
"LabelTasks": "Запущенные задачи",
"LabelTextEditorBulletedList": "Маркированный список",
- "LabelTextEditorLink": "Link",
+ "LabelTextEditorLink": "Связь",
"LabelTextEditorNumberedList": "Нумерованный список",
"LabelTextEditorUnlink": "Отсоединить",
"LabelTheme": "Тема",
"LabelThemeDark": "Темная",
"LabelThemeLight": "Светлая",
"LabelTimeBase": "Временная база",
+ "LabelTimeDurationXHours": "{0} часов",
+ "LabelTimeDurationXMinutes": "{0} минут",
+ "LabelTimeDurationXSeconds": "{0} секунд",
+ "LabelTimeInMinutes": "Время в минутах",
"LabelTimeListened": "Время прослушивания",
"LabelTimeListenedToday": "Время прослушивания сегодня",
"LabelTimeRemaining": "{0} осталось",
@@ -569,6 +608,7 @@
"LabelUnabridged": "Полное издание",
"LabelUndo": "Отменить",
"LabelUnknown": "Неизвестно",
+ "LabelUnknownPublishDate": "Дата публикации неизвестна",
"LabelUpdateCover": "Обновить обложку",
"LabelUpdateCoverHelp": "Позволяет перезаписывать существующие обложки для выбранных книг если будут найдены",
"LabelUpdateDetails": "Обновить подробности",
@@ -585,9 +625,12 @@
"LabelVersion": "Версия",
"LabelViewBookmarks": "Закладки",
"LabelViewChapters": "Главы",
+ "LabelViewPlayerSettings": "Просмотр настроек плеера",
"LabelViewQueue": "Очередь воспроизведения",
"LabelVolume": "Громкость",
"LabelWeekdaysToRun": "Дни недели для запуска",
+ "LabelXBooks": "{0} книг",
+ "LabelXItems": "{0} элементов",
"LabelYearReviewHide": "Скрыть итоги года",
"LabelYearReviewShow": "Итоги года",
"LabelYourAudiobookDuration": "Продолжительность Вашей книги",
@@ -597,6 +640,9 @@
"MessageAddToPlayerQueue": "Добавить в очередь проигрывателя",
"MessageAppriseDescription": "Для использования этой функции необходимо иметь запущенный экземпляр
Apprise API или api которое обрабатывает те же самые запросы.
URL-адрес API Apprise должен быть полным URL-адресом для отправки уведомления, т.е., если API запущено по адресу
http://192.168.1.1:8337
тогда нужно указать
http://192.168.1.1:8337/notify
.",
"MessageBackupsDescription": "Бэкап включает пользователей, прогресс пользователей, данные элементов библиотеки, настройки сервера и изображения хранящиеся в
/metadata/items
и
/metadata/authors
. Бэкапы
НЕ сохраняют файлы из папок библиотек.",
+ "MessageBackupsLocationEditNote": "Примечание: Обновление местоположения резервной копии не приведет к перемещению или изменению существующих резервных копий",
+ "MessageBackupsLocationNoEditNote": "Примечание: Местоположение резервного копирования задается с помощью переменной среды и не может быть изменено здесь.",
+ "MessageBackupsLocationPathEmpty": "Путь к расположению резервной копии не может быть пустым",
"MessageBatchQuickMatchDescription": "Быстрый Поиск попытается добавить отсутствующие обложки и метаданные для выбранных элементов. Включите параметры ниже, чтобы разрешить Быстрому Поиску перезаписывать существующие обложки и/или метаданные.",
"MessageBookshelfNoCollections": "Вы еще не создали ни одной коллекции",
"MessageBookshelfNoRSSFeeds": "Нет открытых RSS-каналов",
@@ -611,16 +657,22 @@
"MessageCheckingCron": "Проверка cron...",
"MessageConfirmCloseFeed": "Вы уверены, что хотите закрыть этот канал?",
"MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?",
+ "MessageConfirmDeleteDevice": "Вы уверены, что хотите удалить устройство для чтения электронных книг \"{0}\"?",
"MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?",
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Это приведет к удалению элемента библиотеки из базы данных и файловой системы. Вы уверены?",
"MessageConfirmDeleteLibraryItems": "Это приведет к удалению {0} элементов библиотеки из базы данных и файловой системы. Вы уверены?",
+ "MessageConfirmDeleteMetadataProvider": "Вы уверены, что хотите удалить пользовательский поставщик метаданных \"{0}\"?",
+ "MessageConfirmDeleteNotification": "Вы уверены, что хотите удалить это уведомление?",
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?",
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?",
+ "MessageConfirmMarkItemFinished": "Вы уверены, что хотите отметить «{0}» как завершенную?",
+ "MessageConfirmMarkItemNotFinished": "Вы уверены, что хотите отметить «{0}» как не завершенную?",
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?",
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?",
+ "MessageConfirmNotificationTestTrigger": "Активировать это уведомление с тестовыми данными?",
"MessageConfirmPurgeCache": "Очистка кэша удалит весь каталог в
/metadata/cache
.
Вы уверены, что хотите удалить каталог кэша?",
"MessageConfirmPurgeItemsCache": "Очистка кэша элементов удалит весь каталог в
/metadata/cache/items
.
Вы уверены?",
"MessageConfirmQuickEmbed": "Предупреждение! Быстрое встраивание не позволяет создавать резервные копии аудиофайлов. Убедитесь, что у вас есть резервная копия аудиофайлов.
Хотите продолжить?",
@@ -639,9 +691,12 @@
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
+ "MessageConfirmResetProgress": "Вы уверены, что хотите сбросить свой прогресс?",
"MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?",
+ "MessageConfirmUnlinkOpenId": "Вы уверены, что хотите отвязать этого пользователя от OpenID?",
"MessageDownloadingEpisode": "Эпизод скачивается",
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
+ "MessageEmbedFailed": "Вставка не удалась!",
"MessageEmbedFinished": "Встраивание завершено!",
"MessageEpisodesQueuedForDownload": "{0} Эпизод(ов) запланировано для закачки",
"MessageEreaderDevices": "Чтобы обеспечить доставку электронных книг, вам может потребоваться добавить указанный выше адрес электронной почты в качестве действительного отправителя для каждого устройства, перечисленного ниже.",
@@ -673,6 +728,7 @@
"MessageNoCollections": "Нет коллекций",
"MessageNoCoversFound": "Обложек не найдено",
"MessageNoDescription": "Нет описания",
+ "MessageNoDevices": "Нет устройств",
"MessageNoDownloadsInProgress": "В настоящее время загрузка не выполняется",
"MessageNoDownloadsQueued": "Нет загрузок в очереди",
"MessageNoEpisodeMatchesFound": "Совпадения эпизодов не найдены",
@@ -692,14 +748,15 @@
"MessageNoSeries": "Нет серий",
"MessageNoTags": "Нет тегов",
"MessageNoTasksRunning": "Нет выполняемых задач",
- "MessageNoUpdateNecessary": "Обновление не требуется",
"MessageNoUpdatesWereNecessary": "Обновления не требовались",
"MessageNoUserPlaylists": "У вас нет плейлистов",
"MessageNotYetImplemented": "Пока не реализовано",
+ "MessageOpmlPreviewNote": "Примечание: Это предварительный просмотр разобранного файла OPML. Фактическое название подкаста будет взято из RSS-канала.",
"MessageOr": "или",
"MessagePauseChapter": "Пауза воспроизведения главы",
"MessagePlayChapter": "Прослушать начало главы",
"MessagePlaylistCreateFromCollection": "Создать плейлист из коллекции",
+ "MessagePleaseWait": "Пожалуйста подождите...",
"MessagePodcastHasNoRSSFeedForMatching": "Подкаст не имеет URL-адреса RSS-канала, который можно использовать для поиска",
"MessageQuickMatchDescription": "Заполняет пустые детали элемента и обложку первым результатом поиска из «{0}». Не перезаписывает сведения, если не включен параметр сервера 'Предпочитать метаданные поиска'.",
"MessageRemoveChapter": "Удалить главу",
@@ -714,6 +771,9 @@
"MessageSelected": "{0} выбрано",
"MessageServerCouldNotBeReached": "Не удалось связаться с сервером",
"MessageSetChaptersFromTracksDescription": "Установка глав с использованием каждого аудиофайла в качестве главы и заголовка главы в качестве имени аудиофайла",
+ "MessageShareExpirationWillBe": "Срок действия истекает
{0} ",
+ "MessageShareExpiresIn": "Срок действия истекает через {0}",
+ "MessageShareURLWillBe": "URL-адрес общего доступа будет
{0} ",
"MessageStartPlaybackAtTime": "Начать воспроизведение для \"{0}\" с {1}?",
"MessageThinking": "Думаю...",
"MessageUploaderItemFailed": "Не удалось загрузить",
@@ -737,26 +797,52 @@
"PlaceholderNewPlaylist": "Новое название плейлиста",
"PlaceholderSearch": "Поиск...",
"PlaceholderSearchEpisode": "Поиск эпизода...",
+ "StatsAuthorsAdded": "авторов добавлено",
+ "StatsBooksAdded": "книг добавлено",
+ "StatsBooksAdditional": "Некоторые дополнения включают в себя…",
+ "StatsBooksFinished": "книг завершено",
+ "StatsBooksFinishedThisYear": "Некоторые книги закончены в этом году…",
+ "StatsBooksListenedTo": "книг прослушано",
+ "StatsCollectionGrewTo": "Ваша коллекция книг пополнилась…",
+ "StatsSessions": "сессий",
+ "StatsSpentListening": "потрачено на прослушивание",
+ "StatsTopAuthor": "ТОП АВТОР",
+ "StatsTopAuthors": "ТОП АВТОРОВ",
+ "StatsTopGenre": "ТОП ЖАНР",
+ "StatsTopGenres": "ТОП ЖАНРЫ",
+ "StatsTopMonth": "ЛУЧШИЙ МЕСЯЦ",
+ "StatsTopNarrator": "ТОП ЧТЕЦ",
+ "StatsTopNarrators": "ТОП ЧТЕЦЫ",
+ "StatsTotalDuration": "С общей продолжительностью…",
+ "StatsYearInReview": "ИТОГИ ГОДА",
"ToastAccountUpdateFailed": "Не удалось обновить учетную запись",
"ToastAccountUpdateSuccess": "Учетная запись обновлена",
- "ToastAuthorImageRemoveFailed": "Не удалось удалить изображение",
+ "ToastAppriseUrlRequired": "Необходимо ввести URL-адрес Apprise",
"ToastAuthorImageRemoveSuccess": "Изображение автора удалено",
+ "ToastAuthorNotFound": "Автор \"{0}\" не найден",
+ "ToastAuthorRemoveSuccess": "Автор удален",
+ "ToastAuthorSearchNotFound": "Автор не найден",
"ToastAuthorUpdateFailed": "Не удалось обновить автора",
"ToastAuthorUpdateMerged": "Автор объединен",
"ToastAuthorUpdateSuccess": "Автор обновлен",
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновлен (изображение не найдено)",
+ "ToastBackupAppliedSuccess": "Применена резервная копия",
"ToastBackupCreateFailed": "Не удалось создать бэкап",
"ToastBackupCreateSuccess": "Бэкап создан",
"ToastBackupDeleteFailed": "Не удалось удалить бэкап",
"ToastBackupDeleteSuccess": "Бэкап удален",
+ "ToastBackupInvalidMaxKeep": "Недопустимое количество резервных копий для хранения",
+ "ToastBackupInvalidMaxSize": "Недопустимый максимальный размер резервной копии",
+ "ToastBackupPathUpdateFailed": "Не удалось обновить путь к резервному копированию",
"ToastBackupRestoreFailed": "Не удалось восстановить из бэкапа",
"ToastBackupUploadFailed": "Не удалось загрузить бэкап",
"ToastBackupUploadSuccess": "Бэкап загружен",
+ "ToastBatchDeleteFailed": "Не удалось выполнить пакетное удаление",
+ "ToastBatchDeleteSuccess": "Успешное пакетное удаление",
"ToastBatchUpdateFailed": "Сбой пакетного обновления",
"ToastBatchUpdateSuccess": "Успешное пакетное обновление",
"ToastBookmarkCreateFailed": "Не удалось создать закладку",
"ToastBookmarkCreateSuccess": "Добавлена закладка",
- "ToastBookmarkRemoveFailed": "Не удалось удалить закладку",
"ToastBookmarkRemoveSuccess": "Закладка удалена",
"ToastBookmarkUpdateFailed": "Не удалось обновить закладку",
"ToastBookmarkUpdateSuccess": "Закладка обновлена",
@@ -764,24 +850,46 @@
"ToastCachePurgeSuccess": "Кэш успешно очищен",
"ToastChaptersHaveErrors": "Главы имеют ошибки",
"ToastChaptersMustHaveTitles": "Главы должны содержать названия",
- "ToastCollectionItemsRemoveFailed": "Не удалось удалить элемент(ы) из коллекции",
+ "ToastChaptersRemoved": "Удалены главы",
+ "ToastCollectionItemsAddFailed": "Не удалось добавить элемент(ы) в коллекцию",
+ "ToastCollectionItemsAddSuccess": "Элемент(ы) добавлены в коллекцию",
"ToastCollectionItemsRemoveSuccess": "Элемент(ы), удалены из коллекции",
- "ToastCollectionRemoveFailed": "Не удалось удалить коллекцию",
"ToastCollectionRemoveSuccess": "Коллекция удалена",
"ToastCollectionUpdateFailed": "Не удалось обновить коллекцию",
"ToastCollectionUpdateSuccess": "Коллекция обновлена",
+ "ToastCoverUpdateFailed": "Не удалось обновить обложку",
"ToastDeleteFileFailed": "Не удалось удалить файл",
"ToastDeleteFileSuccess": "Файл удален",
+ "ToastDeviceAddFailed": "Не удалось добавить устройство",
+ "ToastDeviceNameAlreadyExists": "Устройство для чтения электронных книг с таким именем уже существует",
+ "ToastDeviceTestEmailFailed": "Не удалось отправить тестовое электронное письмо",
+ "ToastDeviceTestEmailSuccess": "Тестовое письмо отправлено",
+ "ToastDeviceUpdateFailed": "Не удалось обновить устройство",
+ "ToastEmailSettingsUpdateFailed": "Не удалось обновить настройки электронной почты",
+ "ToastEmailSettingsUpdateSuccess": "Обновлены настройки электронной почты",
+ "ToastEncodeCancelFailed": "Не удалось отменить кодирование",
+ "ToastEncodeCancelSucces": "Кодирование отменено",
+ "ToastEpisodeDownloadQueueClearFailed": "Не удалось очистить очередь",
+ "ToastEpisodeDownloadQueueClearSuccess": "Очередь загрузки эпизода очищена",
+ "ToastErrorCannotShare": "Невозможно предоставить общий доступ на этом устройстве",
"ToastFailedToLoadData": "Не удалось загрузить данные",
+ "ToastFailedToShare": "Не удалось поделиться",
+ "ToastFailedToUpdateAccount": "Не удалось обновить учетную запись",
+ "ToastFailedToUpdateUser": "Не удалось обновить пользователя",
+ "ToastInvalidImageUrl": "Неверный URL изображения",
+ "ToastInvalidUrl": "Неверный URL",
"ToastItemCoverUpdateFailed": "Не удалось обновить обложку элемента",
"ToastItemCoverUpdateSuccess": "Обложка элемента обновлена",
+ "ToastItemDeletedFailed": "Не удалось удалить элемент",
+ "ToastItemDeletedSuccess": "Удаленный элемент",
"ToastItemDetailsUpdateFailed": "Не удалось обновить сведения об элементе",
"ToastItemDetailsUpdateSuccess": "Обновлены сведения об элементе",
- "ToastItemDetailsUpdateUnneeded": "Для сведений об элементе не требуется никаких обновлений",
"ToastItemMarkedAsFinishedFailed": "Не удалось пометить как Завершенный",
"ToastItemMarkedAsFinishedSuccess": "Элемент помечен как Завершенный",
"ToastItemMarkedAsNotFinishedFailed": "Не удалось пометить как Незавершенный",
"ToastItemMarkedAsNotFinishedSuccess": "Элемент помечен как Незавершенный",
+ "ToastItemUpdateFailed": "Не удалось обновить элемент",
+ "ToastItemUpdateSuccess": "Элемент обновлен",
"ToastLibraryCreateFailed": "Не удалось создать библиотеку",
"ToastLibraryCreateSuccess": "Библиотека \"{0}\" создана",
"ToastLibraryDeleteFailed": "Не удалось удалить библиотеку",
@@ -790,32 +898,78 @@
"ToastLibraryScanStarted": "Запущено сканирование библиотеки",
"ToastLibraryUpdateFailed": "Не удалось обновить библиотеку",
"ToastLibraryUpdateSuccess": "Библиотека \"{0}\" обновлена",
+ "ToastNameEmailRequired": "Имя и адрес электронной почты обязательны",
+ "ToastNameRequired": "Имя обязательно для заполнения",
+ "ToastNewUserCreatedFailed": "Не удалось создать учетную запись: \"{0}\"",
+ "ToastNewUserCreatedSuccess": "Новая учетная запись создана",
+ "ToastNewUserLibraryError": "Необходимо выбрать хотя бы одну библиотеку",
+ "ToastNewUserPasswordError": "Должен иметь пароль, только пользователь root может иметь пустой пароль",
+ "ToastNewUserTagError": "Необходимо выбрать хотя бы один тег",
+ "ToastNewUserUsernameError": "Введите имя пользователя",
+ "ToastNoUpdatesNecessary": "Обновления не требуются",
+ "ToastNotificationCreateFailed": "Не удалось создать уведомление",
+ "ToastNotificationDeleteFailed": "Не удалось удалить уведомление",
+ "ToastNotificationFailedMaximum": "Максимальное количество неудачных попыток должно быть >= 0",
+ "ToastNotificationQueueMaximum": "Максимальная очередь уведомлений должна быть >= 0",
+ "ToastNotificationSettingsUpdateFailed": "Не удалось обновить настройки уведомлений",
+ "ToastNotificationSettingsUpdateSuccess": "Обновлены настройки уведомлений",
+ "ToastNotificationTestTriggerFailed": "Не удалось активировать тестовое уведомление",
+ "ToastNotificationTestTriggerSuccess": "Сработавшее уведомление о тестировании",
+ "ToastNotificationUpdateFailed": "Не удалось обновить уведомление",
+ "ToastNotificationUpdateSuccess": "Уведомление обновлено",
"ToastPlaylistCreateFailed": "Не удалось создать плейлист",
"ToastPlaylistCreateSuccess": "Плейлист создан",
- "ToastPlaylistRemoveFailed": "Не удалось удалить плейлист",
"ToastPlaylistRemoveSuccess": "Плейлист удален",
"ToastPlaylistUpdateFailed": "Не удалось обновить плейлист",
"ToastPlaylistUpdateSuccess": "Плейлист обновлен",
"ToastPodcastCreateFailed": "Не удалось создать подкаст",
"ToastPodcastCreateSuccess": "Подкаст успешно создан",
+ "ToastPodcastGetFeedFailed": "Не удалось получить ленту подкастов",
+ "ToastPodcastNoEpisodesInFeed": "В RSS-ленте эпизодов не найдено",
+ "ToastPodcastNoRssFeed": "В подкасте нет RSS-канала",
+ "ToastProviderCreatedFailed": "Не удалось добавить провайдера",
+ "ToastProviderCreatedSuccess": "Добавлен новый провайдер",
+ "ToastProviderNameAndUrlRequired": "Имя и URL обязательные",
+ "ToastProviderRemoveSuccess": "Провайдер удален",
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-канал",
"ToastRSSFeedCloseSuccess": "RSS-канал закрыт",
+ "ToastRemoveFailed": "Не удалось удалить",
"ToastRemoveItemFromCollectionFailed": "Не удалось удалить элемент из коллекции",
"ToastRemoveItemFromCollectionSuccess": "Элемент удален из коллекции",
+ "ToastRemoveItemsWithIssuesFailed": "Не удалось удалить элементы библиотеки с проблемами",
+ "ToastRemoveItemsWithIssuesSuccess": "Удалены элементы библиотеки с проблемами",
+ "ToastRenameFailed": "Не удалось переименовать",
+ "ToastRescanFailed": "Ошибка повторного сканирования для {0}",
+ "ToastRescanRemoved": "Повторное сканирование завершено, элемент был удален",
+ "ToastRescanUpToDate": "Повторное сканирование завершено, элемент был актуализирован",
+ "ToastRescanUpdated": "Повторное сканирование завершено, элемент был обновлен",
+ "ToastScanFailed": "Не удалось просканировать элемент библиотеки",
+ "ToastSelectAtLeastOneUser": "Выберите хотя бы одного пользователя",
"ToastSendEbookToDeviceFailed": "Не удалось отправить e-книгу на устройство",
"ToastSendEbookToDeviceSuccess": "E-книга отправлена на устройство \"{0}\"",
"ToastSeriesUpdateFailed": "Не удалось обновить серию",
"ToastSeriesUpdateSuccess": "Успешное обновление серии",
"ToastServerSettingsUpdateFailed": "Не удалось обновить настройки сервера",
"ToastServerSettingsUpdateSuccess": "Обновлены настройки сервера",
+ "ToastSessionCloseFailed": "Не удалось закрыть сеанс",
"ToastSessionDeleteFailed": "Не удалось удалить сеанс",
"ToastSessionDeleteSuccess": "Сеанс удален",
+ "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/sl.json b/client/strings/sl.json
new file mode 100644
index 0000000000..b6ae7f5219
--- /dev/null
+++ b/client/strings/sl.json
@@ -0,0 +1,976 @@
+{
+ "ButtonAdd": "Dodaj",
+ "ButtonAddChapters": "Dodaj poglavja",
+ "ButtonAddDevice": "Dodaj napravo",
+ "ButtonAddLibrary": "Dodaj knjižnico",
+ "ButtonAddPodcasts": "Dodaj podcast",
+ "ButtonAddUser": "Dodaj uporabnika",
+ "ButtonAddYourFirstLibrary": "Dodajte svojo prvo knjižnico",
+ "ButtonApply": "Uveljavi",
+ "ButtonApplyChapters": "Uveljavi poglavja",
+ "ButtonAuthors": "Avtorji",
+ "ButtonBack": "Nazaj",
+ "ButtonBrowseForFolder": "Prebrskaj pot do mape",
+ "ButtonCancel": "Prekliči",
+ "ButtonCancelEncode": "Prekliči prekodiranje",
+ "ButtonChangeRootPassword": "Zamenjaj korensko geslo",
+ "ButtonCheckAndDownloadNewEpisodes": "Preveri in prenesi nove epizode",
+ "ButtonChooseAFolder": "Izberite mapo",
+ "ButtonChooseFiles": "Izberite datoteke",
+ "ButtonClearFilter": "Počisti filter",
+ "ButtonCloseFeed": "Zapri vir",
+ "ButtonCloseSession": "Zapri odprto sejo",
+ "ButtonCollections": "Zbirke",
+ "ButtonConfigureScanner": "Nastavi pregledovalnik",
+ "ButtonCreate": "Ustvari",
+ "ButtonCreateBackup": "Ustvari varnostno kopijo",
+ "ButtonDelete": "Izbriši",
+ "ButtonDownloadQueue": "Čakalna vrsta",
+ "ButtonEdit": "Uredi",
+ "ButtonEditChapters": "Uredi poglavja",
+ "ButtonEditPodcast": "Uredi podcast",
+ "ButtonEnable": "Omogoči",
+ "ButtonFireAndFail": "Zaženi in je bilo neuspešno",
+ "ButtonFireOnTest": "Zaženi samo na dogodku onTest",
+ "ButtonForceReScan": "Prisilno ponovno pregledovanje",
+ "ButtonFullPath": "Polna pot",
+ "ButtonHide": "Skrij",
+ "ButtonHome": "Domov",
+ "ButtonIssues": "Težave",
+ "ButtonJumpBackward": "Skoči nazaj",
+ "ButtonJumpForward": "Skoči naprej",
+ "ButtonLatest": "Najnovejše",
+ "ButtonLibrary": "Knjižnica",
+ "ButtonLogout": "Odjava",
+ "ButtonLookup": "Iskanje",
+ "ButtonManageTracks": "Upravljaj s posnetki",
+ "ButtonMapChapterTitles": "Poveži naslove poglavij",
+ "ButtonMatchAllAuthors": "Ujemanje vseh avtorjev",
+ "ButtonMatchBooks": "Ujemanje knjig",
+ "ButtonNevermind": "Ni važno",
+ "ButtonNext": "Naslednje",
+ "ButtonNextChapter": "Naslednje poglavje",
+ "ButtonNextItemInQueue": "Naslednji element v čakalni vrsti",
+ "ButtonOk": "V redu",
+ "ButtonOpenFeed": "Odpri vir",
+ "ButtonOpenManager": "Odpri upravljanje",
+ "ButtonPause": "Premor",
+ "ButtonPlay": "Predvajaj",
+ "ButtonPlaying": "Predvajam",
+ "ButtonPlaylists": "Seznami predvajanj",
+ "ButtonPrevious": "Prejšnje",
+ "ButtonPreviousChapter": "Prejšnje poglavje",
+ "ButtonProbeAudioFile": "Analiziraj zvočno datoteko",
+ "ButtonPurgeAllCache": "Počisti ves predpomnilnik",
+ "ButtonPurgeItemsCache": "Počisti predpomnilnik elementov",
+ "ButtonQueueAddItem": "Dodaj v čakalno vrsto",
+ "ButtonQueueRemoveItem": "Odstrani iz čakalne vrste",
+ "ButtonQuickEmbedMetadata": "Hitra vdelava metapodatkov",
+ "ButtonQuickMatch": "Hitro ujemanje",
+ "ButtonReScan": "Ponovno pregledovanje",
+ "ButtonRead": "Preberi",
+ "ButtonReadLess": "Preberi manj",
+ "ButtonReadMore": "Preberi več",
+ "ButtonRefresh": "Osveži",
+ "ButtonRemove": "Odstrani",
+ "ButtonRemoveAll": "Odstrani vse",
+ "ButtonRemoveAllLibraryItems": "Odstrani vse elemente v knjižnici",
+ "ButtonRemoveFromContinueListening": "Odstrani iz nadaljuj poslušanje",
+ "ButtonRemoveFromContinueReading": "Odstrani iz nadaljuj branje",
+ "ButtonRemoveSeriesFromContinueSeries": "Odstrani serijo iz nadaljuj serijo",
+ "ButtonReset": "Ponastavi",
+ "ButtonResetToDefault": "Ponastavi na privzeto",
+ "ButtonRestore": "Obnovi",
+ "ButtonSave": "Shrani",
+ "ButtonSaveAndClose": "Shrani iz zapri",
+ "ButtonSaveTracklist": "Shrani seznam skladb",
+ "ButtonScan": "Pregledovanje",
+ "ButtonScanLibrary": "Preglej knjižnico",
+ "ButtonSearch": "Poišči",
+ "ButtonSelectFolderPath": "Izberite pot do mape",
+ "ButtonSeries": "Serije",
+ "ButtonSetChaptersFromTracks": "Nastavi poglavja za posnetke",
+ "ButtonShare": "Deli",
+ "ButtonShiftTimes": "Zamakni čase",
+ "ButtonShow": "Prikaži",
+ "ButtonStartM4BEncode": "Zaženi M4B prekodiranje",
+ "ButtonStartMetadataEmbed": "Začni vdelavo metapodatkov",
+ "ButtonStats": "Statistika",
+ "ButtonSubmit": "Posreduj",
+ "ButtonTest": "Test",
+ "ButtonUnlinkOpenId": "Prekini povezavo OpenID",
+ "ButtonUpload": "Naloži",
+ "ButtonUploadBackup": "Naloži varnostno kopijo",
+ "ButtonUploadCover": "Naloži naslovnico",
+ "ButtonUploadOPMLFile": "Naloži OPML datoteko",
+ "ButtonUserDelete": "Izbriši uporabnika {0}",
+ "ButtonUserEdit": "Uredi uporabnika {0}",
+ "ButtonViewAll": "Poglej vse",
+ "ButtonYes": "Da",
+ "ErrorUploadFetchMetadataAPI": "Napaka pri pridobivanju metapodatkov",
+ "ErrorUploadFetchMetadataNoResults": "Ni bilo mogoče pridobiti metapodatkov - poskusi posodobi naslov in/ali avtorja",
+ "ErrorUploadLacksTitle": "Imeti mora naslov",
+ "HeaderAccount": "Račun",
+ "HeaderAddCustomMetadataProvider": "Dodaj ponudnika metapodatkov po meri",
+ "HeaderAdvanced": "Napredno",
+ "HeaderAppriseNotificationSettings": "Nastavitve obvestil Apprise",
+ "HeaderAudioTracks": "Zvočni posnetki",
+ "HeaderAudiobookTools": "Orodja za upravljanje datotek zvočnih knjig",
+ "HeaderAuthentication": "Avtentikacija",
+ "HeaderBackups": "Varnostne kopije",
+ "HeaderChangePassword": "Zamenjaj geslo",
+ "HeaderChapters": "Poglavja",
+ "HeaderChooseAFolder": "Izberite mapo",
+ "HeaderCollection": "Zbirka",
+ "HeaderCollectionItems": "Elementi zbirke",
+ "HeaderCover": "Naslovnica",
+ "HeaderCurrentDownloads": "Trenutni prenosi",
+ "HeaderCustomMessageOnLogin": "Sporočilo po meri ob prijavi",
+ "HeaderCustomMetadataProviders": "Ponudniki metapodatkov po meri",
+ "HeaderDetails": "Podrobnosti",
+ "HeaderDownloadQueue": "Čakalna vrsta prenosa",
+ "HeaderEbookFiles": "Datoteke e-knjig",
+ "HeaderEmail": "E-pošta",
+ "HeaderEmailSettings": "Nastavitve e-pošte",
+ "HeaderEpisodes": "Epizode",
+ "HeaderEreaderDevices": "Ebralne naprave",
+ "HeaderEreaderSettings": "Nastavitve ebralnika",
+ "HeaderFiles": "Datoteke",
+ "HeaderFindChapters": "Najdi poglavja",
+ "HeaderIgnoredFiles": "Prezrte datoteke",
+ "HeaderItemFiles": "Datoteke elementa",
+ "HeaderItemMetadataUtils": "Pripomočki za metapodatke elementa",
+ "HeaderLastListeningSession": "Zadnja seja poslušanja",
+ "HeaderLatestEpisodes": "Zadnje epizode",
+ "HeaderLibraries": "Knjižnice",
+ "HeaderLibraryFiles": "Datoteke knjižnice",
+ "HeaderLibraryStats": "Statistika knjižnice",
+ "HeaderListeningSessions": "Seje poslušanja",
+ "HeaderListeningStats": "Statistika poslušanja",
+ "HeaderLogin": "Prijava",
+ "HeaderLogs": "Dnevniki",
+ "HeaderManageGenres": "Upravljajne žanrov",
+ "HeaderManageTags": "Upravljanje oznak",
+ "HeaderMapDetails": "Podrobnosti povezave",
+ "HeaderMatch": "Ujemanje",
+ "HeaderMetadataOrderOfPrecedence": "Vrstni red metapodatkov",
+ "HeaderMetadataToEmbed": "Metapodatki za vdelavo",
+ "HeaderNewAccount": "Nov račun",
+ "HeaderNewLibrary": "Nova knjižnica",
+ "HeaderNotificationCreate": "Ustvari obvestilo",
+ "HeaderNotificationUpdate": "Posodobi obvestilo",
+ "HeaderNotifications": "Obvestila",
+ "HeaderOpenIDConnectAuthentication": "Preverjanje pristnosti OpenID Connect",
+ "HeaderOpenRSSFeed": "Odpri vir RSS",
+ "HeaderOtherFiles": "Ostale datoteke",
+ "HeaderPasswordAuthentication": "Preverjanje pristnosti gesla",
+ "HeaderPermissions": "Dovoljenja",
+ "HeaderPlayerQueue": "Čakalna vrsta predvajalnika",
+ "HeaderPlayerSettings": "Nastavitve predvajalnika",
+ "HeaderPlaylist": "Seznam predvajanja",
+ "HeaderPlaylistItems": "Elementi seznama predvajanja",
+ "HeaderPodcastsToAdd": "Podcasti za dodajanje",
+ "HeaderPreviewCover": "Naslovnica za predogled",
+ "HeaderRSSFeedGeneral": "RSS podrobnosti",
+ "HeaderRSSFeedIsOpen": "Vir RSS je odprt",
+ "HeaderRSSFeeds": "RSS viri",
+ "HeaderRemoveEpisode": "Odstrani epizodo",
+ "HeaderRemoveEpisodes": "Odstrani {0} epizod",
+ "HeaderSavedMediaProgress": "Shranjen napredek predstavnosti",
+ "HeaderSchedule": "Načrtovanje",
+ "HeaderScheduleLibraryScans": "Načrtuj samodejno pregledovanje knjižnice",
+ "HeaderSession": "Seja",
+ "HeaderSetBackupSchedule": "Nastavite urnik varnostnega kopiranja",
+ "HeaderSettings": "Nastavitve",
+ "HeaderSettingsDisplay": "Zaslon",
+ "HeaderSettingsExperimental": "Eksperimentalne funkcije",
+ "HeaderSettingsGeneral": "Splošno",
+ "HeaderSettingsScanner": "Skener",
+ "HeaderSleepTimer": "Časovnik za izklop",
+ "HeaderStatsLargestItems": "Največji elementi",
+ "HeaderStatsLongestItems": "Najdaljši elementi (ure)",
+ "HeaderStatsMinutesListeningChart": "Minut poslušanja (zadnjih 7 dni)",
+ "HeaderStatsRecentSessions": "Nedavne seje",
+ "HeaderStatsTop10Authors": "Najboljših 10 avtorjev",
+ "HeaderStatsTop5Genres": "Najboljših 5 žanrov",
+ "HeaderTableOfContents": "Kazalo",
+ "HeaderTools": "Orodja",
+ "HeaderUpdateAccount": "Posodobi račun",
+ "HeaderUpdateAuthor": "Posodobi avtorja",
+ "HeaderUpdateDetails": "Posodobi podrobnosti",
+ "HeaderUpdateLibrary": "Posodobi knjižnico",
+ "HeaderUsers": "Uporabniki",
+ "HeaderYearReview": "Leto {0} v pregledu",
+ "HeaderYourStats": "Tvoja statistika",
+ "LabelAbridged": "Skrajšano",
+ "LabelAbridgedChecked": "Skrajšano (omogočeno)",
+ "LabelAbridgedUnchecked": "Neskrajšano (onemogočeno)",
+ "LabelAccessibleBy": "Dostopno iz",
+ "LabelAccountType": "Vrsta računa",
+ "LabelAccountTypeAdmin": "Administrator",
+ "LabelAccountTypeGuest": "Gost",
+ "LabelAccountTypeUser": "Uporabnik",
+ "LabelActivity": "Aktivnost",
+ "LabelAddToCollection": "Dodaj v zbirko",
+ "LabelAddToCollectionBatch": "Dodaj {0} knjig v zbirko",
+ "LabelAddToPlaylist": "Dodaj na seznam predvajanja",
+ "LabelAddToPlaylistBatch": "Dodaj {0} elementov v seznam predvajanja",
+ "LabelAddedAt": "Dodano ob",
+ "LabelAddedDate": "Dodano {0}",
+ "LabelAdminUsersOnly": "Samo administratorji",
+ "LabelAll": "Vsi",
+ "LabelAllUsers": "Vsi uporabniki",
+ "LabelAllUsersExcludingGuests": "Vsi uporabniki razen gosti",
+ "LabelAllUsersIncludingGuests": "Vsi uporabniki vključno z gosti",
+ "LabelAlreadyInYourLibrary": "Že v tvoji knjižnici",
+ "LabelAppend": "Priloži",
+ "LabelAuthor": "Avtor",
+ "LabelAuthorFirstLast": "Avtor (ime priimek)",
+ "LabelAuthorLastFirst": "Avtor (priimek, ime)",
+ "LabelAuthors": "Avtorji",
+ "LabelAutoDownloadEpisodes": "Samodejni prenos epizod",
+ "LabelAutoFetchMetadata": "Samodejno pridobivanje metapodatkov",
+ "LabelAutoFetchMetadataHelp": "Pridobi metapodatke za naslov, avtorja in serijo za poenostavitev nalaganja. Po nalaganju bo morda treba ujemanje dodatnih metapodatkov.",
+ "LabelAutoLaunch": "Samodejni zagon",
+ "LabelAutoLaunchDescription": "Samodejna preusmeritev na ponudnika avtentikacije ob navigaciji na prijavno stran (ročna preglasitev poti
/login?autoLaunch=0
)",
+ "LabelAutoRegister": "Samodejna registracija",
+ "LabelAutoRegisterDescription": "Po prijavi samodejno ustvari nove uporabnike",
+ "LabelBackToUser": "Nazaj na uporabnika",
+ "LabelBackupLocation": "Lokacija rezervne kopije",
+ "LabelBackupsEnableAutomaticBackups": "Omogoči samodejno varnostno kopiranje",
+ "LabelBackupsEnableAutomaticBackupsHelp": "Varnostne kopije shranjene v /metadata/backups",
+ "LabelBackupsMaxBackupSize": "Največja velikost varnostne kopije (v GB) (0 za neomejeno)",
+ "LabelBackupsMaxBackupSizeHelp": "Kot zaščita pred napačno konfiguracijo, varnostne kopije ne bodo uspele, če presežejo konfigurirano velikost.",
+ "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",
+ "LabelButtonText": "Besedilo gumba",
+ "LabelByAuthor": "od {0}",
+ "LabelChangePassword": "Spremeni geslo",
+ "LabelChannels": "Kanali",
+ "LabelChapterTitle": "Naslov poglavja",
+ "LabelChapters": "Poglavja",
+ "LabelChaptersFound": "najdenih poglavij",
+ "LabelClickForMoreInfo": "Klikni za več informacij",
+ "LabelClosePlayer": "Zapri predvajalnik",
+ "LabelCodec": "Kodek",
+ "LabelCollapseSeries": "Strni serije",
+ "LabelCollapseSubSeries": "Strni podserije",
+ "LabelCollection": "Zbirka",
+ "LabelCollections": "Zbirke",
+ "LabelComplete": "Končano",
+ "LabelConfirmPassword": "Potrdi geslo",
+ "LabelContinueListening": "Nadaljuj poslušanje",
+ "LabelContinueReading": "Nadaljuj branje",
+ "LabelContinueSeries": "Nadaljuj s serijo",
+ "LabelCover": "Naslovnica",
+ "LabelCoverImageURL": "URL naslovne slike",
+ "LabelCreatedAt": "Ustvarjeno ob",
+ "LabelCronExpression": "Cron izraz",
+ "LabelCurrent": "Trenutno",
+ "LabelCurrently": "Trenutno:",
+ "LabelCustomCronExpression": "Cron izraz po meri:",
+ "LabelDatetime": "Datum in ura",
+ "LabelDays": "Dnevi",
+ "LabelDeleteFromFileSystemCheckbox": "Izbriši iz datotečnega sistema (počisti polje, če želiš odstraniti samo iz zbirke podatkov)",
+ "LabelDescription": "Opis",
+ "LabelDeselectAll": "Odznači vse",
+ "LabelDevice": "Naprava",
+ "LabelDeviceInfo": "Podatki o napravi",
+ "LabelDeviceIsAvailableTo": "Naprava je na voljo za...",
+ "LabelDirectory": "Imenik",
+ "LabelDiscFromFilename": "Disk iz imena datoteke",
+ "LabelDiscFromMetadata": "Disk iz metapodatkov",
+ "LabelDiscover": "Odkrij",
+ "LabelDownload": "Prenos",
+ "LabelDownloadNEpisodes": "Prenesi {0} epizod",
+ "LabelDuration": "Trajanje",
+ "LabelDurationComparisonExactMatch": "(natančno ujemanje)",
+ "LabelDurationComparisonLonger": "({0} dlje)",
+ "LabelDurationComparisonShorter": "({0} krajše)",
+ "LabelDurationFound": "Najdeno trajanje:",
+ "LabelEbook": "E-knjiga",
+ "LabelEbooks": "E-knjige",
+ "LabelEdit": "Uredi",
+ "LabelEmail": "E-pošta",
+ "LabelEmailSettingsFromAddress": "Iz naslova",
+ "LabelEmailSettingsRejectUnauthorized": "Zavrni nepooblaščena potrdila",
+ "LabelEmailSettingsRejectUnauthorizedHelp": "Če onemogočite preverjanje veljavnosti potrdila SSL, lahko izpostavite svojo povezavo varnostnim tveganjem, kot so napadi človek v sredini. To možnost onemogočite le, če razumete posledice in zaupate poštnemu strežniku, s katerim se povezujete.",
+ "LabelEmailSettingsSecure": "Varno",
+ "LabelEmailSettingsSecureHelp": "Če je omogočeno, bo povezava pri povezovanju s strežnikom uporabljala TLS. Če je onemogočeno, se TLS uporablja, če strežnik podpira razširitev STARTTLS. V večini primerov nastavite to vrednost na omogočeno, če se povezujete z vrati 465. Za vrata 587 ali 25 naj ostane onemogočeno. (iz nodemailer.com/smtp/#authentication)",
+ "LabelEmailSettingsTestAddress": "Testiraj naslov",
+ "LabelEmbeddedCover": "Vdelana naslovnica",
+ "LabelEnable": "Omogoči",
+ "LabelEnd": "Konec",
+ "LabelEndOfChapter": "Konec poglavja",
+ "LabelEpisode": "Epizoda",
+ "LabelEpisodeTitle": "Naslov epizode",
+ "LabelEpisodeType": "Tip epizode",
+ "LabelEpisodes": "Epizode",
+ "LabelExample": "Primer",
+ "LabelExpandSeries": "Razširi serije",
+ "LabelExpandSubSeries": "Razširi podserije",
+ "LabelExplicit": "Eksplicitno",
+ "LabelExplicitChecked": "Eksplicitno (omogočeno)",
+ "LabelExplicitUnchecked": "Ne eksplicitno (onemogočeno)",
+ "LabelExportOPML": "Izvozi OPML",
+ "LabelFeedURL": "URL vir",
+ "LabelFetchingMetadata": "Pridobivam metapodatke",
+ "LabelFile": "Datoteka",
+ "LabelFileBirthtime": "Čas ustvarjanja datoteke",
+ "LabelFileBornDate": "Ustvarjena {0}",
+ "LabelFileModified": "Datoteke spremenjena",
+ "LabelFileModifiedDate": "Spremenjena {0}",
+ "LabelFilename": "Ime datoteke",
+ "LabelFilterByUser": "Filtriraj po uporabniku",
+ "LabelFindEpisodes": "Poišči epizode",
+ "LabelFinished": "Zaključeno",
+ "LabelFolder": "Mapa",
+ "LabelFolders": "Mape",
+ "LabelFontBold": "Krepko",
+ "LabelFontBoldness": "Krepkost pisave",
+ "LabelFontFamily": "Družina pisave",
+ "LabelFontItalic": "Ležeče",
+ "LabelFontScale": "Merilo pisave",
+ "LabelFontStrikethrough": "Prečrtano",
+ "LabelFormat": "Oblika",
+ "LabelGenre": "Žanr",
+ "LabelGenres": "Žanri",
+ "LabelHardDeleteFile": "Trdo brisanje datoteke",
+ "LabelHasEbook": "Ima e-knjigo",
+ "LabelHasSupplementaryEbook": "Ima dodatno e-knjigo",
+ "LabelHideSubtitles": "Skrij podnapise",
+ "LabelHighestPriority": "Najvišja prioriteta",
+ "LabelHost": "Gostitelj",
+ "LabelHour": "Ura",
+ "LabelHours": "Ure",
+ "LabelIcon": "Ikona",
+ "LabelImageURLFromTheWeb": "URL slike iz spleta",
+ "LabelInProgress": "V teku",
+ "LabelIncludeInTracklist": "Vključi v seznam skladb",
+ "LabelIncomplete": "Nepopolno",
+ "LabelInterval": "Interval",
+ "LabelIntervalCustomDailyWeekly": "Dnevno/tedensko po meri",
+ "LabelIntervalEvery12Hours": "Vsakih 12 ur",
+ "LabelIntervalEvery15Minutes": "Vsakih 15 minut",
+ "LabelIntervalEvery2Hours": "Vsake 2 uri",
+ "LabelIntervalEvery30Minutes": "Vsakih 30 minut",
+ "LabelIntervalEvery6Hours": "Vsakih 6 ur",
+ "LabelIntervalEveryDay": "Vsak dan",
+ "LabelIntervalEveryHour": "Vsako uro",
+ "LabelInvert": "Obrni izbor",
+ "LabelItem": "Element",
+ "LabelJumpBackwardAmount": "Količina skoka nazaj",
+ "LabelJumpForwardAmount": "Količina skoka naprej",
+ "LabelLanguage": "Jezik",
+ "LabelLanguageDefaultServer": "Privzeti jezik strežnika",
+ "LabelLanguages": "Jeziki",
+ "LabelLastBookAdded": "Zadnja dodana knjiga",
+ "LabelLastBookUpdated": "Zadnja posodobljena knjiga",
+ "LabelLastSeen": "Nazadnje viden",
+ "LabelLastTime": "Zadnji čas",
+ "LabelLastUpdate": "Zadnja posodobitev",
+ "LabelLayout": "Postavitev",
+ "LabelLayoutSinglePage": "Ena stran",
+ "LabelLayoutSplitPage": "Razdeli stran",
+ "LabelLess": "Manj",
+ "LabelLibrariesAccessibleToUser": "Knjižnice, dostopne uporabniku",
+ "LabelLibrary": "Knjižnica",
+ "LabelLibraryFilterSublistEmpty": "Ne {0}",
+ "LabelLibraryItem": "Element knjižnice",
+ "LabelLibraryName": "Ime knjižnice",
+ "LabelLimit": "Omejitev",
+ "LabelLineSpacing": "Razmik med vrsticami",
+ "LabelListenAgain": "Poslušaj znova",
+ "LabelLogLevelDebug": "Odpravljanje napak",
+ "LabelLogLevelInfo": "Info",
+ "LabelLogLevelWarn": "Opozoritve",
+ "LabelLookForNewEpisodesAfterDate": "Poiščite nove epizode po tem datumu",
+ "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",
+ "LabelMediaPlayer": "Medijski predvajalnik",
+ "LabelMediaType": "Vrsta medija",
+ "LabelMetaTag": "Meta oznaka",
+ "LabelMetaTags": "Meta oznake",
+ "LabelMetadataOrderOfPrecedenceDescription": "Viri metapodatkov višje prioritete bodo preglasili vire metapodatkov nižje prioritete",
+ "LabelMetadataProvider": "Ponudnik metapodatkov",
+ "LabelMinute": "Minuta",
+ "LabelMinutes": "Minute",
+ "LabelMissing": "Manjkajoče",
+ "LabelMissingEbook": "Nima nobene eknjige",
+ "LabelMissingSupplementaryEbook": "Nima nobene dodatne eknjige",
+ "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č",
+ "LabelMoreInfo": "Več informacij",
+ "LabelName": "Naziv",
+ "LabelNarrator": "Bralec",
+ "LabelNarrators": "Bralci",
+ "LabelNew": "Novo",
+ "LabelNewPassword": "Novo geslo",
+ "LabelNewestAuthors": "Najnovejši avtorji",
+ "LabelNewestEpisodes": "Najnovejše epizode",
+ "LabelNextBackupDate": "Naslednji datum varnostnega kopiranja",
+ "LabelNextScheduledRun": "Naslednji načrtovani zagon",
+ "LabelNoCustomMetadataProviders": "Ni ponudnikov metapodatkov po meri",
+ "LabelNoEpisodesSelected": "Izbrana ni nobena epizoda",
+ "LabelNotFinished": "Ni dokončano",
+ "LabelNotStarted": "Ni zagnano",
+ "LabelNotes": "Opombe",
+ "LabelNotificationAppriseURL": "Apprise URL(ji)",
+ "LabelNotificationAvailableVariables": "Razpoložljive spremenljivke",
+ "LabelNotificationBodyTemplate": "Predloga telesa",
+ "LabelNotificationEvent": "Dogodek obvestila",
+ "LabelNotificationTitleTemplate": "Predloga naslova",
+ "LabelNotificationsMaxFailedAttempts": "Najvišje število neuspelih poskusov",
+ "LabelNotificationsMaxFailedAttemptsHelp": "Obvestila so onemogočena, ko se tolikokrat neuspelo pošljejo",
+ "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",
+ "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",
+ "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",
+ "LabelPermissionsDelete": "Lahko briše",
+ "LabelPermissionsDownload": "Lahko prenaša",
+ "LabelPermissionsUpdate": "Lahko posodablja",
+ "LabelPermissionsUpload": "Lahko nalaga",
+ "LabelPersonalYearReview": "Pregled tvojega leta ({0})",
+ "LabelPhotoPathURL": "Slika pot/URL",
+ "LabelPlayMethod": "Metoda predvajanja",
+ "LabelPlayerChapterNumberMarker": "{0} od {1}",
+ "LabelPlaylists": "Seznami predvajanja",
+ "LabelPodcast": "Podcast",
+ "LabelPodcastSearchRegion": "Regija iskanja podcastov",
+ "LabelPodcastType": "Vrsta podcasta",
+ "LabelPodcasts": "Podcasti",
+ "LabelPort": "Vrata",
+ "LabelPrefixesToIgnore": "Predpone, ki jih je treba prezreti (neobčutljivo na velike in male črke)",
+ "LabelPreventIndexing": "Preprečite, da bi vaš vir indeksirali imeniki podcastov iTunes in Google",
+ "LabelPrimaryEbook": "Primarna e-knjiga",
+ "LabelProgress": "Napredek",
+ "LabelProvider": "Ponudnik",
+ "LabelProviderAuthorizationValue": "Vrednost glave avtorizacije",
+ "LabelPubDate": "Datum objave",
+ "LabelPublishYear": "Leto objave",
+ "LabelPublishedDate": "Objavljeno {0}",
+ "LabelPublisher": "Založnik",
+ "LabelPublishers": "Založniki",
+ "LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri",
+ "LabelRSSFeedCustomOwnerName": "Ime lastnika po meri",
+ "LabelRSSFeedOpen": "Odprt vir RSS",
+ "LabelRSSFeedPreventIndexing": "Prepreči indeksiranje",
+ "LabelRSSFeedSlug": "Slug RSS vira",
+ "LabelRSSFeedURL": "URL vira RSS",
+ "LabelRandomly": "Naključno",
+ "LabelReAddSeriesToContinueListening": "Znova dodaj serijo za nadaljevanje poslušanja",
+ "LabelRead": "Preberi",
+ "LabelReadAgain": "Ponovno preberi",
+ "LabelReadEbookWithoutProgress": "Preberi eknjigo brez ohranjanja napredka",
+ "LabelRecentSeries": "Nedavne serije",
+ "LabelRecentlyAdded": "Nedavno dodano",
+ "LabelRecommended": "Priporočeno",
+ "LabelRedo": "Ponovi",
+ "LabelRegion": "Regija",
+ "LabelReleaseDate": "Datum izdaje",
+ "LabelRemoveCover": "Odstrani naslovnico",
+ "LabelRowsPerPage": "Vrstic na stran",
+ "LabelSearchTerm": "Iskalni pojem",
+ "LabelSearchTitle": "Naslov iskanja",
+ "LabelSearchTitleOrASIN": "Naslov iskanja ali ASIN",
+ "LabelSeason": "Sezona",
+ "LabelSelectAll": "Izberite vse",
+ "LabelSelectAllEpisodes": "Izberite vse epizode",
+ "LabelSelectEpisodesShowing": "Izberi {0} prikazanih epizod",
+ "LabelSelectUsers": "Izberite uporabnike",
+ "LabelSendEbookToDevice": "Pošlji eknjigo k...",
+ "LabelSequence": "Zaporedje",
+ "LabelSeries": "Serije",
+ "LabelSeriesName": "Ime serije",
+ "LabelSeriesProgress": "Napredek serije",
+ "LabelServerYearReview": "Pregled leta strežnika ({0})",
+ "LabelSetEbookAsPrimary": "Nastavi kot primarno",
+ "LabelSetEbookAsSupplementary": "Nastavi kot dodatno",
+ "LabelSettingsAudiobooksOnly": "Samo zvočne knjige",
+ "LabelSettingsAudiobooksOnlyHelp": "Če omogočite to nastavitev, bodo datoteke eknjig prezrte, razen če so znotraj mape zvočnih knjig, v tem primeru bodo nastavljene kot dodatne e-knjige",
+ "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",
+ "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",
+ "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.",
+ "LabelSettingsExperimentalFeatures": "Eksperimentalne funkcije",
+ "LabelSettingsExperimentalFeaturesHelp": "Funkcije v razvoju, ki bi lahko uporabile vaše povratne informacije in pomoč pri testiranju. Kliknite, da odprete razpravo na githubu.",
+ "LabelSettingsFindCovers": "Poišči naslovnice",
+ "LabelSettingsFindCoversHelp": "Če vaša zvočna knjiga nima vdelane naslovnice ali slike naslovnice v mapi, bo pregledovalnik poskušal najti naslovnico.
Opomba: To bo podaljšalo čas pregledovanja",
+ "LabelSettingsHideSingleBookSeries": "Skrij serije s samo eno knjigo",
+ "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",
+ "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«",
+ "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",
+ "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",
+ "LabelSettingsStoreCoversWithItem": "Shrani naslovnice skupaj z elementom",
+ "LabelSettingsStoreCoversWithItemHelp": "Naslovnice so privzeto shranjene v /metadata/items, če omogočite to nastavitev, bodo platnice shranjene v mapi elementov knjižnice. Shranjena bo samo ena datoteka z imenom \"cover\"",
+ "LabelSettingsStoreMetadataWithItem": "Shrani metapodatke skupaj z elementom",
+ "LabelSettingsStoreMetadataWithItemHelp": "Datoteke z metapodatki so privzeto shranjene v /metadata/items, če omogočite to nastavitev, boste datoteke z metapodatki shranili v mape elementov vaše knjižnice",
+ "LabelSettingsTimeFormat": "Oblika časa",
+ "LabelShare": "Deli",
+ "LabelShareOpen": "Deli odprto",
+ "LabelShareURL": "Deli URL",
+ "LabelShowAll": "Prikaži vse",
+ "LabelShowSeconds": "Prikaži sekunde",
+ "LabelShowSubtitles": "Prikaži podnapise",
+ "LabelSize": "Velikost",
+ "LabelSleepTimer": "Časovnik za spanje",
+ "LabelSlug": "Slug",
+ "LabelStart": "Začetek",
+ "LabelStartTime": "Začetni čas",
+ "LabelStarted": "Začeto",
+ "LabelStartedAt": "Začeto ob",
+ "LabelStatsAudioTracks": "Zvočni posnetki",
+ "LabelStatsAuthors": "Avtorji",
+ "LabelStatsBestDay": "Najboljši dan",
+ "LabelStatsDailyAverage": "Dnevno povprečje",
+ "LabelStatsDays": "Dnevi",
+ "LabelStatsDaysListened": "Poslušani dnevi",
+ "LabelStatsHours": "Ure",
+ "LabelStatsInARow": "v vrsti",
+ "LabelStatsItemsFinished": "Končani elementi",
+ "LabelStatsItemsInLibrary": "Elementi v knjižnici",
+ "LabelStatsMinutes": "minute",
+ "LabelStatsMinutesListening": "Poslušane minute",
+ "LabelStatsOverallDays": "Skupaj dnevi",
+ "LabelStatsOverallHours": "Skupaj ure",
+ "LabelStatsWeekListening": "Tednov poslušanja",
+ "LabelSubtitle": "Podnapis",
+ "LabelSupportedFileTypes": "Podprte vrste datotek",
+ "LabelTag": "Oznaka",
+ "LabelTags": "Oznake",
+ "LabelTagsAccessibleToUser": "Oznake, dostopne uporabniku",
+ "LabelTagsNotAccessibleToUser": "Oznake, ki niso dostopne uporabniku",
+ "LabelTasks": "Tekoče naloge",
+ "LabelTextEditorBulletedList": "Seznam z oznakami",
+ "LabelTextEditorLink": "Povezava",
+ "LabelTextEditorNumberedList": "Številčni seznam",
+ "LabelTextEditorUnlink": "Odveži",
+ "LabelTheme": "Tema",
+ "LabelThemeDark": "Temna",
+ "LabelThemeLight": "Svetla",
+ "LabelTimeBase": "Odvisna od časa",
+ "LabelTimeDurationXHours": "{0} ur",
+ "LabelTimeDurationXMinutes": "{0} minut",
+ "LabelTimeDurationXSeconds": "{0} sekund",
+ "LabelTimeInMinutes": "Čas v minutah",
+ "LabelTimeListened": "Čas poslušanja",
+ "LabelTimeListenedToday": "Čas poslušanja danes",
+ "LabelTimeRemaining": "Še {0}",
+ "LabelTimeToShift": "Čas prestavljanja v sekundah",
+ "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.",
+ "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",
+ "LabelTotalTimeListened": "Skupni čas poslušanja",
+ "LabelTrackFromFilename": "Posnetek iz datoteke",
+ "LabelTrackFromMetadata": "Posnetek iz metapodatkov",
+ "LabelTracks": "Posnetki",
+ "LabelTracksMultiTrack": "Več posnetkov",
+ "LabelTracksNone": "Brez posnetka",
+ "LabelTracksSingleTrack": "Enojni posnetek",
+ "LabelType": "Vrsta",
+ "LabelUnabridged": "Neskrajšano",
+ "LabelUndo": "Razveljavi",
+ "LabelUnknown": "Neznano",
+ "LabelUnknownPublishDate": "Neznan datum objave",
+ "LabelUpdateCover": "Posodobi naslovnico",
+ "LabelUpdateCoverHelp": "Dovoli prepisovanje obstoječih naslovnic za izbrane knjige, ko se najde ujemanje",
+ "LabelUpdateDetails": "Posodobi podrobnosti",
+ "LabelUpdateDetailsHelp": "Dovoli prepisovanje obstoječih podrobnosti za izbrane knjige, ko se najde ujemanje",
+ "LabelUpdatedAt": "Posodobljeno ob",
+ "LabelUploaderDragAndDrop": "Povleci in spusti datoteke ali mape",
+ "LabelUploaderDropFiles": "Spusti datoteke",
+ "LabelUploaderItemFetchMetadataHelp": "Samodejno pridobi naslov, avtorja in serijo",
+ "LabelUseChapterTrack": "Uporabi posnetek poglavij",
+ "LabelUseFullTrack": "Uporabi celoten posnetek",
+ "LabelUser": "Uporabnik",
+ "LabelUsername": "Uporabniško ime",
+ "LabelValue": "Vrednost",
+ "LabelVersion": "Verzija",
+ "LabelViewBookmarks": "Ogled zaznamkov",
+ "LabelViewChapters": "Ogled poglavij",
+ "LabelViewPlayerSettings": "Ogled nastavitev predvajalnika",
+ "LabelViewQueue": "Ogled čakalno vrsto predvajalnika",
+ "LabelVolume": "Glasnost",
+ "LabelWeekdaysToRun": "Delovni dnevi predvajanja",
+ "LabelXBooks": "{0} knjig",
+ "LabelXItems": "{0} elementov",
+ "LabelYearReviewHide": "Skrij pregled leta",
+ "LabelYearReviewShow": "Poglej 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
.",
+ "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.",
+ "MessageBackupsLocationPathEmpty": "Pot do lokacije varnostne kopije ne sme biti prazna",
+ "MessageBatchQuickMatchDescription": "Hitro ujemanje bo poskušal dodati manjkajoče naslovnice in metapodatke za izbrane elemente. Omogočite spodnje možnosti, da omogočite hitremu ujemanju, da prepiše obstoječe naslovnice in/ali metapodatke.",
+ "MessageBookshelfNoCollections": "Ustvaril nisi še nobene zbirke",
+ "MessageBookshelfNoRSSFeeds": "Noben vir RSS ni odprt",
+ "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",
+ "MessageChapterErrorFirstNotZero": "Prvo poglavje se mora začeti pri 0",
+ "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...",
+ "MessageConfirmCloseFeed": "Ali ste prepričani, da želite zapreti ta vir?",
+ "MessageConfirmDeleteBackup": "Ali ste prepričani, da želite izbrisati varnostno kopijo za {0}?",
+ "MessageConfirmDeleteDevice": "Ali ste prepričani, da želite izbrisati e-bralnik \"{0}\"?",
+ "MessageConfirmDeleteFile": "To bo izbrisalo datoteko iz vašega datotečnega sistema. Ali ste prepričani?",
+ "MessageConfirmDeleteLibrary": "Ali ste prepričani, da želite trajno izbrisati knjižnico \"{0}\"?",
+ "MessageConfirmDeleteLibraryItem": "S tem boste element knjižnice izbrisali iz baze podatkov in vašega datotečnega sistema. Ste prepričani?",
+ "MessageConfirmDeleteLibraryItems": "To bo izbrisalo {0} elementov knjižnice iz baze podatkov in vašega datotečnega sistema. Ste prepričani?",
+ "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?",
+ "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?",
+ "MessageConfirmMarkItemNotFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot nedokončanega?",
+ "MessageConfirmMarkSeriesFinished": "Ali ste prepričani, da želite vse knjige v tej seriji označiti kot dokončane?",
+ "MessageConfirmMarkSeriesNotFinished": "Ali ste prepričani, da želite vse knjige v tej seriji označiti kot nedokončane?",
+ "MessageConfirmNotificationTestTrigger": "Želite sprožiti to obvestilo s testnimi podatki?",
+ "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?",
+ "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?",
+ "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?",
+ "MessageConfirmRenameGenreMergeNote": "Opomba: Ta žanr že obstaja, zato bosta združeni.",
+ "MessageConfirmRenameGenreWarning": "Opozorilo! Podoben žanr z različnimi velikosti črk že obstaja \"{0}\".",
+ "MessageConfirmRenameTag": "Ali ste prepričani, da želite preimenovati oznako \"{0}\" v \"{1}\" za vse elemente?",
+ "MessageConfirmRenameTagMergeNote": "Opomba: Ta oznaka že obstaja, zato bosta združeni.",
+ "MessageConfirmRenameTagWarning": "Opozorilo! Podobna oznaka z različnimi velikosti črk že obstaja \"{0}\".",
+ "MessageConfirmResetProgress": "Ali ste prepričani, da želite ponastaviti svoj napredek?",
+ "MessageConfirmSendEbookToDevice": "Ali ste prepričani, da želite poslati {0} e-knjigo \"{1}\" v napravo \"{2}\"?",
+ "MessageConfirmUnlinkOpenId": "Ali ste prepričani, da želite prekiniti povezavo tega uporabnika z OpenID?",
+ "MessageDownloadingEpisode": "Prenašam epizodo",
+ "MessageDragFilesIntoTrackOrder": "Povlecite datoteke v pravilen vrstni red posnetkov",
+ "MessageEmbedFailed": "Vdelava ni uspela!",
+ "MessageEmbedFinished": "Vdelava končana!",
+ "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.",
+ "MessageImportantNotice": "Pomembno obvestilo!",
+ "MessageInsertChapterBelow": "Spodaj vstavite poglavje",
+ "MessageItemsSelected": "{0} izbranih elementov",
+ "MessageItemsUpdated": "Št. posodobljenih elementov: {0}",
+ "MessageJoinUsOn": "Pridružite se nam",
+ "MessageListeningSessionsInTheLastYear": "{0} sej poslušanja v zadnjem letu",
+ "MessageLoading": "Nalagam...",
+ "MessageLoadingFolders": "Nalagam mape...",
+ "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",
+ "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.",
+ "MessageNoAudioTracks": "Ni zvočnih posnetkov",
+ "MessageNoAuthors": "Brez avtorjev",
+ "MessageNoBackups": "Brez varnostnih kopij",
+ "MessageNoBookmarks": "Brez zaznamkov",
+ "MessageNoChapters": "Brez poglavij",
+ "MessageNoCollections": "Brez zbirk",
+ "MessageNoCoversFound": "Ni naslovnic",
+ "MessageNoDescription": "Ni opisa",
+ "MessageNoDevices": "Ni naprav",
+ "MessageNoDownloadsInProgress": "Trenutno ni prenosov v teku",
+ "MessageNoDownloadsQueued": "Ni prenosov v čakalni vrsti",
+ "MessageNoEpisodeMatchesFound": "Ni zadetkov za epizodo",
+ "MessageNoEpisodes": "Ni epizod",
+ "MessageNoFoldersAvailable": "Ni na voljo nobene mape",
+ "MessageNoGenres": "Ni žanrov",
+ "MessageNoIssues": "Ni težav",
+ "MessageNoItems": "Ni elementov",
+ "MessageNoItemsFound": "Ni najdenih elementov",
+ "MessageNoListeningSessions": "Ni sej poslušanja",
+ "MessageNoLogs": "Ni dnevnikov",
+ "MessageNoMediaProgress": "Ni medijskega napredka",
+ "MessageNoNotifications": "Ni obvestil",
+ "MessageNoPodcastsFound": "Ni podcastov",
+ "MessageNoResults": "Ni rezultatov",
+ "MessageNoSearchResultsFor": "Ni rezultatov iskanja za \"{0}\"",
+ "MessageNoSeries": "Ni serij",
+ "MessageNoTags": "Ni oznak",
+ "MessageNoTasksRunning": "Nobeno opravili ne teče",
+ "MessageNoUpdatesWereNecessary": "Posodobitve niso bile potrebne",
+ "MessageNoUserPlaylists": "Nimate seznamov predvajanja",
+ "MessageNotYetImplemented": "Še ni implementirano",
+ "MessageOpmlPreviewNote": "Opomba: To je predogled razčlenjene datoteke OPML. Dejanski naslov podcasta bo vzet iz vira RSS.",
+ "MessageOr": "ali",
+ "MessagePauseChapter": "Začasno ustavite predvajanje poglavja",
+ "MessagePlayChapter": "Poslušajte začetek poglavja",
+ "MessagePlaylistCreateFromCollection": "Ustvari seznam predvajanja iz zbirke",
+ "MessagePleaseWait": "Prosim počakajte...",
+ "MessagePodcastHasNoRSSFeedForMatching": "Podcast nima URL-ja vira RSS, ki bi ga lahko uporabil za ujemanje",
+ "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}",
+ "MessageRemoveFromPlayerQueue": "Odstrani iz čakalne vrste predvajalnika",
+ "MessageRemoveUserWarning": "Ali ste prepričani, da želite trajno izbrisati uporabnika \"{0}\"?",
+ "MessageReportBugsAndContribute": "Prijavite hrošče, zahtevajte nove funkcije in prispevajte še naprej",
+ "MessageResetChaptersConfirm": "Ali ste prepričani, da želite ponastaviti poglavja in razveljaviti spremembe, ki ste jih naredili?",
+ "MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob",
+ "MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.
Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.
Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.",
+ "MessageSearchResultsFor": "Rezultati iskanja za",
+ "MessageSelected": "{0} izbrano",
+ "MessageServerCouldNotBeReached": "Strežnika ni bilo mogoče doseči",
+ "MessageSetChaptersFromTracksDescription": "Nastavite poglavja z uporabo vsake zvočne datoteke kot poglavja in naslova poglavja kot imena zvočne datoteke",
+ "MessageShareExpirationWillBe": "Potečeno bo
{0} ",
+ "MessageShareExpiresIn": "Poteče čez {0}",
+ "MessageShareURLWillBe": "URL za skupno rabo bo
{0} ",
+ "MessageStartPlaybackAtTime": "Začni predvajanje za \"{0}\" ob {1}?",
+ "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",
+ "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",
+ "NoteChangeRootPassword": "Korenski uporabnik je edini uporabnik, ki ima lahko prazno geslo",
+ "NoteChapterEditorTimes": "Opomba: Začetni čas prvega poglavja mora ostati pri 0:00 in zadnji čas začetka poglavja ne sme preseči tega trajanja zvočne knjige.",
+ "NoteFolderPicker": "Opomba: že preslikane mape ne bodo prikazane",
+ "NoteRSSFeedPodcastAppsHttps": "Opozorilo: večina aplikacij za podcaste bo zahtevala, da URL vira RSS uporablja HTTPS",
+ "NoteRSSFeedPodcastAppsPubDate": "Opozorilo: 1 ali več vaših epizod nima datuma objave. Nekatere aplikacije za podcaste to zahtevajo.",
+ "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.",
+ "PlaceholderNewCollection": "Novo ime zbirke",
+ "PlaceholderNewFolderPath": "Pot nove mape",
+ "PlaceholderNewPlaylist": "Novo ime seznama predvajanja",
+ "PlaceholderSearch": "Poišči..",
+ "PlaceholderSearchEpisode": "Poišči epizodo...",
+ "StatsAuthorsAdded": "dodanih avtorjev",
+ "StatsBooksAdded": "dodanih knjig",
+ "StatsBooksAdditional": "Nekateri dodatki vključujejo…",
+ "StatsBooksFinished": "končane knjige",
+ "StatsBooksFinishedThisYear": "Nekaj knjig, ki so bile dokončane letos…",
+ "StatsBooksListenedTo": "poslušane knjige",
+ "StatsCollectionGrewTo": "Vaša zbirka knjig se je povečala na …",
+ "StatsSessions": "seje",
+ "StatsSpentListening": "porabil za poslušanje",
+ "StatsTopAuthor": "TOP AVTOR",
+ "StatsTopAuthors": "TOP AVTORJI",
+ "StatsTopGenre": "TOP ŽANR",
+ "StatsTopGenres": "TOP ŽANRI",
+ "StatsTopMonth": "TOP MESEC",
+ "StatsTopNarrator": "TOP BRALEC",
+ "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",
+ "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)",
+ "ToastBackupAppliedSuccess": "Uporabljena varnostna kopija",
+ "ToastBackupCreateFailed": "Varnostne kopije ni bilo mogoče ustvariti",
+ "ToastBackupCreateSuccess": "Varnostna kopija ustvarjena",
+ "ToastBackupDeleteFailed": "Varnostne kopije ni bilo mogoče izbrisati",
+ "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",
+ "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",
+ "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",
+ "ToastDeleteFileSuccess": "Datoteka je bila izbrisana",
+ "ToastDeviceAddFailed": "Naprave ni bilo mogoče dodati",
+ "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",
+ "ToastErrorCannotShare": "V tej napravi ni mogoče dati v skupno rabo",
+ "ToastFailedToLoadData": "Podatkov ni bilo mogoče naložiti",
+ "ToastFailedToShare": "Skupna raba ni uspela",
+ "ToastFailedToUpdateAccount": "Računa ni bilo mogoče posodobiti",
+ "ToastFailedToUpdateUser": "Uporabnika ni bilo mogoče posodobiti",
+ "ToastInvalidImageUrl": "Neveljaven URL slike",
+ "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",
+ "ToastLibraryDeleteFailed": "Knjižnice ni bilo mogoče izbrisati",
+ "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",
+ "ToastNameEmailRequired": "Ime in e-pošta sta obvezna",
+ "ToastNameRequired": "Ime je obvezno",
+ "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",
+ "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",
+ "ToastProviderCreatedFailed": "Ponudnika ni bilo mogoče dodati",
+ "ToastProviderCreatedSuccess": "Dodan je bil nov ponudnik",
+ "ToastProviderNameAndUrlRequired": "Obvezen podatek sta ime in URL",
+ "ToastProviderRemoveSuccess": "Ponudnik je bil odstranjen",
+ "ToastRSSFeedCloseFailed": "Vira RSS ni bilo mogoče zapreti",
+ "ToastRSSFeedCloseSuccess": "Vir RSS je bil zaprt",
+ "ToastRemoveFailed": "Odstranitev ni uspela",
+ "ToastRemoveItemFromCollectionFailed": "Elementa ni bilo mogoče odstraniti iz zbirke",
+ "ToastRemoveItemFromCollectionSuccess": "Element je bil odstranjen iz zbirke",
+ "ToastRemoveItemsWithIssuesFailed": "Elementov knjižnice s težavami ni bilo mogoče odstraniti",
+ "ToastRemoveItemsWithIssuesSuccess": "Odstranjeni so bili elementi knjižnice s težavami",
+ "ToastRenameFailed": "Preimenovanje ni uspelo",
+ "ToastRescanFailed": "Ponovni pregled ni uspel za {0}",
+ "ToastRescanRemoved": "Ponovni pregled celotnega elementa je bil odstranjen",
+ "ToastRescanUpToDate": "Ponovni pregled celotnega elementa je bil ažuren",
+ "ToastRescanUpdated": "Ponovni pregled celotnega elementa je bil posodobljen",
+ "ToastScanFailed": "Pregled elementa knjižnice ni uspel",
+ "ToastSelectAtLeastOneUser": "Izberite vsaj enega uporabnika",
+ "ToastSendEbookToDeviceFailed": "E-knjige ni bilo mogoče poslati v napravo",
+ "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",
+ "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",
+ "ToastUnlinkOpenIdFailed": "Prekinitev povezave uporabnika z OpenID ni uspela",
+ "ToastUnlinkOpenIdSuccess": "Uporabnik je prekinil povezavo z OpenID",
+ "ToastUserDeleteFailed": "Brisanje uporabnika ni uspelo",
+ "ToastUserDeleteSuccess": "Uporabnik je bil izbrisan",
+ "ToastUserPasswordChangeSuccess": "Geslo je bilo uspešno spremenjeno",
+ "ToastUserPasswordMismatch": "Gesli se ne ujemata",
+ "ToastUserPasswordMustChange": "Novo geslo se ne sme ujemati s starim geslom",
+ "ToastUserRootRequireName": "Vnesti morate korensko uporabniško ime"
+}
diff --git a/client/strings/sv.json b/client/strings/sv.json
index 59da191fb7..0db5cbd3a2 100644
--- a/client/strings/sv.json
+++ b/client/strings/sv.json
@@ -9,7 +9,7 @@
"ButtonApply": "Tillämpa",
"ButtonApplyChapters": "Tillämpa kapitel",
"ButtonAuthors": "Författare",
- "ButtonBack": "Back",
+ "ButtonBack": "Tillbaka",
"ButtonBrowseForFolder": "Bläddra efter mapp",
"ButtonCancel": "Avbryt",
"ButtonCancelEncode": "Avbryt kodning",
@@ -33,8 +33,6 @@
"ButtonHide": "Dölj",
"ButtonHome": "Hem",
"ButtonIssues": "Problem",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Senaste",
"ButtonLibrary": "Bibliotek",
"ButtonLogout": "Logga ut",
@@ -44,17 +42,12 @@
"ButtonMatchAllAuthors": "Matcha alla författare",
"ButtonMatchBooks": "Matcha böcker",
"ButtonNevermind": "Glöm det",
- "ButtonNext": "Next",
- "ButtonNextChapter": "Next Chapter",
"ButtonOk": "Okej",
"ButtonOpenFeed": "Öppna flöde",
"ButtonOpenManager": "Öppna Manager",
- "ButtonPause": "Pause",
"ButtonPlay": "Spela",
"ButtonPlaying": "Spelar",
"ButtonPlaylists": "Spellistor",
- "ButtonPrevious": "Previous",
- "ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Rensa all cache",
"ButtonPurgeItemsCache": "Rensa föremåls-cache",
"ButtonQueueAddItem": "Lägg till i kön",
@@ -62,9 +55,6 @@
"ButtonQuickMatch": "Snabb matchning",
"ButtonReScan": "Omstart",
"ButtonRead": "Läs",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
"ButtonRemove": "Ta bort",
"ButtonRemoveAll": "Ta bort alla",
"ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt",
@@ -83,7 +73,6 @@
"ButtonSelectFolderPath": "Välj mappens sökväg",
"ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Ställ in kapitel från spår",
- "ButtonShare": "Share",
"ButtonShiftTimes": "Förskjut tider",
"ButtonShow": "Visa",
"ButtonStartM4BEncode": "Starta M4B-kodning",
@@ -98,15 +87,11 @@
"ButtonUserEdit": "Redigera användare {0}",
"ButtonViewAll": "Visa alla",
"ButtonYes": "Ja",
- "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Avancerad",
"HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar",
"HeaderAudioTracks": "Ljudspår",
"HeaderAudiobookTools": "Ljudbokshantering",
- "HeaderAuthentication": "Authentication",
"HeaderBackups": "Säkerhetskopior",
"HeaderChangePassword": "Ändra lösenord",
"HeaderChapters": "Kapitel",
@@ -115,8 +100,6 @@
"HeaderCollectionItems": "Samlingselement",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Aktuella nedladdningar",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
- "HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Nedladdningskö",
"HeaderEbookFiles": "E-boksfiler",
@@ -148,10 +131,8 @@
"HeaderNewAccount": "Nytt konto",
"HeaderNewLibrary": "Nytt bibliotek",
"HeaderNotifications": "Meddelanden",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Öppna RSS-flöde",
"HeaderOtherFiles": "Andra filer",
- "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Behörigheter",
"HeaderPlayerQueue": "Spelarkö",
"HeaderPlaylist": "Spellista",
@@ -166,7 +147,6 @@
"HeaderSavedMediaProgress": "Sparad medieförlopp",
"HeaderSchedule": "Schema",
"HeaderScheduleLibraryScans": "Schemalagda biblioteksskanningar",
- "HeaderSession": "Session",
"HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia",
"HeaderSettings": "Inställningar",
"HeaderSettingsDisplay": "Visning",
@@ -187,14 +167,9 @@
"HeaderUpdateDetails": "Uppdatera detaljer",
"HeaderUpdateLibrary": "Uppdatera bibliotek",
"HeaderUsers": "Användare",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Dina statistik",
"LabelAbridged": "Förkortad",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Kontotyp",
- "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Gäst",
"LabelAccountTypeUser": "Användare",
"LabelActivity": "Aktivitet",
@@ -202,7 +177,6 @@
"LabelAddToCollectionBatch": "Lägg till {0} böcker i Samlingen",
"LabelAddToPlaylist": "Lägg till i Spellista",
"LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan",
- "LabelAdded": "Tillagd",
"LabelAddedAt": "Tillagd vid",
"LabelAdminUsersOnly": "Endast administratörer",
"LabelAll": "Alla",
@@ -216,12 +190,6 @@
"LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)",
"LabelAuthors": "Författare",
"LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Tillbaka till användaren",
"LabelBackupLocation": "Säkerhetskopia Plats",
"LabelBackupsEnableAutomaticBackups": "Aktivera automatiska säkerhetskopior",
@@ -232,8 +200,6 @@
"LabelBackupsNumberToKeepHelp": "Endast en säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än detta bör du ta bort dem manuellt.",
"LabelBitrate": "Bitfrekvens",
"LabelBooks": "Böcker",
- "LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Ändra lösenord",
"LabelChannels": "Kanaler",
"LabelChapterTitle": "Kapitelrubrik",
@@ -241,15 +207,14 @@
"LabelChaptersFound": "hittade kapitel",
"LabelClickForMoreInfo": "Klicka för mer information",
"LabelClosePlayer": "Stäng spelaren",
- "LabelCodec": "Codec",
"LabelCollapseSeries": "Fäll ihop serie",
"LabelCollection": "Samling",
"LabelCollections": "Samlingar",
"LabelComplete": "Komplett",
"LabelConfirmPassword": "Bekräfta lösenord",
- "LabelContinueListening": "Fortsätt lyssna",
- "LabelContinueReading": "Fortsätt läsa",
- "LabelContinueSeries": "Fortsätt serie",
+ "LabelContinueListening": "Fortsätt Lyssna",
+ "LabelContinueReading": "Fortsätt Läsa",
+ "LabelContinueSeries": "Forsätt Serie",
"LabelCover": "Omslag",
"LabelCoverImageURL": "URL till omslagsbild",
"LabelCreatedAt": "Skapad vid",
@@ -271,32 +236,24 @@
"LabelDownload": "Ladda ner",
"LabelDownloadNEpisodes": "Ladda ner {0} avsnitt",
"LabelDuration": "Varaktighet",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Varaktighet hittad:",
"LabelEbook": "E-bok",
- "LabelEbooks": "E-böcker",
+ "LabelEbooks": "Eböcker",
"LabelEdit": "Redigera",
"LabelEmail": "E-post",
"LabelEmailSettingsFromAddress": "Från adress",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "Säker",
"LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testadress",
"LabelEmbeddedCover": "Inbäddat omslag",
"LabelEnable": "Aktivera",
"LabelEnd": "Slut",
+ "LabelEndOfChapter": "Slut av kapitel",
"LabelEpisode": "Avsnitt",
"LabelEpisodeTitle": "Avsnittsrubrik",
"LabelEpisodeType": "Avsnittstyp",
"LabelExample": "Exempel",
- "LabelExplicit": "Explicit",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "Flödes-URL",
- "LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Fil",
"LabelFileBirthtime": "Födelse-tidpunkt för fil",
"LabelFileModified": "Fil ändrad",
@@ -306,19 +263,12 @@
"LabelFinished": "Avslutad",
"LabelFolder": "Mapp",
"LabelFolders": "Mappar",
- "LabelFontBold": "Bold",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Teckensnittsfamilj",
- "LabelFontItalic": "Italic",
"LabelFontScale": "Teckensnittsskala",
- "LabelFontStrikethrough": "Strikethrough",
- "LabelFormat": "Format",
- "LabelGenre": "Genre",
"LabelGenres": "Genrer",
"LabelHardDeleteFile": "Hård radering av fil",
- "LabelHasEbook": "Har e-bok",
- "LabelHasSupplementaryEbook": "Har kompletterande e-bok",
- "LabelHighestPriority": "Highest priority",
+ "LabelHasEbook": "Har E-bok",
+ "LabelHasSupplementaryEbook": "Har komplimenterande E-bok",
"LabelHost": "Värd",
"LabelHour": "Timme",
"LabelIcon": "Ikon",
@@ -339,19 +289,16 @@
"LabelItem": "Objekt",
"LabelLanguage": "Språk",
"LabelLanguageDefaultServer": "Standardspråk för server",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "Senaste bok tillagd",
"LabelLastBookUpdated": "Senaste bok uppdaterad",
"LabelLastSeen": "Senast sedd",
"LabelLastTime": "Senaste gången",
"LabelLastUpdate": "Senaste uppdatering",
- "LabelLayout": "Layout",
"LabelLayoutSinglePage": "En sida",
"LabelLayoutSplitPage": "Dela sida",
"LabelLess": "Mindre",
"LabelLibrariesAccessibleToUser": "Åtkomliga bibliotek för användare",
"LabelLibrary": "Bibliotek",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Biblioteksobjekt",
"LabelLibraryName": "Biblioteksnamn",
"LabelLimit": "Begränsning",
@@ -361,21 +308,13 @@
"LabelLogLevelInfo": "Felsökningsnivå: Information",
"LabelLogLevelWarn": "Felsökningsnivå: Varning",
"LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum",
- "LabelLowestPriority": "Lowest Priority",
- "LabelMatchExistingUsersBy": "Match existing users by",
- "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Mediaspelare",
"LabelMediaType": "Mediatyp",
"LabelMetaTag": "Metamärke",
"LabelMetaTags": "Metamärken",
- "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadataleverantör",
"LabelMinute": "Minut",
"LabelMissing": "Saknad",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
- "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
- "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (
*
) as the sole entry permits any URI.",
"LabelMore": "Mer",
"LabelMoreInfo": "Mer information",
"LabelName": "Namn",
@@ -387,7 +326,6 @@
"LabelNewestEpisodes": "Nyaste avsnitt",
"LabelNextBackupDate": "Nästa säkerhetskopia datum",
"LabelNextScheduledRun": "Nästa schemalagda körning",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Inga avsnitt valda",
"LabelNotFinished": "Ej avslutad",
"LabelNotStarted": "Inte påbörjad",
@@ -403,9 +341,6 @@
"LabelNotificationsMaxQueueSizeHelp": "Evenemang är begränsade till att utlösa ett per sekund. Evenemang kommer att ignoreras om kön är full. Detta förhindrar aviseringsspam.",
"LabelNumberOfBooks": "Antal böcker",
"LabelNumberOfEpisodes": "Antal avsnitt",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Öppna RSS-flöde",
"LabelOverwrite": "Skriv över",
"LabelPassword": "Lösenord",
@@ -417,16 +352,11 @@
"LabelPermissionsDownload": "Kan ladda ner",
"LabelPermissionsUpdate": "Kan uppdatera",
"LabelPermissionsUpload": "Kan ladda upp",
- "LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Bildsökväg/URL",
"LabelPlayMethod": "Spelläge",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Spellistor",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcast-sökområde",
"LabelPodcastType": "Podcasttyp",
- "LabelPodcasts": "Podcasts",
- "LabelPort": "Port",
"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",
@@ -435,7 +365,6 @@
"LabelPubDate": "Publiceringsdatum",
"LabelPublishYear": "Publiceringsår",
"LabelPublisher": "Utgivare",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
"LabelRSSFeedOpen": "Öppna RSS-flöde",
@@ -448,16 +377,12 @@
"LabelRecentSeries": "Senaste serier",
"LabelRecentlyAdded": "Nyligen tillagd",
"LabelRecommended": "Rekommenderad",
- "LabelRedo": "Redo",
- "LabelRegion": "Region",
"LabelReleaseDate": "Utgivningsdatum",
"LabelRemoveCover": "Ta bort omslag",
- "LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Sökterm",
"LabelSearchTitle": "Sök titel",
"LabelSearchTitleOrASIN": "Sök titel eller ASIN",
"LabelSeason": "Säsong",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Välj alla avsnitt",
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
"LabelSelectUsers": "Välj användare",
@@ -466,7 +391,6 @@
"LabelSeries": "Serie",
"LabelSeriesName": "Serienamn",
"LabelSeriesProgress": "Serieframsteg",
- "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Ange som primär",
"LabelSetEbookAsSupplementary": "Ange som kompletterande",
"LabelSettingsAudiobooksOnly": "Endast ljudböcker",
@@ -480,8 +404,6 @@
"LabelSettingsEnableWatcher": "Aktivera Watcher",
"LabelSettingsEnableWatcherForLibrary": "Aktivera mappbevakning för bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
"LabelSettingsFindCovers": "Hitta omslag",
@@ -490,8 +412,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier som har en enda bok kommer att döljas från seriesidan och hyllsidan på startsidan.",
"LabelSettingsHomePageBookshelfView": "Startsida använd bokhyllvy",
"LabelSettingsLibraryBookshelfView": "Bibliotek använd bokhyllvy",
- "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": "Analysera undertexter",
"LabelSettingsParseSubtitlesHelp": "Extrahera undertexter från mappnamn för ljudböcker.
Undertext måste vara åtskilda av \" - \"
t.ex. \"Boktitel - En undertitel här\" har undertiteln \"En undertitel här\"",
"LabelSettingsPreferMatchedMetadata": "Föredra matchad metadata",
@@ -508,11 +428,8 @@
"LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i /metadata/items, att aktivera detta alternativ kommer att lagra metadatafiler i dina biblioteksmappar",
"LabelSettingsTimeFormat": "Tidsformat",
"LabelShowAll": "Visa alla",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Storlek",
"LabelSleepTimer": "Sleeptimer",
- "LabelSlug": "Slug",
- "LabelStart": "Start",
"LabelStartTime": "Starttid",
"LabelStarted": "Startad",
"LabelStartedAt": "Startad vid",
@@ -538,10 +455,6 @@
"LabelTagsAccessibleToUser": "Taggar tillgängliga för användaren",
"LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren",
"LabelTasks": "Körande uppgifter",
- "LabelTextEditorBulletedList": "Bulleted list",
- "LabelTextEditorLink": "Link",
- "LabelTextEditorNumberedList": "Numbered list",
- "LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Tema",
"LabelThemeDark": "Mörkt",
"LabelThemeLight": "Ljust",
@@ -567,7 +480,6 @@
"LabelTracksSingleTrack": "Enspårigt",
"LabelType": "Typ",
"LabelUnabridged": "Oavkortad",
- "LabelUndo": "Undo",
"LabelUnknown": "Okänd",
"LabelUpdateCover": "Uppdatera omslag",
"LabelUpdateCoverHelp": "Tillåt överskrivning av befintliga omslag för de valda böckerna när en matchning hittas",
@@ -576,20 +488,16 @@
"LabelUpdatedAt": "Uppdaterad vid",
"LabelUploaderDragAndDrop": "Dra och släpp filer eller mappar",
"LabelUploaderDropFiles": "Släpp filer",
- "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Använd kapitelspår",
"LabelUseFullTrack": "Använd hela spåret",
"LabelUser": "Användare",
"LabelUsername": "Användarnamn",
"LabelValue": "Värde",
- "LabelVersion": "Version",
"LabelViewBookmarks": "Visa bokmärken",
"LabelViewChapters": "Visa kapitel",
"LabelViewQueue": "Visa spellista",
"LabelVolume": "Volym",
"LabelWeekdaysToRun": "Vardagar att köra",
- "LabelYearReviewHide": "Hide Year in Review",
- "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Din ljudboks varaktighet",
"LabelYourBookmarks": "Dina bokmärken",
"LabelYourPlaylists": "Dina spellistor",
@@ -601,7 +509,6 @@
"MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar",
"MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna",
"MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Du har inga serier",
"MessageChapterEndIsAfter": "Kapitelns slut är efter din ljudboks slut",
"MessageChapterErrorFirstNotZero": "Första kapitlet måste börja vid 0",
@@ -621,8 +528,6 @@
"MessageConfirmMarkAllEpisodesNotFinished": "Är du säker på att du vill markera alla avsnitt som inte avslutade?",
"MessageConfirmMarkSeriesFinished": "Är du säker på att du vill markera alla böcker i denna serie som avslutade?",
"MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som inte avslutade?",
- "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": "Varning! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler.
Vill du fortsätta?",
"MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra omgenomsökning för {0} objekt?",
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
@@ -630,7 +535,6 @@
"MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?",
"MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?",
- "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort berättaren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på genren \"{0}\" till \"{1}\" för alla objekt?",
@@ -644,7 +548,6 @@
"MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning",
"MessageEmbedFinished": "Inbäddning klar!",
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning",
- "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": "Flödes-URL kommer att vara {0}",
"MessageFetching": "Hämtar...",
"MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.",
@@ -656,7 +559,6 @@
"MessageListeningSessionsInTheLastYear": "{0} lyssningssessioner det senaste året",
"MessageLoading": "Laddar...",
"MessageLoadingFolders": "Laddar mappar...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B misslyckades!",
"MessageM4BFinished": "M4B klar!",
"MessageMapChapterTitles": "Kartlägg kapitelrubriker till dina befintliga ljudbokskapitel utan att justera tidstämplar",
@@ -692,7 +594,6 @@
"MessageNoSeries": "Inga serier",
"MessageNoTags": "Inga taggar",
"MessageNoTasksRunning": "Inga pågående uppgifter",
- "MessageNoUpdateNecessary": "Ingen uppdatering krävs",
"MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga",
"MessageNoUserPlaylists": "Du har inga spellistor",
"MessageNotYetImplemented": "Ännu inte implementerad",
@@ -711,7 +612,6 @@
"MessageRestoreBackupConfirm": "Är du säker på att du vill återställa säkerhetskopian som skapades den",
"MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.
Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.
Alla klienter som använder din server kommer att uppdateras automatiskt.",
"MessageSearchResultsFor": "Sökresultat för",
- "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Servern kunde inte nås",
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
"MessageStartPlaybackAtTime": "Starta uppspelning för \"{0}\" kl. {1}?",
@@ -739,7 +639,6 @@
"PlaceholderSearchEpisode": "Sök avsnitt...",
"ToastAccountUpdateFailed": "Det gick inte att uppdatera kontot",
"ToastAccountUpdateSuccess": "Kontot uppdaterat",
- "ToastAuthorImageRemoveFailed": "Det gick inte att ta bort författarens bild",
"ToastAuthorImageRemoveSuccess": "Författarens bild borttagen",
"ToastAuthorUpdateFailed": "Det gick inte att uppdatera författaren",
"ToastAuthorUpdateMerged": "Författaren sammanslagen",
@@ -756,28 +655,19 @@
"ToastBatchUpdateSuccess": "Batchuppdateringen lyckades",
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",
"ToastBookmarkCreateSuccess": "Bokmärket tillagt",
- "ToastBookmarkRemoveFailed": "Det gick inte att ta bort bokmärket",
"ToastBookmarkRemoveSuccess": "Bokmärket borttaget",
"ToastBookmarkUpdateFailed": "Det gick inte att uppdatera bokmärket",
"ToastBookmarkUpdateSuccess": "Bokmärket uppdaterat",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Kapitlen har fel",
"ToastChaptersMustHaveTitles": "Kapitel måste ha titlar",
- "ToastCollectionItemsRemoveFailed": "Det gick inte att ta bort objekt från samlingen",
"ToastCollectionItemsRemoveSuccess": "Objekt borttagna från samlingen",
- "ToastCollectionRemoveFailed": "Det gick inte att ta bort samlingen",
"ToastCollectionRemoveSuccess": "Samlingen borttagen",
"ToastCollectionUpdateFailed": "Det gick inte att uppdatera samlingen",
"ToastCollectionUpdateSuccess": "Samlingen uppdaterad",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "Det gick inte att uppdatera objektets omslag",
"ToastItemCoverUpdateSuccess": "Objektets omslag uppdaterat",
"ToastItemDetailsUpdateFailed": "Det gick inte att uppdatera objektdetaljerna",
"ToastItemDetailsUpdateSuccess": "Objektdetaljer uppdaterade",
- "ToastItemDetailsUpdateUnneeded": "Inga uppdateringar behövs för objektdetaljerna",
"ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera som färdig",
"ToastItemMarkedAsFinishedSuccess": "Objekt markerat som färdig",
"ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera som ej färdig",
@@ -792,7 +682,6 @@
"ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" uppdaterat",
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
"ToastPlaylistCreateSuccess": "Spellistan skapad",
- "ToastPlaylistRemoveFailed": "Det gick inte att ta bort spellistan",
"ToastPlaylistRemoveSuccess": "Spellistan borttagen",
"ToastPlaylistUpdateFailed": "Det gick inte att uppdatera spellistan",
"ToastPlaylistUpdateSuccess": "Spellistan uppdaterad",
@@ -806,16 +695,11 @@
"ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"",
"ToastSeriesUpdateFailed": "Serieuppdateringen misslyckades",
"ToastSeriesUpdateSuccess": "Serieuppdateringen lyckades",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Misslyckades med att ta bort sessionen",
"ToastSessionDeleteSuccess": "Sessionen borttagen",
"ToastSocketConnected": "Socket ansluten",
"ToastSocketDisconnected": "Socket frånkopplad",
"ToastSocketFailedToConnect": "Socket misslyckades med att ansluta",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Misslyckades med att ta bort användaren",
"ToastUserDeleteSuccess": "Användaren borttagen"
}
diff --git a/client/strings/uk.json b/client/strings/uk.json
index ba6575d8d3..d1a161c853 100644
--- a/client/strings/uk.json
+++ b/client/strings/uk.json
@@ -205,7 +205,6 @@
"LabelAddToCollectionBatch": "Додати книги до добірки: {0}",
"LabelAddToPlaylist": "Додати до списку відтворення",
"LabelAddToPlaylistBatch": "Додано елементів у список відтворення: {0}",
- "LabelAdded": "Додано",
"LabelAddedAt": "Дата додавання",
"LabelAdminUsersOnly": "Тільки для адміністраторів",
"LabelAll": "Усе",
@@ -362,7 +361,6 @@
"LabelLess": "Менше",
"LabelLibrariesAccessibleToUser": "Бібліотеки, доступні користувачу",
"LabelLibrary": "Бібліотека",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Елемент бібліотеки",
"LabelLibraryName": "Назва бібліотеки",
"LabelLimit": "Обмеження",
@@ -433,7 +431,6 @@
"LabelPersonalYearReview": "Ваші підсумки року ({0})",
"LabelPhotoPathURL": "Шлях/URL фото",
"LabelPlayMethod": "Метод відтворення",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Списки відтворення",
"LabelPodcast": "Подкаст",
"LabelPodcastSearchRegion": "Регіон пошуку подкасту",
@@ -455,6 +452,7 @@
"LabelRSSFeedPreventIndexing": "Запобігати індексації",
"LabelRSSFeedSlug": "Назва RSS-каналу",
"LabelRSSFeedURL": "Адреса RSS-каналу",
+ "LabelRandomly": "Випадково",
"LabelReAddSeriesToContinueListening": "Заново додати серії до Продовжити слухати",
"LabelRead": "Читати",
"LabelReadAgain": "Читати знову",
@@ -611,6 +609,8 @@
"LabelViewQueue": "Переглянути чергу відтворення",
"LabelVolume": "Гучність",
"LabelWeekdaysToRun": "Виконувати у дні",
+ "LabelXBooks": "{0} книг",
+ "LabelXItems": "{0} елементів",
"LabelYearReviewHide": "Сховати підсумки року",
"LabelYearReviewShow": "Переглянути підсумки року",
"LabelYourAudiobookDuration": "Тривалість вашої аудіокниги",
@@ -668,6 +668,7 @@
"MessageConfirmSendEbookToDevice": "Ви дійсно хочете відправити на пристрій \"{2}\" електроні книги: {0}, \"{1}\"?",
"MessageDownloadingEpisode": "Завантаження епізоду",
"MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку",
+ "MessageEmbedFailed": "Не вдалося вбудувати!",
"MessageEmbedFinished": "Вбудовано!",
"MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}",
"MessageEreaderDevices": "Аби гарантувати отримання електронних книг, вам може знадобитися додати вказану вище адресу електронної пошти як правильного відправника на кожному з пристроїв зі списку нижче.",
@@ -718,7 +719,6 @@
"MessageNoSeries": "Без серії",
"MessageNoTags": "Без міток",
"MessageNoTasksRunning": "Немає активних завдань",
- "MessageNoUpdateNecessary": "Оновлення не потрібно",
"MessageNoUpdatesWereNecessary": "Оновлень не потрібно",
"MessageNoUserPlaylists": "У вас немає списків відтворення",
"MessageNotYetImplemented": "Ще не реалізовано",
@@ -767,9 +767,26 @@
"PlaceholderNewPlaylist": "Нова назва списку",
"PlaceholderSearch": "Пошук...",
"PlaceholderSearchEpisode": "Шукати епізод...",
+ "StatsAuthorsAdded": "авторів додано",
+ "StatsBooksAdded": "книг додано",
+ "StatsBooksAdditional": "Було додано…",
+ "StatsBooksFinished": "книг завершено",
+ "StatsBooksFinishedThisYear": "Дещо з завершеного цьогоріч…",
+ "StatsBooksListenedTo": "книг, які слухали",
+ "StatsCollectionGrewTo": "Ваша колекція книг зросла до…",
+ "StatsSessions": "сесій",
+ "StatsSpentListening": "слухали",
+ "StatsTopAuthor": "УЛЮБЛЕНИЙ АВТОР",
+ "StatsTopAuthors": "УЛЮБЛЕНІ АВТОРИ",
+ "StatsTopGenre": "УЛЮБЛЕНИЙ ЖАНР",
+ "StatsTopGenres": "УЛЮБЛЕНІ ЖАНРИ",
+ "StatsTopMonth": "НАЙКРАЩИЙ МІСЯЦЬ",
+ "StatsTopNarrator": "УЛЮБЛЕНИЙ ЧИТЕЦЬ",
+ "StatsTopNarrators": "УЛЮБЛЕНІ ЧИТЦІ",
+ "StatsTotalDuration": "Загальною довжиною…",
+ "StatsYearInReview": "ОГЛЯД РОКУ",
"ToastAccountUpdateFailed": "Не вдалося оновити профіль",
"ToastAccountUpdateSuccess": "Профіль оновлено",
- "ToastAuthorImageRemoveFailed": "Не вдалося видалити зображення",
"ToastAuthorImageRemoveSuccess": "Фото автора видалено",
"ToastAuthorUpdateFailed": "Не вдалося оновити автора",
"ToastAuthorUpdateMerged": "Автора об'єднано",
@@ -786,7 +803,6 @@
"ToastBatchUpdateSuccess": "Обрані успішно оновлено",
"ToastBookmarkCreateFailed": "Не вдалося створити закладку",
"ToastBookmarkCreateSuccess": "Закладку додано",
- "ToastBookmarkRemoveFailed": "Не вдалося видалити закладку",
"ToastBookmarkRemoveSuccess": "Закладку видалено",
"ToastBookmarkUpdateFailed": "Не вдалося оновити закладку",
"ToastBookmarkUpdateSuccess": "Закладку оновлено",
@@ -794,20 +810,18 @@
"ToastCachePurgeSuccess": "Кеш очищено",
"ToastChaptersHaveErrors": "Глави містять помилки",
"ToastChaptersMustHaveTitles": "Глави повинні мати назви",
- "ToastCollectionItemsRemoveFailed": "Не вдалося видалити елемент(и) з добірки",
"ToastCollectionItemsRemoveSuccess": "Елемент(и) видалено з добірки",
- "ToastCollectionRemoveFailed": "Не вдалося видалити добірку",
"ToastCollectionRemoveSuccess": "Добірку видалено",
"ToastCollectionUpdateFailed": "Не вдалося оновити добірку",
"ToastCollectionUpdateSuccess": "Добірку оновлено",
"ToastDeleteFileFailed": "Не вдалося видалити файл",
"ToastDeleteFileSuccess": "Файл видалено",
+ "ToastErrorCannotShare": "Не можна типово поширити на цей пристрій",
"ToastFailedToLoadData": "Не вдалося завантажити дані",
"ToastItemCoverUpdateFailed": "Не вдалося оновити обкладинку",
"ToastItemCoverUpdateSuccess": "Обкладинку елемента оновлено",
"ToastItemDetailsUpdateFailed": "Не вдалося оновити подробиці елемента",
"ToastItemDetailsUpdateSuccess": "Подробиці про елемент оновлено",
- "ToastItemDetailsUpdateUnneeded": "Оновлення подробиць непотрібне",
"ToastItemMarkedAsFinishedFailed": "Не вдалося позначити як завершене",
"ToastItemMarkedAsFinishedSuccess": "Елемент позначено як завершений",
"ToastItemMarkedAsNotFinishedFailed": "Не вдалося позначити незавершеним",
@@ -822,7 +836,6 @@
"ToastLibraryUpdateSuccess": "Бібліотеку \"{0}\" оновлено",
"ToastPlaylistCreateFailed": "Не вдалося створити список",
"ToastPlaylistCreateSuccess": "Список відтворення створено",
- "ToastPlaylistRemoveFailed": "Не вдалося видалити список",
"ToastPlaylistRemoveSuccess": "Список відтворення видалено",
"ToastPlaylistUpdateFailed": "Не вдалося оновити список",
"ToastPlaylistUpdateSuccess": "Список відтворення оновлено",
diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json
index b92f481893..02fa75f58d 100644
--- a/client/strings/vi-vn.json
+++ b/client/strings/vi-vn.json
@@ -9,7 +9,6 @@
"ButtonApply": "Áp Dụng",
"ButtonApplyChapters": "Áp Dụng Chương",
"ButtonAuthors": "Tác Giả",
- "ButtonBack": "Back",
"ButtonBrowseForFolder": "Duyệt Thư Mục",
"ButtonCancel": "Hủy",
"ButtonCancelEncode": "Hủy Mã Hóa",
@@ -28,7 +27,6 @@
"ButtonEdit": "Chỉnh Sửa",
"ButtonEditChapters": "Chỉnh Sửa Chương",
"ButtonEditPodcast": "Chỉnh Sửa Podcast",
- "ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Đường Dẫn Đầy Đủ",
"ButtonHide": "Ẩn",
"ButtonHome": "Trang Chủ",
@@ -46,7 +44,6 @@
"ButtonNevermind": "Không Sao",
"ButtonNext": "Tiếp Theo",
"ButtonNextChapter": "Chương Tiếp Theo",
- "ButtonOk": "Ok",
"ButtonOpenFeed": "Mở Feed",
"ButtonOpenManager": "Mở Quản Lý",
"ButtonPause": "Tạm Dừng",
@@ -62,8 +59,6 @@
"ButtonQuickMatch": "Khớp Nhanh",
"ButtonReScan": "Quét Lại",
"ButtonRead": "Đọc",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
"ButtonRefresh": "Làm Mới",
"ButtonRemove": "Xóa",
"ButtonRemoveAll": "Xóa Tất Cả",
@@ -81,7 +76,6 @@
"ButtonScanLibrary": "Quét Thư Viện",
"ButtonSearch": "Tìm Kiếm",
"ButtonSelectFolderPath": "Chọn Đường Dẫn Thư Mục",
- "ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Đặt chương từ các track",
"ButtonShare": "Chia Sẻ",
"ButtonShiftTimes": "Dời Thời Gian",
@@ -115,12 +109,10 @@
"HeaderCollectionItems": "Các Mục Bộ Sưu Tập",
"HeaderCover": "Bìa",
"HeaderCurrentDownloads": "Tải Xuống Hiện Tại",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
"HeaderCustomMetadataProviders": "Các Nhà Cung Cấp Metadata Tùy Chỉnh",
"HeaderDetails": "Chi Tiết",
"HeaderDownloadQueue": "Hàng Đợi Tải Xuống",
"HeaderEbookFiles": "Tệp Ebook",
- "HeaderEmail": "Email",
"HeaderEmailSettings": "Cài Đặt Email",
"HeaderEpisodes": "Tập Phim",
"HeaderEreaderDevices": "Thiết Bị Đọc Sách",
@@ -160,7 +152,6 @@
"HeaderPreviewCover": "Xem Trước Bìa",
"HeaderRSSFeedGeneral": "Chi Tiết RSS",
"HeaderRSSFeedIsOpen": "RSS Feed Đã Mở",
- "HeaderRSSFeeds": "RSS Feeds",
"HeaderRemoveEpisode": "Xóa Tập",
"HeaderRemoveEpisodes": "Xóa {0} Tập",
"HeaderSavedMediaProgress": "Tiến Trình Phương Tiện Đã Lưu",
@@ -190,9 +181,6 @@
"HeaderYearReview": "Năm {0} trong Xem Xét",
"HeaderYourStats": "Thống Kê Của Bạn",
"LabelAbridged": "Rút Gọn",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "Loại Tài Khoản",
"LabelAccountTypeAdmin": "Quản Trị Viên",
"LabelAccountTypeGuest": "Khách",
@@ -201,39 +189,10 @@
"LabelAddToCollection": "Thêm vào Bộ Sưu Tập",
"LabelAddToCollectionBatch": "Thêm {0} Sách vào Bộ Sưu Tập",
"LabelAddToPlaylist": "Thêm vào Danh Sách Phát",
- "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
- "LabelAdded": "Đã Thêm",
"LabelAddedAt": "Đã Thêm Lúc",
- "LabelAdminUsersOnly": "Admin users only",
- "LabelAll": "All",
- "LabelAllUsers": "All Users",
- "LabelAllUsersExcludingGuests": "All users excluding guests",
- "LabelAllUsersIncludingGuests": "All users including guests",
- "LabelAlreadyInYourLibrary": "Already in your library",
- "LabelAppend": "Append",
- "LabelAuthor": "Author",
- "LabelAuthorFirstLast": "Author (First Last)",
- "LabelAuthorLastFirst": "Author (Last, First)",
- "LabelAuthors": "Authors",
- "LabelAutoDownloadEpisodes": "Auto Download Episodes",
- "LabelAutoFetchMetadata": "Auto Fetch Metadata",
- "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
- "LabelAutoLaunch": "Auto Launch",
- "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path
/login?autoLaunch=0
)",
- "LabelAutoRegister": "Auto Register",
- "LabelAutoRegisterDescription": "Automatically create new users after logging in",
- "LabelBackToUser": "Back to User",
- "LabelBackupLocation": "Backup Location",
- "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
- "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB)",
- "LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
- "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",
"LabelBooks": "Sách",
"LabelButtonText": "Nút Văn Bản",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "Đổi Mật Khẩu",
"LabelChannels": "Kênh",
"LabelChapterTitle": "Tiêu đề Chương",
@@ -271,17 +230,10 @@
"LabelDownload": "Tải Xuống",
"LabelDownloadNEpisodes": "Tải Xuống {0} Tập",
"LabelDuration": "Thời Lượng",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "Thời lượng được tìm thấy:",
- "LabelEbook": "Ebook",
"LabelEbooks": "Các Ebook",
"LabelEdit": "Chỉnh Sửa",
- "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Địa chỉ Gửi từ",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "Bảo Mật",
"LabelEmailSettingsSecureHelp": "Nếu đúng thì kết nối sẽ sử dụng TLS khi kết nối đến máy chủ. Nếu sai thì TLS sẽ được sử dụng nếu máy chủ hỗ trợ phần mở rộng STARTTLS. Trong hầu hết các trường hợp, hãy đặt giá trị này là đúng nếu bạn kết nối đến cổng 465. Đối với cổng 587 hoặc 25, giữ nó sai. (từ nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Địa Chỉ Kiểm Tra",
@@ -293,8 +245,6 @@
"LabelEpisodeType": "Loại Tập",
"LabelExample": "Ví Dụ",
"LabelExplicit": "Rõ Ràng",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "URL Feed",
"LabelFetchingMetadata": "Đang Lấy Metadata",
"LabelFile": "Tệp",
@@ -307,7 +257,6 @@
"LabelFolder": "Thư Mục",
"LabelFolders": "Các Thư Mục",
"LabelFontBold": "Đậm",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Gia đình font",
"LabelFontItalic": "Nghiêng",
"LabelFontScale": "Tỷ lệ font",
@@ -339,7 +288,6 @@
"LabelItem": "Mục",
"LabelLanguage": "Ngôn ngữ",
"LabelLanguageDefaultServer": "Ngôn ngữ Máy chủ mặc định",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "Sách mới nhất được thêm",
"LabelLastBookUpdated": "Sách mới nhất được cập nhật",
"LabelLastSeen": "Lần cuối nhìn thấy",
@@ -351,7 +299,6 @@
"LabelLess": "Ít hơn",
"LabelLibrariesAccessibleToUser": "Thư viện có thể truy cập cho người dùng",
"LabelLibrary": "Thư viện",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Mục thư viện",
"LabelLibraryName": "Tên thư viện",
"LabelLimit": "Giới hạn",
@@ -387,7 +334,6 @@
"LabelNewestEpisodes": "Tập mới nhất",
"LabelNextBackupDate": "Ngày sao lưu tiếp theo",
"LabelNextScheduledRun": "Chạy tiếp theo theo lịch trình",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "Không có tập nào được chọn",
"LabelNotFinished": "Chưa hoàn thành",
"LabelNotStarted": "Chưa bắt đầu",
@@ -403,9 +349,6 @@
"LabelNotificationsMaxQueueSizeHelp": "Các sự kiện bị giới hạn mỗi giây chỉ gửi 1 lần. Các sự kiện sẽ bị bỏ qua nếu hàng đợi đạt kích thước tối đa. Điều này ngăn chặn spam thông báo.",
"LabelNumberOfBooks": "Số lượng Sách",
"LabelNumberOfEpisodes": "# của Tập",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "Mở RSS Feed",
"LabelOverwrite": "Ghi đè",
"LabelPassword": "Mật khẩu",
@@ -420,9 +363,7 @@
"LabelPersonalYearReview": "Năm của Bạn trong Bài Đánh Giá ({0})",
"LabelPhotoPathURL": "Đường dẫn/URL ảnh",
"LabelPlayMethod": "Phương pháp phát",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "Danh sách phát",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Vùng tìm kiếm podcast",
"LabelPodcastType": "Loại Podcast",
"LabelPodcasts": "Các podcast",
@@ -435,7 +376,6 @@
"LabelPubDate": "Ngày Xuất bản",
"LabelPublishYear": "Năm Xuất bản",
"LabelPublisher": "Nhà xuất bản",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Email chủ sở hữu tùy chỉnh",
"LabelRSSFeedCustomOwnerName": "Tên chủ sở hữu tùy chỉnh",
"LabelRSSFeedOpen": "Mở RSS Feed",
@@ -457,7 +397,6 @@
"LabelSearchTitle": "Tìm kiếm Tiêu đề",
"LabelSearchTitleOrASIN": "Tìm kiếm Tiêu đề hoặc ASIN",
"LabelSeason": "Mùa",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Chọn tất cả các tập",
"LabelSelectEpisodesShowing": "Chọn {0} tập đang hiển thị",
"LabelSelectUsers": "Chọn người dùng",
@@ -480,8 +419,6 @@
"LabelSettingsEnableWatcher": "Bật Watcher",
"LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện",
"LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm",
"LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.",
"LabelSettingsFindCovers": "Tìm ảnh bìa",
@@ -490,8 +427,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "Các loạt sách chỉ có một cuốn sách sẽ được ẩn khỏi trang loạt sách và kệ trang chủ.",
"LabelSettingsHomePageBookshelfView": "Trang chủ sử dụng chế độ xem kệ sách",
"LabelSettingsLibraryBookshelfView": "Thư viện sử dụng chế độ xem kệ sách",
- "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": "Phân tích phụ đề",
"LabelSettingsParseSubtitlesHelp": "Trích xuất phụ đề từ tên thư mục sách nói.
Phụ đề phải được tách bằng \" - \"
i.e. \"Book Title - A Subtitle Here\" có phụ đề \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Ưu tiên siêu dữ liệu phù hợp",
@@ -508,15 +443,12 @@
"LabelSettingsStoreMetadataWithItemHelp": "Theo mặc định, các tệp siêu dữ liệu được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ các tệp siêu dữ liệu trong các thư mục mục của thư viện bạn",
"LabelSettingsTimeFormat": "Định dạng Thời gian",
"LabelShowAll": "Hiển thị Tất cả",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "Kích thước",
"LabelSleepTimer": "Hẹn giờ tắt",
- "LabelSlug": "Slug",
"LabelStart": "Bắt đầu",
"LabelStartTime": "Thời gian bắt đầu",
"LabelStarted": "Đã bắt đầu",
"LabelStartedAt": "Bắt đầu vào",
- "LabelStatsAudioTracks": "Audio Tracks",
"LabelStatsAuthors": "Tác giả",
"LabelStatsBestDay": "Ngày tốt nhất",
"LabelStatsDailyAverage": "Trung bình hàng ngày",
@@ -601,7 +533,6 @@
"MessageBookshelfNoCollections": "Bạn chưa tạo bất kỳ bộ sưu tập nào",
"MessageBookshelfNoRSSFeeds": "Không có nguồn cung cấp RSS nào đang mở",
"MessageBookshelfNoResultsForFilter": "Không có Kết quả cho bộ lọc \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "Bạn không có bộ sách",
"MessageChapterEndIsAfter": "Kết thúc chương sau khi kết thúc sách nói của bạn",
"MessageChapterErrorFirstNotZero": "Chương đầu tiên phải bắt đầu từ 0",
@@ -621,8 +552,6 @@
"MessageConfirmMarkAllEpisodesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim chưa kết thúc không?",
"MessageConfirmMarkSeriesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này đã kết thúc không?",
"MessageConfirmMarkSeriesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này chưa kết thúc không?",
- "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": "Cảnh báo! Quick embed sẽ không sao lưu các tệp âm thanh của bạn. Đảm bảo bạn có một bản sao lưu của các tệp âm thanh của bạn.
Bạn có muốn tiếp tục không?",
"MessageConfirmReScanLibraryItems": "Bạn có chắc chắn muốn quét lại {0} mục không?",
"MessageConfirmRemoveAllChapters": "Bạn có chắc chắn muốn xóa tất cả các chương không?",
@@ -644,7 +573,6 @@
"MessageDragFilesIntoTrackOrder": "Kéo tệp vào thứ tự track đúng",
"MessageEmbedFinished": "Nhúng Hoàn thành!",
"MessageEpisodesQueuedForDownload": "{0} Tập(s) đã được thêm vào hàng đợi để tải xuống",
- "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": "URL nguồn cấp sẽ là {0}",
"MessageFetching": "Đang tìm...",
"MessageForceReScanDescription": "sẽ quét lại tất cả các tệp như một quét mới. Các thẻ ID3 của tệp âm thanh, tệp OPF và tệp văn bản sẽ được quét làm mới.",
@@ -656,7 +584,6 @@
"MessageListeningSessionsInTheLastYear": "{0} phiên nghe trong năm qua",
"MessageLoading": "Đang tải...",
"MessageLoadingFolders": "Đang tải các thư mục...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B thất bại!",
"MessageM4BFinished": "M4B Hoàn thành!",
"MessageMapChapterTitles": "Ánh xạ tiêu đề chương với các chương hiện có của sách audio của bạn mà không điều chỉnh thời gian",
@@ -692,7 +619,6 @@
"MessageNoSeries": "Không có Bộ",
"MessageNoTags": "Không có Thẻ",
"MessageNoTasksRunning": "Không có Công việc đang chạy",
- "MessageNoUpdateNecessary": "Không cần cập nhật",
"MessageNoUpdatesWereNecessary": "Không cần cập nhật",
"MessageNoUserPlaylists": "Bạn chưa có danh sách phát",
"MessageNotYetImplemented": "Chưa được triển khai",
@@ -739,7 +665,6 @@
"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",
- "ToastAuthorImageRemoveFailed": "Không thể xóa ảnh tác giả",
"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",
@@ -756,28 +681,19 @@
"ToastBatchUpdateSuccess": "Cập nhật nhóm thành công",
"ToastBookmarkCreateFailed": "Tạo đánh dấu thất bại",
"ToastBookmarkCreateSuccess": "Đã thêm đánh dấu",
- "ToastBookmarkRemoveFailed": "Xóa đánh dấu thất bại",
"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",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Các chương có lỗi",
"ToastChaptersMustHaveTitles": "Các chương phải có tiêu đề",
- "ToastCollectionItemsRemoveFailed": "Xóa mục từ bộ sưu tập thất bại",
"ToastCollectionItemsRemoveSuccess": "Mục đã được xóa khỏi bộ sưu tập",
- "ToastCollectionRemoveFailed": "Xóa bộ sưu tập thất bại",
"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",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"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",
- "ToastItemDetailsUpdateUnneeded": "Không cần cập nhật chi tiết mục",
"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",
"ToastItemMarkedAsNotFinishedFailed": "Đánh dấu mục là Chưa hoàn thành thất bại",
@@ -792,7 +708,6 @@
"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",
- "ToastPlaylistRemoveFailed": "Xóa danh sách phát thất bại",
"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",
@@ -806,16 +721,11 @@
"ToastSendEbookToDeviceSuccess": "Ebook đã được gửi đến thiết bị \"{0}\"",
"ToastSeriesUpdateFailed": "Cập nhật loạt truyện thất bại",
"ToastSeriesUpdateSuccess": "Cập nhật loạt truyện thành công",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "Xóa phiên thất bại",
"ToastSessionDeleteSuccess": "Phiên đã được xóa",
"ToastSocketConnected": "Kết nối socket",
"ToastSocketDisconnected": "Ngắt kết nối socket",
"ToastSocketFailedToConnect": "Không thể kết nối socket",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "Xóa người dùng thất bại",
"ToastUserDeleteSuccess": "Người dùng đã được xóa"
}
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index b15eb99e9d..df0d77f7c7 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.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,6 +50,7 @@
"ButtonNevermind": "没有关系",
"ButtonNext": "下一个",
"ButtonNextChapter": "下一章节",
+ "ButtonNextItemInQueue": "队列中的下一个项目",
"ButtonOk": "确定",
"ButtonOpenFeed": "打开源",
"ButtonOpenManager": "打开管理器",
@@ -55,6 +60,7 @@
"ButtonPlaylists": "播放列表",
"ButtonPrevious": "上一个",
"ButtonPreviousChapter": "上一章节",
+ "ButtonProbeAudioFile": "探测音频文件",
"ButtonPurgeAllCache": "清理所有缓存",
"ButtonPurgeItemsCache": "清理项目缓存",
"ButtonQueueAddItem": "添加到队列",
@@ -92,6 +98,7 @@
"ButtonStats": "统计数据",
"ButtonSubmit": "提交",
"ButtonTest": "测试",
+ "ButtonUnlinkOpenId": "取消 OpenID 链接",
"ButtonUpload": "上传",
"ButtonUploadBackup": "上传备份",
"ButtonUploadCover": "上传封面",
@@ -104,6 +111,7 @@
"ErrorUploadFetchMetadataNoResults": "无法获取元数据 - 尝试更新标题和/或作者",
"ErrorUploadLacksTitle": "必须有标题",
"HeaderAccount": "帐户",
+ "HeaderAddCustomMetadataProvider": "添加自定义元数据提供商",
"HeaderAdvanced": "高级",
"HeaderAppriseNotificationSettings": "测试通知设置",
"HeaderAudioTracks": "音轨",
@@ -118,7 +126,7 @@
"HeaderCover": "封面",
"HeaderCurrentDownloads": "当前下载",
"HeaderCustomMessageOnLogin": "登录时的自定义消息",
- "HeaderCustomMetadataProviders": "自定义元数据提供者",
+ "HeaderCustomMetadataProviders": "自定义元数据提供商",
"HeaderDetails": "详情",
"HeaderDownloadQueue": "下载队列",
"HeaderEbookFiles": "电子书文件",
@@ -149,6 +157,8 @@
"HeaderMetadataToEmbed": "嵌入元数据",
"HeaderNewAccount": "新建帐户",
"HeaderNewLibrary": "新建媒体库",
+ "HeaderNotificationCreate": "创建通知",
+ "HeaderNotificationUpdate": "更新通知",
"HeaderNotifications": "通知",
"HeaderOpenIDConnectAuthentication": "OpenID 连接身份验证",
"HeaderOpenRSSFeed": "打开 RSS 源",
@@ -205,8 +215,8 @@
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
"LabelAddToPlaylist": "添加到播放列表",
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
- "LabelAdded": "添加",
"LabelAddedAt": "添加于",
+ "LabelAddedDate": "添加 {0}",
"LabelAdminUsersOnly": "仅限管理员用户",
"LabelAll": "全部",
"LabelAllUsers": "所有用户",
@@ -236,7 +246,7 @@
"LabelBitrate": "比特率",
"LabelBooks": "图书",
"LabelButtonText": "按钮文本",
- "LabelByAuthor": "by {0}",
+ "LabelByAuthor": "由 {0}",
"LabelChangePassword": "修改密码",
"LabelChannels": "声道",
"LabelChapterTitle": "章节标题",
@@ -246,6 +256,7 @@
"LabelClosePlayer": "关闭播放器",
"LabelCodec": "编解码",
"LabelCollapseSeries": "折叠系列",
+ "LabelCollapseSubSeries": "折叠子系列",
"LabelCollection": "收藏",
"LabelCollections": "收藏",
"LabelComplete": "已完成",
@@ -296,8 +307,10 @@
"LabelEpisode": "剧集",
"LabelEpisodeTitle": "剧集标题",
"LabelEpisodeType": "剧集类型",
+ "LabelEpisodes": "剧集",
"LabelExample": "示例",
"LabelExpandSeries": "展开系列",
+ "LabelExpandSubSeries": "展开子系列",
"LabelExplicit": "信息准确",
"LabelExplicitChecked": "明确(已选中)",
"LabelExplicitUnchecked": "不明确 (未选中)",
@@ -306,7 +319,9 @@
"LabelFetchingMetadata": "正在获取元数据",
"LabelFile": "文件",
"LabelFileBirthtime": "文件创建时间",
+ "LabelFileBornDate": "生于 {0}",
"LabelFileModified": "文件修改时间",
+ "LabelFileModifiedDate": "已修改 {0}",
"LabelFilename": "文件名",
"LabelFilterByUser": "按用户筛选",
"LabelFindEpisodes": "查找剧集",
@@ -325,7 +340,7 @@
"LabelHardDeleteFile": "完全删除文件",
"LabelHasEbook": "有电子书",
"LabelHasSupplementaryEbook": "有补充电子书",
- "LabelHideSubtitles": "隐藏标题",
+ "LabelHideSubtitles": "隐藏副标题",
"LabelHighestPriority": "最高优先级",
"LabelHost": "主机",
"LabelHour": "小时",
@@ -362,7 +377,7 @@
"LabelLess": "较少",
"LabelLibrariesAccessibleToUser": "用户可访问的媒体库",
"LabelLibrary": "媒体库",
- "LabelLibraryFilterSublistEmpty": "No {0}",
+ "LabelLibraryFilterSublistEmpty": "没有 {0}",
"LabelLibraryItem": "媒体库项目",
"LabelLibraryName": "媒体库名称",
"LabelLimit": "限制",
@@ -374,13 +389,13 @@
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
"LabelLowestPriority": "最低优先级",
"LabelMatchExistingUsersBy": "匹配现有用户",
- "LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过SSO提供商提供的唯一 id 进行匹配",
+ "LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过 SSO 提供商提供的唯一 id 进行匹配",
"LabelMediaPlayer": "媒体播放器",
"LabelMediaType": "媒体类型",
"LabelMetaTag": "元数据标签",
"LabelMetaTags": "元标签",
"LabelMetadataOrderOfPrecedenceDescription": "较高优先级的元数据源将覆盖较低优先级的元数据源",
- "LabelMetadataProvider": "元数据提供者",
+ "LabelMetadataProvider": "元数据提供商",
"LabelMinute": "分钟",
"LabelMinutes": "分钟",
"LabelMissing": "丢失",
@@ -399,7 +414,7 @@
"LabelNewestEpisodes": "最新剧集",
"LabelNextBackupDate": "下次备份日期",
"LabelNextScheduledRun": "下次任务运行",
- "LabelNoCustomMetadataProviders": "没有自定义元数据提供程序",
+ "LabelNoCustomMetadataProviders": "没有自定义元数据提供商",
"LabelNoEpisodesSelected": "未选择任何剧集",
"LabelNotFinished": "未听完",
"LabelNotStarted": "未开始",
@@ -415,7 +430,7 @@
"LabelNotificationsMaxQueueSizeHelp": "通知事件被限制为每秒触发 1 个. 如果队列处于最大大小, 则将忽略事件. 这可以防止通知垃圾邮件.",
"LabelNumberOfBooks": "图书数量",
"LabelNumberOfEpisodes": "# 集",
- "LabelOpenIDAdvancedPermsClaimDescription": "OpenID 声明的名称, 该声明包含应用程序内用户操作的高级权限, 该权限将应用于非管理员角色(
如果已配置 ). 如果响应中缺少声明, 获取 ABS 的权限将被拒绝. 如果缺少单个选项, 它将被视为
禁用
. 确保身份提供者的声明与预期结构匹配:",
+ "LabelOpenIDAdvancedPermsClaimDescription": "OpenID 声明的名称, 该声明包含应用程序内用户操作的高级权限, 该权限将应用于非管理员角色(
如果已配置 ). 如果响应中缺少声明, 获取 ABS 的权限将被拒绝. 如果缺少单个选项, 它将被视为
禁用
. 确保身份提供商的声明与预期结构匹配:",
"LabelOpenIDClaims": "将以下选项留空以禁用高级组和权限分配, 然后自动分配 'User' 组.",
"LabelOpenIDGroupClaimDescription": "OpenID 声明的名称, 该声明包含用户组的列表. 通常称为
组
如果已配置 , 应用程序将根据用户的组成员身份自动分配角色, 前提是这些组在声明中以不区分大小写的方式命名为 'Admin', 'User' 或 'Guest'. 声明应包含一个列表, 如果用户属于多个组, 则应用程序将分配与最高访问级别相对应的角色. 如果没有组匹配, 访问将被拒绝.",
"LabelOpenRSSFeed": "打开 RSS 源",
@@ -433,7 +448,7 @@
"LabelPersonalYearReview": "你的年度回顾 ({0})",
"LabelPhotoPathURL": "图片路径或 URL",
"LabelPlayMethod": "播放方法",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
+ "LabelPlayerChapterNumberMarker": "{0} 于 {1}",
"LabelPlaylists": "播放列表",
"LabelPodcast": "播客",
"LabelPodcastSearchRegion": "播客搜索地区",
@@ -444,9 +459,11 @@
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引",
"LabelPrimaryEbook": "主电子书",
"LabelProgress": "进度",
- "LabelProvider": "供应商",
+ "LabelProvider": "提供商",
+ "LabelProviderAuthorizationValue": "授权标头值",
"LabelPubDate": "出版日期",
"LabelPublishYear": "发布年份",
+ "LabelPublishedDate": "已发布 {0}",
"LabelPublisher": "出版商",
"LabelPublishers": "出版商",
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
@@ -592,6 +609,7 @@
"LabelUnabridged": "未删节",
"LabelUndo": "撤消",
"LabelUnknown": "未知",
+ "LabelUnknownPublishDate": "未知发布日期",
"LabelUpdateCover": "更新封面",
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
"LabelUpdateDetails": "更新详细信息",
@@ -640,16 +658,22 @@
"MessageCheckingCron": "检查计划任务...",
"MessageConfirmCloseFeed": "你确定要关闭此订阅源吗?",
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
+ "MessageConfirmDeleteDevice": "您确定要删除电子阅读器设备 \"{0}\" 吗?",
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "这将从数据库和文件系统中删除库项目. 你确定吗?",
"MessageConfirmDeleteLibraryItems": "这将从数据库和文件系统中删除 {0} 个库项目. 你确定吗?",
+ "MessageConfirmDeleteMetadataProvider": "是否确实要删除自定义元数据提供商 \"{0}\" ?",
+ "MessageConfirmDeleteNotification": "您确定要删除此通知吗?",
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
"MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?",
"MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?",
+ "MessageConfirmMarkItemFinished": "您确定要将 \"{0}\" 标记为已完成吗?",
+ "MessageConfirmMarkItemNotFinished": "您确定要将 \"{0}\" 标记为未完成吗?",
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
+ "MessageConfirmNotificationTestTrigger": "使用测试数据触发此通知吗?",
"MessageConfirmPurgeCache": "清除缓存将删除
/metadata/cache
整个目录.
你确定要删除缓存目录吗?",
"MessageConfirmPurgeItemsCache": "清除项目缓存将删除
/metadata/cache/items
整个目录.
你确定吗?",
"MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份.
你是否想继续吗?",
@@ -668,7 +692,9 @@
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
+ "MessageConfirmResetProgress": "你确定要重置进度吗?",
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
+ "MessageConfirmUnlinkOpenId": "您确定要取消该用户与 OpenID 的链接吗?",
"MessageDownloadingEpisode": "正在下载剧集",
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
"MessageEmbedFailed": "嵌入失败!",
@@ -703,6 +729,7 @@
"MessageNoCollections": "没有收藏",
"MessageNoCoversFound": "没有找到封面",
"MessageNoDescription": "没有描述",
+ "MessageNoDevices": "没有设备",
"MessageNoDownloadsInProgress": "当前没有正在进行的下载",
"MessageNoDownloadsQueued": "下载队列无任务",
"MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项",
@@ -722,7 +749,6 @@
"MessageNoSeries": "无系列",
"MessageNoTags": "无标签",
"MessageNoTasksRunning": "没有正在运行的任务",
- "MessageNoUpdateNecessary": "无需更新",
"MessageNoUpdatesWereNecessary": "无需更新",
"MessageNoUserPlaylists": "你没有播放列表",
"MessageNotYetImplemented": "尚未实施",
@@ -731,6 +757,7 @@
"MessagePauseChapter": "暂停章节播放",
"MessagePlayChapter": "开始章节播放",
"MessagePlaylistCreateFromCollection": "从收藏中创建播放列表",
+ "MessagePleaseWait": "请稍等...",
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
"MessageRemoveChapter": "移除章节",
@@ -771,26 +798,52 @@
"PlaceholderNewPlaylist": "输入播放列表名称",
"PlaceholderSearch": "查找..",
"PlaceholderSearchEpisode": "搜索剧集..",
+ "StatsAuthorsAdded": "添加作者",
+ "StatsBooksAdded": "添加书籍",
+ "StatsBooksAdditional": "一些新增内容包括…",
+ "StatsBooksFinished": "已完成书籍",
+ "StatsBooksFinishedThisYear": "今年完成的一些书…",
+ "StatsBooksListenedTo": "听过的书",
+ "StatsCollectionGrewTo": "您的藏书已增长到…",
+ "StatsSessions": "会话",
+ "StatsSpentListening": "花时间聆听",
+ "StatsTopAuthor": "热门作者",
+ "StatsTopAuthors": "热门作者",
+ "StatsTopGenre": "热门流派",
+ "StatsTopGenres": "热门流派",
+ "StatsTopMonth": "最佳月份",
+ "StatsTopNarrator": "最佳叙述者",
+ "StatsTopNarrators": "最佳叙述者",
+ "StatsTotalDuration": "总时长为…",
+ "StatsYearInReview": "年度回顾",
"ToastAccountUpdateFailed": "账户更新失败",
"ToastAccountUpdateSuccess": "帐户已更新",
- "ToastAuthorImageRemoveFailed": "作者图像删除失败",
+ "ToastAppriseUrlRequired": "必须输入 Apprise URL",
"ToastAuthorImageRemoveSuccess": "作者图像已删除",
+ "ToastAuthorNotFound": "未找到作者 \"{0}\"",
+ "ToastAuthorRemoveSuccess": "作者已删除",
+ "ToastAuthorSearchNotFound": "未找到作者",
"ToastAuthorUpdateFailed": "作者更新失败",
"ToastAuthorUpdateMerged": "作者已合并",
"ToastAuthorUpdateSuccess": "作者已更新",
"ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到图像)",
+ "ToastBackupAppliedSuccess": "已应用备份",
"ToastBackupCreateFailed": "备份创建失败",
"ToastBackupCreateSuccess": "备份已创建",
"ToastBackupDeleteFailed": "备份删除失败",
"ToastBackupDeleteSuccess": "备份已删除",
+ "ToastBackupInvalidMaxKeep": "要保留的备份数无效",
+ "ToastBackupInvalidMaxSize": "最大备份大小无效",
+ "ToastBackupPathUpdateFailed": "无法更新备份路径",
"ToastBackupRestoreFailed": "备份还原失败",
"ToastBackupUploadFailed": "上传备份失败",
"ToastBackupUploadSuccess": "备份已上传",
+ "ToastBatchDeleteFailed": "批量删除失败",
+ "ToastBatchDeleteSuccess": "批量删除成功",
"ToastBatchUpdateFailed": "批量更新失败",
"ToastBatchUpdateSuccess": "批量更新成功",
"ToastBookmarkCreateFailed": "创建书签失败",
"ToastBookmarkCreateSuccess": "书签已添加",
- "ToastBookmarkRemoveFailed": "书签删除失败",
"ToastBookmarkRemoveSuccess": "书签已删除",
"ToastBookmarkUpdateFailed": "书签更新失败",
"ToastBookmarkUpdateSuccess": "书签已更新",
@@ -798,24 +851,46 @@
"ToastCachePurgeSuccess": "缓存清除成功",
"ToastChaptersHaveErrors": "章节有错误",
"ToastChaptersMustHaveTitles": "章节必须有标题",
- "ToastCollectionItemsRemoveFailed": "从收藏夹移除项目失败",
+ "ToastChaptersRemoved": "已删除章节",
+ "ToastCollectionItemsAddFailed": "项目添加到收藏夹失败",
+ "ToastCollectionItemsAddSuccess": "项目添加到收藏夹成功",
"ToastCollectionItemsRemoveSuccess": "项目从收藏夹移除",
- "ToastCollectionRemoveFailed": "删除收藏夹失败",
"ToastCollectionRemoveSuccess": "收藏夹已删除",
"ToastCollectionUpdateFailed": "更新收藏夹失败",
"ToastCollectionUpdateSuccess": "收藏夹已更新",
+ "ToastCoverUpdateFailed": "封面更新失败",
"ToastDeleteFileFailed": "删除文件失败",
"ToastDeleteFileSuccess": "文件已删除",
+ "ToastDeviceAddFailed": "添加设备失败",
+ "ToastDeviceNameAlreadyExists": "同名的电子阅读器设备已存在",
+ "ToastDeviceTestEmailFailed": "无法发送测试电子邮件",
+ "ToastDeviceTestEmailSuccess": "测试邮件已发送",
+ "ToastDeviceUpdateFailed": "无法更新设备",
+ "ToastEmailSettingsUpdateFailed": "无法更新电子邮件设置",
+ "ToastEmailSettingsUpdateSuccess": "电子邮件设置已更新",
+ "ToastEncodeCancelFailed": "取消编码失败",
+ "ToastEncodeCancelSucces": "编码已取消",
+ "ToastEpisodeDownloadQueueClearFailed": "无法清除队列",
+ "ToastEpisodeDownloadQueueClearSuccess": "剧集下载队列已清空",
+ "ToastErrorCannotShare": "无法在此设备上本地共享",
"ToastFailedToLoadData": "加载数据失败",
+ "ToastFailedToShare": "分享失败",
+ "ToastFailedToUpdateAccount": "无法更新账户",
+ "ToastFailedToUpdateUser": "无法更新用户",
+ "ToastInvalidImageUrl": "图片网址无效",
+ "ToastInvalidUrl": "网址无效",
"ToastItemCoverUpdateFailed": "更新项目封面失败",
"ToastItemCoverUpdateSuccess": "项目封面已更新",
+ "ToastItemDeletedFailed": "删除项目失败",
+ "ToastItemDeletedSuccess": "已删除项目",
"ToastItemDetailsUpdateFailed": "更新项目详细信息失败",
"ToastItemDetailsUpdateSuccess": "项目详细信息已更新",
- "ToastItemDetailsUpdateUnneeded": "项目详细信息无需更新",
"ToastItemMarkedAsFinishedFailed": "无法标记为已听完",
"ToastItemMarkedAsFinishedSuccess": "标记为已听完的项目",
"ToastItemMarkedAsNotFinishedFailed": "无法标记为未听完",
"ToastItemMarkedAsNotFinishedSuccess": "标记为未听完的项目",
+ "ToastItemUpdateFailed": "更新项目失败",
+ "ToastItemUpdateSuccess": "项目已更新",
"ToastLibraryCreateFailed": "创建媒体库失败",
"ToastLibraryCreateSuccess": "媒体库 \"{0}\" 创建成功",
"ToastLibraryDeleteFailed": "删除媒体库失败",
@@ -824,32 +899,78 @@
"ToastLibraryScanStarted": "媒体库扫描已启动",
"ToastLibraryUpdateFailed": "更新图书库失败",
"ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新",
+ "ToastNameEmailRequired": "姓名和电子邮件为必填项",
+ "ToastNameRequired": "姓名为必填项",
+ "ToastNewUserCreatedFailed": "无法创建帐户: \"{0}\"",
+ "ToastNewUserCreatedSuccess": "已创建新帐户",
+ "ToastNewUserLibraryError": "必须至少选择一个图书馆",
+ "ToastNewUserPasswordError": "必须有密码, 只有root用户可以有空密码",
+ "ToastNewUserTagError": "必须至少选择一个标签",
+ "ToastNewUserUsernameError": "输入用户名",
+ "ToastNoUpdatesNecessary": "无需更新",
+ "ToastNotificationCreateFailed": "无法创建通知",
+ "ToastNotificationDeleteFailed": "删除通知失败",
+ "ToastNotificationFailedMaximum": "最大失败尝试次数必须 >= 0",
+ "ToastNotificationQueueMaximum": "最大通知队列必须 >= 0",
+ "ToastNotificationSettingsUpdateFailed": "无法更新通知设置",
+ "ToastNotificationSettingsUpdateSuccess": "通知设置已更新",
+ "ToastNotificationTestTriggerFailed": "无法触发测试通知",
+ "ToastNotificationTestTriggerSuccess": "触发测试通知",
+ "ToastNotificationUpdateFailed": "更新通知失败",
+ "ToastNotificationUpdateSuccess": "通知已更新",
"ToastPlaylistCreateFailed": "创建播放列表失败",
"ToastPlaylistCreateSuccess": "已成功创建播放列表",
- "ToastPlaylistRemoveFailed": "删除播放列表失败",
"ToastPlaylistRemoveSuccess": "播放列表已删除",
"ToastPlaylistUpdateFailed": "更新播放列表失败",
"ToastPlaylistUpdateSuccess": "播放列表已更新",
"ToastPodcastCreateFailed": "创建播客失败",
"ToastPodcastCreateSuccess": "已成功创建播客",
+ "ToastPodcastGetFeedFailed": "无法获取播客信息",
+ "ToastPodcastNoEpisodesInFeed": "RSS 订阅中未找到任何剧集",
+ "ToastPodcastNoRssFeed": "播客没有 RSS 源",
+ "ToastProviderCreatedFailed": "无法添加提供商",
+ "ToastProviderCreatedSuccess": "已添加新提供商",
+ "ToastProviderNameAndUrlRequired": "名称和网址必需填写",
+ "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": "会话已删除",
+ "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/zh-tw.json b/client/strings/zh-tw.json
index 8687053f84..be023813be 100644
--- a/client/strings/zh-tw.json
+++ b/client/strings/zh-tw.json
@@ -9,7 +9,6 @@
"ButtonApply": "應用",
"ButtonApplyChapters": "應用到章節",
"ButtonAuthors": "作者",
- "ButtonBack": "Back",
"ButtonBrowseForFolder": "瀏覽資料夾",
"ButtonCancel": "取消",
"ButtonCancelEncode": "取消編碼",
@@ -33,8 +32,6 @@
"ButtonHide": "隱藏",
"ButtonHome": "首頁",
"ButtonIssues": "問題",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
"ButtonLatest": "最新",
"ButtonLibrary": "媒體庫",
"ButtonLogout": "登出",
@@ -53,7 +50,6 @@
"ButtonPlay": "播放",
"ButtonPlaying": "正在播放",
"ButtonPlaylists": "播放列表",
- "ButtonPrevious": "Previous",
"ButtonPreviousChapter": "過去的章節",
"ButtonPurgeAllCache": "清理所有快取",
"ButtonPurgeItemsCache": "清理項目快取",
@@ -62,8 +58,6 @@
"ButtonQuickMatch": "快速匹配",
"ButtonReScan": "重新掃描",
"ButtonRead": "讀取",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
"ButtonRefresh": "重整",
"ButtonRemove": "移除",
"ButtonRemoveAll": "移除所有",
@@ -83,7 +77,6 @@
"ButtonSelectFolderPath": "選擇資料夾路徑",
"ButtonSeries": "系列",
"ButtonSetChaptersFromTracks": "將音軌設定為章節",
- "ButtonShare": "Share",
"ButtonShiftTimes": "快速調整時間",
"ButtonShow": "顯示",
"ButtonStartM4BEncode": "開始 M4B 編碼",
@@ -115,7 +108,6 @@
"HeaderCollectionItems": "收藏項目",
"HeaderCover": "封面",
"HeaderCurrentDownloads": "當前下載",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
"HeaderCustomMetadataProviders": "自訂 Metadata 提供者",
"HeaderDetails": "詳情",
"HeaderDownloadQueue": "下載佇列",
@@ -187,12 +179,8 @@
"HeaderUpdateDetails": "更新詳情",
"HeaderUpdateLibrary": "更新媒體庫",
"HeaderUsers": "使用者",
- "HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "你的統計數據",
"LabelAbridged": "概要",
- "LabelAbridgedChecked": "Abridged (checked)",
- "LabelAbridgedUnchecked": "Unabridged (unchecked)",
- "LabelAccessibleBy": "Accessible by",
"LabelAccountType": "帳號類型",
"LabelAccountTypeAdmin": "管理員",
"LabelAccountTypeGuest": "來賓",
@@ -202,7 +190,6 @@
"LabelAddToCollectionBatch": "批量新增 {0} 個媒體到收藏",
"LabelAddToPlaylist": "新增到播放列表",
"LabelAddToPlaylistBatch": "新增 {0} 個項目到播放列表",
- "LabelAdded": "新增",
"LabelAddedAt": "新增於",
"LabelAdminUsersOnly": "僅限管理員使用者",
"LabelAll": "全部",
@@ -233,7 +220,6 @@
"LabelBitrate": "位元率",
"LabelBooks": "圖書",
"LabelButtonText": "按鈕文本",
- "LabelByAuthor": "by {0}",
"LabelChangePassword": "修改密碼",
"LabelChannels": "聲道",
"LabelChapterTitle": "章節標題",
@@ -271,17 +257,12 @@
"LabelDownload": "下載",
"LabelDownloadNEpisodes": "下載 {0} 集",
"LabelDuration": "持續時間",
- "LabelDurationComparisonExactMatch": "(exact match)",
- "LabelDurationComparisonLonger": "({0} longer)",
- "LabelDurationComparisonShorter": "({0} shorter)",
"LabelDurationFound": "找到持續時間:",
"LabelEbook": "電子書",
"LabelEbooks": "電子書",
"LabelEdit": "編輯",
"LabelEmail": "郵箱",
"LabelEmailSettingsFromAddress": "發件人位址",
- "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
- "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
"LabelEmailSettingsSecure": "安全",
"LabelEmailSettingsSecureHelp": "如果選是, 則連接將在連接到伺服器時使用TLS. 如果選否, 則若伺服器支援STARTTLS擴展, 則使用TLS. 在大多數情況下, 如果連接到465埠, 請將該值設定為是. 對於587或25埠, 請保持為否. (來自nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "測試位址",
@@ -293,8 +274,6 @@
"LabelEpisodeType": "劇集類型",
"LabelExample": "示例",
"LabelExplicit": "信息準確",
- "LabelExplicitChecked": "Explicit (checked)",
- "LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "源 URL",
"LabelFetchingMetadata": "正在獲取元數據",
"LabelFile": "檔案",
@@ -306,8 +285,6 @@
"LabelFinished": "已聽完",
"LabelFolder": "資料夾",
"LabelFolders": "資料夾",
- "LabelFontBold": "Bold",
- "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "字體系列",
"LabelFontItalic": "斜體",
"LabelFontScale": "字體比例",
@@ -339,7 +316,6 @@
"LabelItem": "項目",
"LabelLanguage": "語言",
"LabelLanguageDefaultServer": "預設伺服器語言",
- "LabelLanguages": "Languages",
"LabelLastBookAdded": "最後新增的書",
"LabelLastBookUpdated": "最後更新的書",
"LabelLastSeen": "上次查看時間",
@@ -351,7 +327,6 @@
"LabelLess": "較少",
"LabelLibrariesAccessibleToUser": "使用者可存取的媒體庫",
"LabelLibrary": "媒體庫",
- "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "媒體庫項目",
"LabelLibraryName": "媒體庫名稱",
"LabelLimit": "限制",
@@ -372,8 +347,6 @@
"LabelMetadataProvider": "元數據提供者",
"LabelMinute": "分鐘",
"LabelMissing": "丟失",
- "LabelMissingEbook": "Has no ebook",
- "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "允許移動應用重定向 URI",
"LabelMobileRedirectURIsDescription": "這是移動應用程序的有效重定向 URI 白名單. 預設值為
audiobookshelf://oauth
,您可以刪除它或加入其他 URI 以進行第三方應用集成. 使用星號 (
*
) 作為唯一條目允許任何 URI.",
"LabelMore": "更多",
@@ -387,7 +360,6 @@
"LabelNewestEpisodes": "最新劇集",
"LabelNextBackupDate": "下次備份日期",
"LabelNextScheduledRun": "下次任務運行",
- "LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "未選擇任何劇集",
"LabelNotFinished": "未聽完",
"LabelNotStarted": "未開始",
@@ -403,9 +375,6 @@
"LabelNotificationsMaxQueueSizeHelp": "通知事件被限制為每秒觸發 1 個. 如果佇列處於最大大小, 則將忽略事件. 這可以防止通知垃圾郵件.",
"LabelNumberOfBooks": "圖書數量",
"LabelNumberOfEpisodes": "# 集",
- "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (
if configured ). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as
false
. Ensure the identity provider's claim matches the expected structure:",
- "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
- "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": "打開 RSS 源",
"LabelOverwrite": "覆蓋",
"LabelPassword": "密碼",
@@ -420,7 +389,6 @@
"LabelPersonalYearReview": "你的年度回顧 ({0})",
"LabelPhotoPathURL": "圖片路徑或 URL",
"LabelPlayMethod": "播放方法",
- "LabelPlayerChapterNumberMarker": "{0} of {1}",
"LabelPlaylists": "播放列表",
"LabelPodcast": "播客",
"LabelPodcastSearchRegion": "播客搜尋地區",
@@ -435,7 +403,6 @@
"LabelPubDate": "出版日期",
"LabelPublishYear": "發布年份",
"LabelPublisher": "出版商",
- "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "自定義所有者電子郵件",
"LabelRSSFeedCustomOwnerName": "自定義所有者名稱",
"LabelRSSFeedOpen": "打開 RSS 源",
@@ -457,10 +424,8 @@
"LabelSearchTitle": "搜尋標題",
"LabelSearchTitleOrASIN": "搜尋標題或 ASIN",
"LabelSeason": "季",
- "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "選擇所有劇集",
"LabelSelectEpisodesShowing": "選擇正在播放的 {0} 劇集",
- "LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "發送電子書到...",
"LabelSequence": "序列",
"LabelSeries": "系列",
@@ -480,8 +445,6 @@
"LabelSettingsEnableWatcher": "啟用監視程序",
"LabelSettingsEnableWatcherForLibrary": "為庫啟用資料夾監視程序",
"LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器",
- "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
- "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
"LabelSettingsExperimentalFeatures": "實驗功能",
"LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.",
"LabelSettingsFindCovers": "查找封面",
@@ -490,8 +453,6 @@
"LabelSettingsHideSingleBookSeriesHelp": "只有一本書的系列將從系列頁面和主頁書架中隱藏.",
"LabelSettingsHomePageBookshelfView": "首頁使用書架視圖",
"LabelSettingsLibraryBookshelfView": "媒體庫使用書架視圖",
- "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": "解析副標題",
"LabelSettingsParseSubtitlesHelp": "從有聲書資料夾中提取副標題.
副標題必須用 \" - \" 分隔.
例: \"書名 - 這裡是副標題\" 則顯示副標題 \"這裡是副標題\"",
"LabelSettingsPreferMatchedMetadata": "首選匹配的元數據",
@@ -508,10 +469,8 @@
"LabelSettingsStoreMetadataWithItemHelp": "預設情況下元數據檔案存儲在/metadata/items資料夾中, 啟用此設定將存儲元數據在你媒體項目資料夾中",
"LabelSettingsTimeFormat": "時間格式",
"LabelShowAll": "全部顯示",
- "LabelShowSeconds": "Show seconds",
"LabelSize": "檔案大小",
"LabelSleepTimer": "睡眠定時",
- "LabelSlug": "Slug",
"LabelStart": "開始",
"LabelStartTime": "開始時間",
"LabelStarted": "開始於",
@@ -539,7 +498,6 @@
"LabelTagsNotAccessibleToUser": "使用者無法存取標籤",
"LabelTasks": "正在運行的任務",
"LabelTextEditorBulletedList": "項目符號列表",
- "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "編號列表",
"LabelTextEditorUnlink": "取消連結",
"LabelTheme": "主題",
@@ -567,7 +525,6 @@
"LabelTracksSingleTrack": "單軌",
"LabelType": "類型",
"LabelUnabridged": "未刪節",
- "LabelUndo": "Undo",
"LabelUnknown": "未知",
"LabelUpdateCover": "更新封面",
"LabelUpdateCoverHelp": "找到匹配項時允許覆蓋所選書籍存在的封面",
@@ -601,7 +558,6 @@
"MessageBookshelfNoCollections": "你尚未進行任何收藏",
"MessageBookshelfNoRSSFeeds": "沒有打開的 RSS 源",
"MessageBookshelfNoResultsForFilter": "過濾器無結果 \"{0}: {1}\"",
- "MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "你沒有系列",
"MessageChapterEndIsAfter": "章節結束是在有聲書結束之後",
"MessageChapterErrorFirstNotZero": "第一章節必須從 0 開始",
@@ -621,8 +577,6 @@
"MessageConfirmMarkAllEpisodesNotFinished": "你確定要將所有劇集都標記為未完成嗎?",
"MessageConfirmMarkSeriesFinished": "你確定要將此系列中的所有書籍都標記為已聽完嗎?",
"MessageConfirmMarkSeriesNotFinished": "你確定要將此系列中的所有書籍都標記為未聽完嗎?",
- "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": "警告! 快速嵌入不會備份你的音頻檔案. 確保你有音頻檔案的備份.
你是否想繼續嗎?",
"MessageConfirmReScanLibraryItems": "你確定要重新掃描 {0} 個項目嗎?",
"MessageConfirmRemoveAllChapters": "你確定要移除所有章節嗎?",
@@ -644,7 +598,6 @@
"MessageDragFilesIntoTrackOrder": "將檔案拖動到正確的音軌順序",
"MessageEmbedFinished": "嵌入完成!",
"MessageEpisodesQueuedForDownload": "{0} 個劇集排隊等待下載",
- "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": "源 URL 將改為 {0}",
"MessageFetching": "正在獲取...",
"MessageForceReScanDescription": "將像重新掃描一樣再次掃描所有檔案. 音頻檔 ID3 標籤, OPF 檔和文本檔將被掃描為新檔案.",
@@ -656,7 +609,6 @@
"MessageListeningSessionsInTheLastYear": "去年收聽 {0} 個會話",
"MessageLoading": "讀取...",
"MessageLoadingFolders": "讀取資料夾...",
- "MessageLogsDescription": "Logs are stored in
/metadata/logs
as JSON files. Crash logs are stored in
/metadata/logs/crash_logs.txt
.",
"MessageM4BFailed": "M4B 失敗!",
"MessageM4BFinished": "M4B 完成!",
"MessageMapChapterTitles": "將章節標題映射到現有的有聲書章節, 無需調整時間戳",
@@ -692,7 +644,6 @@
"MessageNoSeries": "無系列",
"MessageNoTags": "無標籤",
"MessageNoTasksRunning": "沒有正在運行的任務",
- "MessageNoUpdateNecessary": "無需更新",
"MessageNoUpdatesWereNecessary": "無需更新",
"MessageNoUserPlaylists": "你沒有播放列表",
"MessageNotYetImplemented": "尚未實施",
@@ -739,7 +690,6 @@
"PlaceholderSearchEpisode": "搜尋劇集..",
"ToastAccountUpdateFailed": "帳號更新失敗",
"ToastAccountUpdateSuccess": "帳號已更新",
- "ToastAuthorImageRemoveFailed": "作者圖像刪除失敗",
"ToastAuthorImageRemoveSuccess": "作者圖像已刪除",
"ToastAuthorUpdateFailed": "作者更新失敗",
"ToastAuthorUpdateMerged": "作者已合併",
@@ -756,28 +706,19 @@
"ToastBatchUpdateSuccess": "批量更新成功",
"ToastBookmarkCreateFailed": "創建書籤失敗",
"ToastBookmarkCreateSuccess": "書籤已新增",
- "ToastBookmarkRemoveFailed": "書籤刪除失敗",
"ToastBookmarkRemoveSuccess": "書籤已刪除",
"ToastBookmarkUpdateFailed": "書籤更新失敗",
"ToastBookmarkUpdateSuccess": "書籤已更新",
- "ToastCachePurgeFailed": "Failed to purge cache",
- "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "章節有錯誤",
"ToastChaptersMustHaveTitles": "章節必須有標題",
- "ToastCollectionItemsRemoveFailed": "從收藏夾移除項目失敗",
"ToastCollectionItemsRemoveSuccess": "項目從收藏夾移除",
- "ToastCollectionRemoveFailed": "刪除收藏夾失敗",
"ToastCollectionRemoveSuccess": "收藏夾已刪除",
"ToastCollectionUpdateFailed": "更新收藏夾失敗",
"ToastCollectionUpdateSuccess": "收藏夾已更新",
- "ToastDeleteFileFailed": "Failed to delete file",
- "ToastDeleteFileSuccess": "File deleted",
- "ToastFailedToLoadData": "Failed to load data",
"ToastItemCoverUpdateFailed": "更新項目封面失敗",
"ToastItemCoverUpdateSuccess": "項目封面已更新",
"ToastItemDetailsUpdateFailed": "更新項目詳細信息失敗",
"ToastItemDetailsUpdateSuccess": "項目詳細信息已更新",
- "ToastItemDetailsUpdateUnneeded": "項目詳細信息無需更新",
"ToastItemMarkedAsFinishedFailed": "標記為聽完失敗",
"ToastItemMarkedAsFinishedSuccess": "標記為聽完的項目",
"ToastItemMarkedAsNotFinishedFailed": "標記為未聽完失敗",
@@ -792,7 +733,6 @@
"ToastLibraryUpdateSuccess": "媒體庫 \"{0}\" 已更新",
"ToastPlaylistCreateFailed": "創建播放列表失敗",
"ToastPlaylistCreateSuccess": "已成功創建播放列表",
- "ToastPlaylistRemoveFailed": "刪除播放列表失敗",
"ToastPlaylistRemoveSuccess": "播放列表已刪除",
"ToastPlaylistUpdateFailed": "更新播放列表失敗",
"ToastPlaylistUpdateSuccess": "播放列表已更新",
@@ -806,16 +746,11 @@
"ToastSendEbookToDeviceSuccess": "電子書已經發送到設備 \"{0}\"",
"ToastSeriesUpdateFailed": "更新系列失敗",
"ToastSeriesUpdateSuccess": "系列已更新",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionDeleteFailed": "刪除會話失敗",
"ToastSessionDeleteSuccess": "會話已刪除",
"ToastSocketConnected": "網路已連接",
"ToastSocketDisconnected": "網路已斷開",
"ToastSocketFailedToConnect": "網路連接失敗",
- "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
- "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
- "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastUserDeleteFailed": "刪除使用者失敗",
"ToastUserDeleteSuccess": "使用者已刪除"
}
diff --git a/package-lock.json b/package-lock.json
index 168401a47f..90493a065c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
- "version": "2.12.3",
+ "version": "2.13.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
- "version": "2.12.3",
+ "version": "2.13.4",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
@@ -21,6 +21,7 @@
"p-throttle": "^4.1.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
+ "semver": "^7.6.3",
"sequelize": "^6.35.2",
"socket.io": "^4.5.4",
"sqlite3": "^5.1.6",
@@ -173,6 +174,15 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/@babel/generator": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz",
@@ -213,6 +223,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -586,17 +605,6 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
- "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -611,20 +619,6 @@
"node": ">=6"
}
},
- "node_modules/@mapbox/node-pre-gyp/node_modules/semver": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
- "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@npmcli/fs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
@@ -635,33 +629,6 @@
"semver": "^7.3.5"
}
},
- "node_modules/@npmcli/fs/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "optional": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@npmcli/fs/node_modules/semver": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
- "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
- "optional": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@npmcli/move-file": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz",
@@ -2576,6 +2543,15 @@
"node": ">=8"
}
},
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/istanbul-lib-processinfo": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz",
@@ -2628,18 +2604,6 @@
"node": ">=8"
}
},
- "node_modules/istanbul-lib-report/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/istanbul-lib-report/node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -2655,21 +2619,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/istanbul-lib-report/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/istanbul-lib-report/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2804,36 +2753,11 @@
"npm": ">=6"
}
},
- "node_modules/jsonwebtoken/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
- "node_modules/jsonwebtoken/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/just-extend": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
@@ -2970,6 +2894,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/make-fetch-happen": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz",
@@ -3585,18 +3517,6 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
- "node_modules/node-gyp/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "optional": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/node-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -3627,21 +3547,6 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
- "node_modules/node-gyp/node_modules/semver": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
- "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
- "optional": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/node-preload": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
@@ -4336,11 +4241,14 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"bin": {
"semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/send": {
@@ -4456,36 +4364,11 @@
}
}
},
- "node_modules/sequelize/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/sequelize/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
- "node_modules/sequelize/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/serialize-javascript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
diff --git a/package.json b/package.json
index c064288998..da10e00012 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "2.12.3",
+ "version": "2.13.4",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
@@ -47,6 +47,7 @@
"p-throttle": "^4.1.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
+ "semver": "^7.6.3",
"sequelize": "^6.35.2",
"socket.io": "^4.5.4",
"sqlite3": "^5.1.6",
diff --git a/server/Database.js b/server/Database.js
index d3966e922b..9eedfe1c5f 100644
--- a/server/Database.js
+++ b/server/Database.js
@@ -8,6 +8,8 @@ const Logger = require('./Logger')
const dbMigration = require('./utils/migrations/dbMigration')
const Auth = require('./Auth')
+const MigrationManager = require('./managers/MigrationManager')
+
class Database {
constructor() {
this.sequelize = null
@@ -142,6 +144,11 @@ class Database {
return this.models.mediaItemShare
}
+ /** @type {typeof import('./models/Device')} */
+ get deviceModel() {
+ return this.models.device
+ }
+
/**
* Check if db file exists
* @returns {boolean}
@@ -168,6 +175,15 @@ class Database {
throw new Error('Database connection failed')
}
+ try {
+ const migrationManager = new MigrationManager(this.sequelize, global.ConfigPath)
+ await migrationManager.init(packageJson.version)
+ if (!this.isNew) await migrationManager.runMigrations()
+ } catch (error) {
+ Logger.error(`[Database] Failed to run migrations`, error)
+ throw new Error('Database migration failed')
+ }
+
await this.buildModels(force)
Logger.info(`[Database] Db initialized with models:`, Object.keys(this.sequelize.models).join(', '))
@@ -384,11 +400,6 @@ class Database {
return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook)))
}
- updateLibrary(oldLibrary) {
- if (!this.sequelize) return false
- return this.models.library.updateFromOld(oldLibrary)
- }
-
removeLibrary(libraryId) {
if (!this.sequelize) return false
return this.models.library.removeById(libraryId)
@@ -447,46 +458,6 @@ class Database {
await this.models.feed.removeById(feedId)
}
- updateSeries(oldSeries) {
- if (!this.sequelize) return false
- return this.models.series.updateFromOld(oldSeries)
- }
-
- async createSeries(oldSeries) {
- if (!this.sequelize) return false
- await this.models.series.createFromOld(oldSeries)
- }
-
- async createBulkSeries(oldSeriesObjs) {
- if (!this.sequelize) return false
- await this.models.series.createBulkFromOld(oldSeriesObjs)
- }
-
- async removeSeries(seriesId) {
- if (!this.sequelize) return false
- await this.models.series.removeById(seriesId)
- }
-
- async createAuthor(oldAuthor) {
- if (!this.sequelize) return false
- await this.models.author.createFromOld(oldAuthor)
- }
-
- async createBulkAuthors(oldAuthors) {
- if (!this.sequelize) return false
- await this.models.author.createBulkFromOld(oldAuthors)
- }
-
- updateAuthor(oldAuthor) {
- if (!this.sequelize) return false
- return this.models.author.updateFromOld(oldAuthor)
- }
-
- async removeAuthor(authorId) {
- if (!this.sequelize) return false
- await this.models.author.removeById(authorId)
- }
-
async createBulkBookAuthors(bookAuthors) {
if (!this.sequelize) return false
await this.models.bookAuthor.bulkCreate(bookAuthors)
@@ -523,21 +494,6 @@ class Database {
return this.models.playbackSession.removeById(sessionId)
}
- getDeviceByDeviceId(deviceId) {
- if (!this.sequelize) return false
- return this.models.device.getOldDeviceByDeviceId(deviceId)
- }
-
- updateDevice(oldDevice) {
- if (!this.sequelize) return false
- return this.models.device.updateFromOld(oldDevice)
- }
-
- createDevice(oldDevice) {
- if (!this.sequelize) return false
- return this.models.device.createFromOld(oldDevice)
- }
-
replaceTagInFilterData(oldTag, newTag) {
for (const libraryId in this.libraryFilterData) {
const indexOf = this.libraryFilterData[libraryId].tags.findIndex((n) => n === oldTag)
@@ -689,7 +645,7 @@ class Database {
*/
async getAuthorIdByName(libraryId, authorName) {
if (!this.libraryFilterData[libraryId]) {
- return (await this.authorModel.getOldByNameAndLibrary(authorName, libraryId))?.id || null
+ return (await this.authorModel.getByNameAndLibrary(authorName, libraryId))?.id || null
}
return this.libraryFilterData[libraryId].authors.find((au) => au.name === authorName)?.id || null
}
@@ -703,7 +659,7 @@ class Database {
*/
async getSeriesIdByName(libraryId, seriesName) {
if (!this.libraryFilterData[libraryId]) {
- return (await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId))?.id || null
+ return (await this.seriesModel.getByNameAndLibrary(seriesName, libraryId))?.id || null
}
return this.libraryFilterData[libraryId].series.find((se) => se.name === seriesName)?.id || null
}
diff --git a/server/Logger.js b/server/Logger.js
index 3e38f0fd5c..5d1a7fa59d 100644
--- a/server/Logger.js
+++ b/server/Logger.js
@@ -1,5 +1,6 @@
const date = require('./libs/dateAndTime')
const { LogLevel } = require('./utils/constants')
+const util = require('util')
class Logger {
constructor() {
@@ -69,27 +70,29 @@ class Logger {
/**
*
* @param {number} level
+ * @param {string} levelName
* @param {string[]} args
* @param {string} src
*/
- async handleLog(level, args, src) {
+ async #logToFileAndListeners(level, levelName, args, src) {
+ const expandedArgs = args.map((arg) => (typeof arg !== 'string' ? util.inspect(arg) : arg))
const logObj = {
timestamp: this.timestamp,
source: src,
- message: args.join(' '),
- levelName: this.getLogLevelString(level),
+ message: expandedArgs.join(' '),
+ levelName,
level
}
// Emit log to sockets that are listening to log events
this.socketListeners.forEach((socketListener) => {
- if (socketListener.level <= level) {
+ if (level >= LogLevel.FATAL || level >= socketListener.level) {
socketListener.socket.emit('log', logObj)
}
})
// Save log to file
- if (level >= this.logLevel) {
+ if (level >= LogLevel.FATAL || level >= this.logLevel) {
await this.logManager?.logToFile(logObj)
}
}
@@ -99,50 +102,50 @@ class Logger {
this.debug(`Set Log Level to ${this.levelString}`)
}
+ static ConsoleMethods = {
+ TRACE: 'trace',
+ DEBUG: 'debug',
+ INFO: 'info',
+ WARN: 'warn',
+ ERROR: 'error',
+ FATAL: 'error',
+ NOTE: 'log'
+ }
+
+ #log(levelName, source, ...args) {
+ const level = LogLevel[levelName]
+ if (level < LogLevel.FATAL && level < this.logLevel) return
+ const consoleMethod = Logger.ConsoleMethods[levelName]
+ console[consoleMethod](`[${this.timestamp}] ${levelName}:`, ...args)
+ this.#logToFileAndListeners(level, levelName, args, source)
+ }
+
trace(...args) {
- if (this.logLevel > LogLevel.TRACE) return
- console.trace(`[${this.timestamp}] TRACE:`, ...args)
- this.handleLog(LogLevel.TRACE, args, this.source)
+ this.#log('TRACE', this.source, ...args)
}
debug(...args) {
- if (this.logLevel > LogLevel.DEBUG) return
- console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`)
- this.handleLog(LogLevel.DEBUG, args, this.source)
+ this.#log('DEBUG', this.source, ...args)
}
info(...args) {
- if (this.logLevel > LogLevel.INFO) return
- console.info(`[${this.timestamp}] INFO:`, ...args)
- this.handleLog(LogLevel.INFO, args, this.source)
+ this.#log('INFO', this.source, ...args)
}
warn(...args) {
- if (this.logLevel > LogLevel.WARN) return
- console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`)
- this.handleLog(LogLevel.WARN, args, this.source)
+ this.#log('WARN', this.source, ...args)
}
error(...args) {
- if (this.logLevel > LogLevel.ERROR) return
- console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`)
- this.handleLog(LogLevel.ERROR, args, this.source)
+ this.#log('ERROR', this.source, ...args)
}
- /**
- * Fatal errors are ones that exit the process
- * Fatal logs are saved to crash_logs.txt
- *
- * @param {...any} args
- */
fatal(...args) {
- console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`)
- return this.handleLog(LogLevel.FATAL, args, this.source)
+ this.#log('FATAL', this.source, ...args)
}
note(...args) {
- console.log(`[${this.timestamp}] NOTE:`, ...args)
- this.handleLog(LogLevel.NOTE, args, this.source)
+ this.#log('NOTE', this.source, ...args)
}
}
module.exports = new Logger()
diff --git a/server/Server.js b/server/Server.js
index 0110ab6a70..7e5921c548 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -41,6 +41,7 @@ const LibraryScanner = require('./scanner/LibraryScanner')
//Import the main Passport and Express-Session library
const passport = require('passport')
const expressSession = require('express-session')
+const MemoryStore = require('./libs/memorystore')
class Server {
constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
@@ -136,13 +137,14 @@ class Server {
}
await this.cleanUserData() // Remove invalid user item progress
+ await this.deduplicateSeries() // Deduplicate series by name
await CacheManager.ensureCachePaths()
await ShareManager.init()
await this.backupManager.init()
await this.rssFeedManager.init()
- const libraries = await Database.libraryModel.getAllOldLibraries()
+ const libraries = await Database.libraryModel.getAllWithFolders()
await this.cronManager.init(libraries)
this.apiCacheManager.init()
@@ -232,7 +234,8 @@ class Server {
cookie: {
// also send the cookie if were are not on https (not every use has https)
secure: false
- }
+ },
+ store: new MemoryStore(86400000, 86400000, 1000)
})
)
// init passport.js
@@ -420,6 +423,58 @@ class Server {
}
}
+ /**
+ * Deduplicate series by name and libraryId, keeping the most recent series and
+ * updating references from deleted series
+ */
+ async deduplicateSeries() {
+ // Step 1: Find duplicate series by name and libraryId
+ const duplicates = await Database.seriesModel.findAll({
+ attributes: ['name', 'libraryId', [Sequelize.fn('MAX', Sequelize.col('updatedAt')), 'latestUpdatedAt']],
+ group: ['name', 'libraryId'],
+ having: Sequelize.literal('COUNT(name) > 1')
+ })
+
+ for (const duplicate of duplicates) {
+ // Step 2: Find all series with the same name and libraryId
+ const allSeries = await Database.seriesModel.findAll({
+ where: {
+ name: duplicate.name,
+ libraryId: duplicate.libraryId
+ },
+ order: [['updatedAt', 'DESC']]
+ })
+
+ // The first one in the ordered list is the most recent
+ const mostRecentSeries = allSeries[0]
+
+ try {
+ // Step 3: Update BookSeries to map to the most recent series
+ const seriesIdsToUpdate = allSeries.slice(1).map((s) => s.id)
+ await Database.bookSeriesModel.update(
+ { seriesId: mostRecentSeries.id },
+ {
+ where: {
+ seriesId: { [Sequelize.Op.in]: seriesIdsToUpdate }
+ }
+ }
+ )
+
+ // Step 4: Delete all older series and report how many were deleted
+ const seriesRemoved = await Database.seriesModel.destroy({
+ where: {
+ id: { [Sequelize.Op.in]: seriesIdsToUpdate }
+ }
+ })
+ if (seriesRemoved) {
+ Logger.info(`[Server] Deduplicated series "${duplicate.name}" in library ${duplicate.libraryId} - Removed ${seriesRemoved} series`)
+ }
+ } catch (error) {
+ Logger.error(`[Server] Failed to deduplicate series "${duplicate.name}" in library ${duplicate.libraryId}`, error)
+ }
+ }
+ }
+
/**
* Gracefully stop server
* Stops watcher and socket server
diff --git a/server/Watcher.js b/server/Watcher.js
index cb8b030f4a..83c45234c9 100644
--- a/server/Watcher.js
+++ b/server/Watcher.js
@@ -19,7 +19,7 @@ class FolderWatcher extends EventEmitter {
constructor() {
super()
- /** @type {{id:string, name:string, folders:import('./objects/Folder')[], paths:string[], watcher:Watcher[]}[]} */
+ /** @type {{id:string, name:string, libraryFolders:import('./models/Folder')[], paths:string[], watcher:Watcher[]}[]} */
this.libraryWatchers = []
/** @type {PendingFileUpdate[]} */
this.pendingFileUpdates = []
@@ -45,6 +45,10 @@ class FolderWatcher extends EventEmitter {
return this.pendingFileUpdates.map((f) => f.path)
}
+ /**
+ *
+ * @param {import('./models/Library')} library
+ */
buildLibraryWatcher(library) {
if (this.libraryWatchers.find((w) => w.id === library.id)) {
Logger.warn('[Watcher] Already watching library', library.name)
@@ -52,7 +56,7 @@ class FolderWatcher extends EventEmitter {
}
Logger.info(`[Watcher] Initializing watcher for "${library.name}".`)
- const folderPaths = library.folderPaths
+ const folderPaths = library.libraryFolders.map((f) => f.path)
folderPaths.forEach((fp) => {
Logger.debug(`[Watcher] Init watcher for library folder path "${fp}"`)
})
@@ -90,12 +94,16 @@ class FolderWatcher extends EventEmitter {
this.libraryWatchers.push({
id: library.id,
name: library.name,
- folders: library.folders,
- paths: library.folderPaths,
+ libraryFolders: library.libraryFolders,
+ paths: folderPaths,
watcher
})
}
+ /**
+ *
+ * @param {import('./models/Library')[]} libraries
+ */
initWatcher(libraries) {
libraries.forEach((lib) => {
if (!lib.settings.disableWatcher) {
@@ -104,6 +112,10 @@ class FolderWatcher extends EventEmitter {
})
}
+ /**
+ *
+ * @param {import('./models/Library')} library
+ */
addLibrary(library) {
if (this.disabled || library.settings.disableWatcher) return
this.buildLibraryWatcher(library)
@@ -111,7 +123,7 @@ class FolderWatcher extends EventEmitter {
/**
*
- * @param {import('./objects/Library')} library
+ * @param {import('./models/Library')} library
*/
updateLibrary(library) {
if (this.disabled) return
@@ -129,8 +141,9 @@ class FolderWatcher extends EventEmitter {
libwatcher.name = library.name
// If any folder paths were added or removed then re-init watcher
- const pathsToAdd = library.folderPaths.filter((path) => !libwatcher.paths.includes(path))
- const pathsRemoved = libwatcher.paths.filter((path) => !library.folderPaths.includes(path))
+ const folderPaths = library.libraryFolders.map((f) => f.path)
+ const pathsToAdd = folderPaths.filter((path) => !libwatcher.paths.includes(path))
+ const pathsRemoved = libwatcher.paths.filter((path) => !folderPaths.includes(path))
if (pathsToAdd.length || pathsRemoved.length) {
Logger.info(`[Watcher] Re-Initializing watcher for "${library.name}".`)
@@ -145,6 +158,10 @@ class FolderWatcher extends EventEmitter {
}
}
+ /**
+ *
+ * @param {import('./models/Library')} library
+ */
removeLibrary(library) {
if (this.disabled) return
var libwatcher = this.libraryWatchers.find((lib) => lib.id === library.id)
@@ -255,15 +272,15 @@ class FolderWatcher extends EventEmitter {
}
// Get file folder
- const folder = libwatcher.folders.find((fold) => isSameOrSubPath(fold.fullPath, path))
+ const folder = libwatcher.libraryFolders.find((fold) => isSameOrSubPath(fold.path, path))
if (!folder) {
Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
return
}
- const folderFullPath = filePathToPOSIX(folder.fullPath)
+ const folderPath = filePathToPOSIX(folder.path)
- const relPath = path.replace(folderFullPath, '')
+ const relPath = path.replace(folderPath, '')
if (Path.extname(relPath).toLowerCase() === '.part') {
Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`)
diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js
index 99b977637b..54a6418563 100644
--- a/server/controllers/AuthorController.js
+++ b/server/controllers/AuthorController.js
@@ -21,6 +21,11 @@ const naturalSort = createNewSortInstance({
* @property {import('../models/User')} user
*
* @typedef {Request & RequestUserObject} RequestWithUser
+ *
+ * @typedef RequestEntityObject
+ * @property {import('../models/Author')} author
+ *
+ * @typedef {RequestWithUser & RequestEntityObject} AuthorControllerRequest
*/
class AuthorController {
@@ -29,13 +34,13 @@ class AuthorController {
/**
* GET: /api/authors/:id
*
- * @param {RequestWithUser} req
+ * @param {AuthorControllerRequest} req
* @param {Response} res
*/
async findOne(req, res) {
const include = (req.query.include || '').split(',')
- const authorJson = req.author.toJSON()
+ const authorJson = req.author.toOldJSON()
// Used on author landing page to include library items and items grouped in series
if (include.includes('items')) {
@@ -80,25 +85,33 @@ class AuthorController {
/**
* PATCH: /api/authors/:id
*
- * @param {RequestWithUser} req
+ * @param {AuthorControllerRequest} req
* @param {Response} res
*/
async update(req, res) {
- const payload = req.body
- let hasUpdated = false
-
- // author imagePath must be set through other endpoints as of v2.4.5
- if (payload.imagePath !== undefined) {
- Logger.warn(`[AuthorController] Updating local author imagePath is not supported`)
- delete payload.imagePath
+ const keysToUpdate = ['name', 'description', 'asin']
+ const payload = {}
+ for (const key in req.body) {
+ if (keysToUpdate.includes(key) && (typeof req.body[key] === 'string' || req.body[key] === null)) {
+ payload[key] = req.body[key]
+ }
+ }
+ if (!Object.keys(payload).length) {
+ Logger.error(`[AuthorController] Invalid request payload. No valid keys found`, req.body)
+ return res.status(400).send('Invalid request payload. No valid keys found')
}
+ let hasUpdated = false
+
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
+ if (authorNameUpdate) {
+ payload.lastFirst = Database.authorModel.getLastFirst(payload.name)
+ }
// Check if author name matches another author and merge the authors
let existingAuthor = null
if (authorNameUpdate) {
- const author = await Database.authorModel.findOne({
+ existingAuthor = await Database.authorModel.findOne({
where: {
id: {
[sequelize.Op.not]: req.author.id
@@ -106,7 +119,6 @@ class AuthorController {
name: payload.name
}
})
- existingAuthor = author?.getOldAuthor()
}
if (existingAuthor) {
Logger.info(`[AuthorController] Merging author "${req.author.name}" with "${existingAuthor.name}"`)
@@ -143,86 +155,92 @@ class AuthorController {
}
// Remove old author
- await Database.removeAuthor(req.author.id)
- SocketAuthority.emitter('author_removed', req.author.toJSON())
+ const oldAuthorJSON = req.author.toOldJSON()
+ await req.author.destroy()
+ SocketAuthority.emitter('author_removed', oldAuthorJSON)
// Update filter data
- Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
+ Database.removeAuthorFromFilterData(oldAuthorJSON.libraryId, oldAuthorJSON.id)
// Send updated num books for merged author
const numBooks = await Database.bookAuthorModel.getCountForAuthor(existingAuthor.id)
- SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
+ SocketAuthority.emitter('author_updated', existingAuthor.toOldJSONExpanded(numBooks))
res.json({
- author: existingAuthor.toJSON(),
+ author: existingAuthor.toOldJSON(),
merged: true
})
- } else {
- // Regular author update
- if (req.author.update(payload)) {
- hasUpdated = true
- }
+ return
+ }
- if (hasUpdated) {
- req.author.updatedAt = Date.now()
+ // If lastFirst is not set, get it from the name
+ if (!authorNameUpdate && !req.author.lastFirst) {
+ payload.lastFirst = Database.authorModel.getLastFirst(req.author.name)
+ }
- let numBooksForAuthor = 0
- if (authorNameUpdate) {
- const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id)
+ // Regular author update
+ req.author.set(payload)
+ if (req.author.changed()) {
+ await req.author.save()
+ hasUpdated = true
+ }
- numBooksForAuthor = allItemsWithAuthor.length
- const oldLibraryItems = []
- // Update author name on all books
- for (const libraryItem of allItemsWithAuthor) {
- libraryItem.media.authors = libraryItem.media.authors.map((au) => {
- if (au.id === req.author.id) {
- au.name = req.author.name
- }
- return au
- })
- const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
- oldLibraryItems.push(oldLibraryItem)
+ if (hasUpdated) {
+ let numBooksForAuthor = 0
+ if (authorNameUpdate) {
+ const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id)
- await libraryItem.saveMetadataFile()
- }
+ numBooksForAuthor = allItemsWithAuthor.length
+ const oldLibraryItems = []
+ // Update author name on all books
+ for (const libraryItem of allItemsWithAuthor) {
+ libraryItem.media.authors = libraryItem.media.authors.map((au) => {
+ if (au.id === req.author.id) {
+ au.name = req.author.name
+ }
+ return au
+ })
+ const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
+ oldLibraryItems.push(oldLibraryItem)
- if (oldLibraryItems.length) {
- SocketAuthority.emitter(
- 'items_updated',
- oldLibraryItems.map((li) => li.toJSONExpanded())
- )
- }
- } else {
- numBooksForAuthor = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
+ await libraryItem.saveMetadataFile()
}
- await Database.updateAuthor(req.author)
- SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooksForAuthor))
+ if (oldLibraryItems.length) {
+ SocketAuthority.emitter(
+ 'items_updated',
+ oldLibraryItems.map((li) => li.toJSONExpanded())
+ )
+ }
+ } else {
+ numBooksForAuthor = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
}
- res.json({
- author: req.author.toJSON(),
- updated: hasUpdated
- })
+ SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooksForAuthor))
}
+
+ res.json({
+ author: req.author.toOldJSON(),
+ updated: hasUpdated
+ })
}
/**
* DELETE: /api/authors/:id
* Remove author from all books and delete
*
- * @param {RequestWithUser} req
+ * @param {AuthorControllerRequest} req
* @param {Response} res
*/
async delete(req, res) {
Logger.info(`[AuthorController] Removing author "${req.author.name}"`)
- await Database.authorModel.removeById(req.author.id)
-
if (req.author.imagePath) {
await CacheManager.purgeImageCache(req.author.id) // Purge cache
}
- SocketAuthority.emitter('author_removed', req.author.toJSON())
+ await req.author.destroy()
+
+ SocketAuthority.emitter('author_removed', req.author.toOldJSON())
// Update filter data
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
@@ -234,7 +252,7 @@ class AuthorController {
* POST: /api/authors/:id/image
* Upload author image from web URL
*
- * @param {RequestWithUser} req
+ * @param {AuthorControllerRequest} req
* @param {Response} res
*/
async uploadImage(req, res) {
@@ -265,13 +283,14 @@ class AuthorController {
}
req.author.imagePath = result.path
- req.author.updatedAt = Date.now()
- await Database.authorModel.updateFromOld(req.author)
+ // imagePath may not have changed, but we still want to update the updatedAt field to bust image cache
+ req.author.changed('imagePath', true)
+ await req.author.save()
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
- SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
+ SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks))
res.json({
- author: req.author.toJSON()
+ author: req.author.toOldJSON()
})
}
@@ -279,7 +298,7 @@ class AuthorController {
* DELETE: /api/authors/:id/image
* Remove author image & delete image file
*
- * @param {RequestWithUser} req
+ * @param {AuthorControllerRequest} req
* @param {Response} res
*/
async deleteImage(req, res) {
@@ -291,19 +310,19 @@ class AuthorController {
await CacheManager.purgeImageCache(req.author.id) // Purge cache
await CoverManager.removeFile(req.author.imagePath)
req.author.imagePath = null
- await Database.authorModel.updateFromOld(req.author)
+ await req.author.save()
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
- SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
+ SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks))
res.json({
- author: req.author.toJSON()
+ author: req.author.toOldJSON()
})
}
/**
* POST: /api/authors/:id/match
*
- * @param {RequestWithUser} req
+ * @param {AuthorControllerRequest} req
* @param {Response} res
*/
async match(req, res) {
@@ -342,24 +361,22 @@ class AuthorController {
}
if (hasUpdates) {
- req.author.updatedAt = Date.now()
-
- await Database.updateAuthor(req.author)
+ await req.author.save()
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
- SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
+ SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks))
}
res.json({
updated: hasUpdates,
- author: req.author
+ author: req.author.toOldJSON()
})
}
/**
* GET: /api/authors/:id/image
*
- * @param {RequestWithUser} req
+ * @param {AuthorControllerRequest} req
* @param {Response} res
*/
async getImage(req, res) {
@@ -392,7 +409,7 @@ class AuthorController {
* @param {NextFunction} next
*/
async middleware(req, res, next) {
- const author = await Database.authorModel.getOldById(req.params.id)
+ const author = await Database.authorModel.findByPk(req.params.id)
if (!author) return res.sendStatus(404)
if (req.method == 'DELETE' && !req.user.canDelete) {
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 31e6e2da1c..65243acc44 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -4,7 +4,6 @@ const Path = require('path')
const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
-const Library = require('../objects/Library')
const libraryHelpers = require('../utils/libraryHelpers')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
@@ -28,6 +27,11 @@ const authorFilters = require('../utils/queries/authorFilters')
* @property {import('../models/User')} user
*
* @typedef {Request & RequestUserObject} RequestWithUser
+ *
+ * @typedef RequestEntityObject
+ * @property {import('../models/Library')} library
+ *
+ * @typedef {RequestWithUser & RequestEntityObject} LibraryControllerRequest
*/
class LibraryController {
@@ -148,40 +152,44 @@ class LibraryController {
library.libraryFolders = await library.getLibraryFolders()
- // TODO: Migrate to new library model
- const oldLibrary = Database.libraryModel.getOldLibrary(library)
-
// Only emit to users with access to library
const userFilter = (user) => {
- return user.checkCanAccessLibrary?.(oldLibrary.id)
+ return user.checkCanAccessLibrary?.(library.id)
}
- SocketAuthority.emitter('library_added', oldLibrary.toJSON(), userFilter)
+ SocketAuthority.emitter('library_added', library.toOldJSON(), userFilter)
// Add library watcher
- this.watcher.addLibrary(oldLibrary)
+ this.watcher.addLibrary(library)
- res.json(oldLibrary)
+ res.json(library.toOldJSON())
}
+ /**
+ * GET: /api/libraries
+ * Get all libraries
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
async findAll(req, res) {
- const libraries = await Database.libraryModel.getAllOldLibraries()
+ const libraries = await Database.libraryModel.getAllWithFolders()
const librariesAccessible = req.user.permissions?.librariesAccessible || []
if (librariesAccessible.length) {
return res.json({
- libraries: libraries.filter((lib) => librariesAccessible.includes(lib.id)).map((lib) => lib.toJSON())
+ libraries: libraries.filter((lib) => librariesAccessible.includes(lib.id)).map((lib) => lib.toOldJSON())
})
}
res.json({
- libraries: libraries.map((lib) => lib.toJSON())
+ libraries: libraries.map((lib) => lib.toOldJSON())
})
}
/**
* GET: /api/libraries/:id
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async findOne(req, res) {
@@ -195,16 +203,17 @@ class LibraryController {
issues: filterdata.numIssues,
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
customMetadataProviders,
- library: req.library
+ library: req.library.toOldJSON()
})
}
- res.json(req.library)
+ res.json(req.library.toOldJSON())
}
/**
* GET: /api/libraries/:id/episode-downloads
* Get podcast episodes in download queue
- * @param {RequestWithUser} req
+ *
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getEpisodeDownloadQueue(req, res) {
@@ -215,12 +224,28 @@ class LibraryController {
/**
* PATCH: /api/libraries/:id
*
- * @param {RequestWithUser} req
+ * @this {import('../routers/ApiRouter')}
+ *
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async update(req, res) {
- /** @type {import('../objects/Library')} */
- const library = req.library
+ // Validation
+ const updatePayload = {}
+ const keysToCheck = ['name', 'provider', 'mediaType', 'icon']
+ for (const key of keysToCheck) {
+ if (!req.body[key]) continue
+ if (typeof req.body[key] !== '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)) {
+ return res.status(400).send('Invalid request. displayOrder must be a number')
+ }
+ updatePayload.displayOrder = req.body.displayOrder
+ }
// Validate that the custom provider exists if given any
if (req.body.provider?.startsWith('custom-')) {
@@ -230,21 +255,72 @@ class LibraryController {
}
}
+ // Validate settings
+ const updatedSettings = {
+ ...(req.library.settings || Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType))
+ }
+ 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 (key === 'metadataPrecedence') {
+ if (!Array.isArray(req.body.settings[key])) {
+ return res.status(400).send('Invalid request. Settings "metadataPrecedence" must be an array')
+ }
+ if (JSON.stringify(req.body.settings[key]) !== JSON.stringify(updatedSettings[key])) {
+ hasUpdates = true
+ updatedSettings[key] = [...req.body.settings[key]]
+ Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
+ }
+ } else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') {
+ if (req.body.settings[key] !== null && typeof req.body.settings[key] !== 'string') {
+ return res.status(400).send(`Invalid request. Settings "${key}" must be a string`)
+ }
+ if (req.body.settings[key] !== updatedSettings[key]) {
+ if (key === 'autoScanCronExpression') hasUpdatedScanCron = true
+
+ hasUpdates = true
+ updatedSettings[key] = 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]) {
+ return res.status(400).send(`Invalid request. Setting "${key}" must be of type ${typeof updatedSettings[key]}`)
+ }
+ if (req.body.settings[key] !== updatedSettings[key]) {
+ if (key === 'disableWatcher') hasUpdatedDisableWatcher = true
+
+ hasUpdates = true
+ updatedSettings[key] = req.body.settings[key]
+ Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
+ }
+ }
+ }
+ if (hasUpdates) {
+ updatePayload.settings = updatedSettings
+ req.library.changed('settings', true)
+ }
+ }
+
+ let hasFolderUpdates = false
// Validate new folder paths exist or can be created & resolve rel paths
// returns 400 if a new folder fails to access
- if (req.body.folders) {
+ if (Array.isArray(req.body.folders)) {
const newFolderPaths = []
req.body.folders = req.body.folders.map((f) => {
if (!f.id) {
- f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath))
- newFolderPaths.push(f.fullPath)
+ const path = f.fullPath || f.path
+ f.path = fileUtils.filePathToPOSIX(Path.resolve(path))
+ newFolderPaths.push(f.path)
}
return f
})
for (const path of newFolderPaths) {
const pathExists = await fs.pathExists(path)
if (!pathExists) {
- // Ensure dir will recursively create directories which might be preferred over mkdir
const success = await fs
.ensureDir(path)
.then(() => true)
@@ -256,10 +332,17 @@ class LibraryController {
return res.status(400).send(`Invalid folder directory "${path}"`)
}
}
+ // Create folder
+ const libraryFolder = await Database.libraryFolderModel.create({
+ path,
+ libraryId: req.library.id
+ })
+ Logger.info(`[LibraryController] Created folder "${libraryFolder.path}" for library "${req.library.name}"`)
+ hasFolderUpdates = true
}
// Handle removing folders
- for (const folder of library.folders) {
+ for (const folder of req.library.libraryFolders) {
if (!req.body.folders.some((f) => f.id === folder.id)) {
// Remove library items in folder
const libraryItemsInFolder = await Database.libraryItemModel.findAll({
@@ -278,66 +361,82 @@ class LibraryController {
}
]
})
- Logger.info(`[LibraryController] Removed folder "${folder.fullPath}" from library "${library.name}" with ${libraryItemsInFolder.length} library items`)
+ Logger.info(`[LibraryController] Removed folder "${folder.path}" from library "${req.library.name}" with ${libraryItemsInFolder.length} library items`)
for (const libraryItem of libraryItemsInFolder) {
let mediaItemIds = []
- if (library.isPodcast) {
+ if (req.library.isPodcast) {
mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id)
} else {
mediaItemIds.push(libraryItem.mediaId)
}
- Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.fullPath}"`)
+ Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`)
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
}
+
+ // Remove folder
+ await folder.destroy()
+ hasFolderUpdates = true
}
}
}
- const hasUpdates = library.update(req.body)
- // TODO: Should check if this is an update to folder paths or name only
- if (hasUpdates) {
- // Update watcher
- this.watcher.updateLibrary(library)
+ if (Object.keys(updatePayload).length) {
+ req.library.set(updatePayload)
+ if (req.library.changed()) {
+ Logger.debug(`[LibraryController] Updated library "${req.library.name}" with changed keys ${req.library.changed()}`)
+ hasUpdates = true
+ await req.library.save()
+ }
+ }
+ if (hasUpdatedScanCron) {
+ Logger.debug(`[LibraryController] Updated library "${req.library.name}" auto scan cron`)
// Update auto scan cron
- this.cronManager.updateLibraryScanCron(library)
+ this.cronManager.updateLibraryScanCron(req.library)
+ }
+
+ if (hasFolderUpdates || hasUpdatedDisableWatcher) {
+ req.library.libraryFolders = await req.library.getLibraryFolders()
- await Database.updateLibrary(library)
+ // Update watcher
+ this.watcher.updateLibrary(req.library)
+ hasUpdates = true
+ }
+
+ if (hasUpdates) {
// Only emit to users with access to library
const userFilter = (user) => {
- return user.checkCanAccessLibrary?.(library.id)
+ return user.checkCanAccessLibrary?.(req.library.id)
}
- SocketAuthority.emitter('library_updated', library.toJSON(), userFilter)
+ SocketAuthority.emitter('library_updated', req.library.toOldJSON(), userFilter)
- await Database.resetLibraryIssuesFilterData(library.id)
+ await Database.resetLibraryIssuesFilterData(req.library.id)
}
- return res.json(library.toJSON())
+ return res.json(req.library.toOldJSON())
}
/**
* DELETE: /api/libraries/:id
* Delete a library
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async delete(req, res) {
- const library = req.library
-
// Remove library watcher
- this.watcher.removeLibrary(library)
+ this.watcher.removeLibrary(req.library)
// Remove collections for library
- const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(library.id)
+ const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(req.library.id)
if (numCollectionsRemoved) {
- Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`)
+ Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${req.library.name}"`)
}
// Remove items in this library
const libraryItemsInLibrary = await Database.libraryItemModel.findAll({
where: {
- libraryId: library.id
+ libraryId: req.library.id
},
attributes: ['id', 'mediaId', 'mediaType'],
include: [
@@ -351,20 +450,20 @@ class LibraryController {
}
]
})
- Logger.info(`[LibraryController] Removing ${libraryItemsInLibrary.length} library items in library "${library.name}"`)
+ Logger.info(`[LibraryController] Removing ${libraryItemsInLibrary.length} library items in library "${req.library.name}"`)
for (const libraryItem of libraryItemsInLibrary) {
let mediaItemIds = []
- if (library.isPodcast) {
+ if (req.library.isPodcast) {
mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id)
} else {
mediaItemIds.push(libraryItem.mediaId)
}
- Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${library.name}"`)
+ Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`)
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
}
- const libraryJson = library.toJSON()
- await Database.removeLibrary(library.id)
+ const libraryJson = req.library.toOldJSON()
+ await Database.removeLibrary(req.library.id)
// Re-order libraries
await Database.libraryModel.resetDisplayOrder()
@@ -372,8 +471,8 @@ class LibraryController {
SocketAuthority.emitter('library_removed', libraryJson)
// Remove library filter data
- if (Database.libraryFilterData[library.id]) {
- delete Database.libraryFilterData[library.id]
+ if (Database.libraryFilterData[req.library.id]) {
+ delete Database.libraryFilterData[req.library.id]
}
return res.json(libraryJson)
@@ -382,7 +481,7 @@ class LibraryController {
/**
* GET /api/libraries/:id/items
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getLibraryItems(req, res) {
@@ -422,9 +521,10 @@ class LibraryController {
}
/**
- * DELETE: /libraries/:id/issues
+ * DELETE: /api/libraries/:id/issues
* Remove all library items missing or invalid
- * @param {RequestWithUser} req
+ *
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async removeLibraryItemsWithIssues(req, res) {
@@ -482,7 +582,7 @@ class LibraryController {
* GET: /api/libraries/:id/series
* Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getAllSeriesForLibrary(req, res) {
@@ -518,7 +618,7 @@ class LibraryController {
* rssfeed: adds `rssFeed` to series object if a feed is open
* progress: adds `progress` to series object with { libraryItemIds:Array
, libraryItemIdsFinished:Array, isFinished:boolean }
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res - Series
*/
async getSeriesForLibrary(req, res) {
@@ -529,11 +629,10 @@ class LibraryController {
const series = await Database.seriesModel.findByPk(req.params.seriesId)
if (!series) return res.sendStatus(404)
- const oldSeries = series.getOldSeries()
- const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.user)
+ const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
- const seriesJson = oldSeries.toJSON()
+ const seriesJson = series.toOldJSON()
if (include.includes('progress')) {
const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.media.id)?.isFinished)
seriesJson.progress = {
@@ -555,7 +654,7 @@ class LibraryController {
* GET: /api/libraries/:id/collections
* Get all collections for library
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getCollectionsForLibrary(req, res) {
@@ -594,7 +693,7 @@ class LibraryController {
* GET: /api/libraries/:id/playlists
* Get playlists for user in library
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getUserPlaylistsForLibrary(req, res) {
@@ -619,7 +718,7 @@ class LibraryController {
/**
* GET: /api/libraries/:id/filterdata
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getLibraryFilterData(req, res) {
@@ -631,7 +730,7 @@ class LibraryController {
* GET: /api/libraries/:id/personalized
* Home page shelves
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getUserPersonalizedShelves(req, res) {
@@ -648,7 +747,13 @@ class LibraryController {
* POST: /api/libraries/order
* Change the display order of libraries
*
- * @param {RequestWithUser} req
+ * @typedef LibraryReorderObj
+ * @property {string} id
+ * @property {number} newOrder
+ *
+ * @typedef {Request<{}, {}, LibraryReorderObj[], {}> & RequestUserObject} LibraryReorderRequest
+ *
+ * @param {LibraryReorderRequest} req
* @param {Response} res
*/
async reorder(req, res) {
@@ -656,20 +761,25 @@ class LibraryController {
Logger.error(`[LibraryController] Non-admin user "${req.user}" attempted to reorder libraries`)
return res.sendStatus(403)
}
- const libraries = await Database.libraryModel.getAllOldLibraries()
+
+ const libraries = await Database.libraryModel.getAllWithFolders()
const orderdata = req.body
+ if (!Array.isArray(orderdata) || orderdata.some((o) => typeof o?.id !== 'string' || typeof o?.newOrder !== 'number')) {
+ return res.status(400).send('Invalid request. Request body must be an array of objects')
+ }
+
let hasUpdates = false
for (let i = 0; i < orderdata.length; i++) {
const library = libraries.find((lib) => lib.id === orderdata[i].id)
if (!library) {
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
- return res.sendStatus(500)
- }
- if (library.update({ displayOrder: orderdata[i].newOrder })) {
- hasUpdates = true
- await Database.updateLibrary(library)
+ return res.status(400).send(`Library not found with id ${orderdata[i].id}`)
}
+ if (library.displayOrder === orderdata[i].newOrder) continue
+ library.displayOrder = orderdata[i].newOrder
+ await library.save()
+ hasUpdates = true
}
if (hasUpdates) {
@@ -680,7 +790,7 @@ class LibraryController {
}
res.json({
- libraries: libraries.map((lib) => lib.toJSON())
+ libraries: libraries.map((lib) => lib.toOldJSON())
})
}
@@ -689,13 +799,14 @@ class LibraryController {
* Search library items with query
*
* ?q=search
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async search(req, res) {
if (!req.query.q || typeof req.query.q !== 'string') {
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())
@@ -707,7 +818,7 @@ class LibraryController {
* GET: /api/libraries/:id/stats
* Get stats for library
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async stats(req, res) {
@@ -715,7 +826,7 @@ class LibraryController {
largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10)
}
- if (req.library.isBook) {
+ if (req.library.mediaType === 'book') {
const authors = await authorFilters.getAuthorsWithCount(req.library.id, 10)
const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
@@ -750,7 +861,7 @@ class LibraryController {
* GET: /api/libraries/:id/authors
* Get authors for library
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getAuthors(req, res) {
@@ -775,8 +886,7 @@ class LibraryController {
const oldAuthors = []
for (const author of authors) {
- const oldAuthor = author.getOldAuthor().toJSON()
- oldAuthor.numBooks = author.books.length
+ const oldAuthor = author.toOldJSONExpanded(author.books.length)
oldAuthor.lastFirst = author.lastFirst
oldAuthors.push(oldAuthor)
}
@@ -789,7 +899,7 @@ class LibraryController {
/**
* GET: /api/libraries/:id/narrators
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getNarrators(req, res) {
@@ -836,7 +946,7 @@ class LibraryController {
* :narratorId is base64 encoded name
* req.body { name }
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async updateNarrator(req, res) {
@@ -887,7 +997,7 @@ class LibraryController {
* Remove narrator
* :narratorId is base64 encoded name
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async removeNarrator(req, res) {
@@ -930,7 +1040,7 @@ class LibraryController {
* GET: /api/libraries/:id/matchall
* Quick match all library items. Book libraries only.
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async matchAll(req, res) {
@@ -947,7 +1057,7 @@ class LibraryController {
* Optional query:
* ?force=1
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async scan(req, res) {
@@ -968,11 +1078,11 @@ class LibraryController {
* GET: /api/libraries/:id/recent-episodes
* Used for latest page
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getRecentEpisodes(req, res) {
- if (!req.library.isPodcast) {
+ if (req.library.mediaType !== 'podcast') {
return res.sendStatus(404)
}
@@ -991,7 +1101,7 @@ class LibraryController {
* GET: /api/libraries/:id/opml
* Get OPML file for a podcast library
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async getOPMLFile(req, res) {
@@ -1015,9 +1125,10 @@ class LibraryController {
}
/**
+ * POST: /api/libraries/:id/remove-metadata
* Remove all metadata.json or metadata.abs files in library item folders
*
- * @param {RequestWithUser} req
+ * @param {LibraryControllerRequest} req
* @param {Response} res
*/
async removeAllMetadataFiles(req, res) {
@@ -1076,7 +1187,7 @@ class LibraryController {
return res.sendStatus(403)
}
- const library = await Database.libraryModel.getOldById(req.params.id)
+ const library = await Database.libraryModel.findByIdWithFolders(req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index 9a87f7a798..c77e1d3a59 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -151,6 +151,8 @@ class LibraryItemController {
* PATCH: /items/:id/media
* Update media for a library item. Will create new authors & series when necessary
*
+ * @this {import('../routers/ApiRouter')}
+ *
* @param {RequestWithUser} req
* @param {Response} res
*/
@@ -185,6 +187,12 @@ class LibraryItemController {
seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
}
+ let authorsRemoved = []
+ if (libraryItem.isBook && mediaPayload.metadata?.authors) {
+ const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
+ authorsRemoved = libraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
+ }
+
const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url
if (hasUpdates) {
libraryItem.updatedAt = Date.now()
@@ -205,6 +213,15 @@ class LibraryItemController {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
+
+ if (authorsRemoved.length) {
+ // Check remove empty authors
+ Logger.debug(`[LibraryItemController] Authors were removed from book. Check if authors are now empty.`)
+ await this.checkRemoveAuthorsWithNoBooks(
+ libraryItem.libraryId,
+ authorsRemoved.map((au) => au.id)
+ )
+ }
}
res.json({
updated: hasUpdates,
@@ -367,7 +384,7 @@ class LibraryItemController {
* @param {Response} res
*/
startPlaybackSession(req, res) {
- if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') {
+ if (!req.libraryItem.media.numTracks) {
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
return res.sendStatus(404)
}
@@ -823,7 +840,7 @@ class LibraryItemController {
// We actually need to check for Webkit on Apple mobile devices because this issue impacts all browsers on iOS/iPadOS/etc, not just Safari.
const isAppleMobileBrowser = ua.device.vendor === 'Apple' && ua.device.type === 'mobile' && ua.engine.name === 'WebKit'
if (isAppleMobileBrowser && audioMimeType === AudioMimeType.M4B) {
- audioMimeType = 'audio/m4b'
+ audioMimeType = 'audio/m4b'
}
res.setHeader('Content-Type', audioMimeType)
}
diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js
index ac6afff727..f3dd0c6d40 100644
--- a/server/controllers/MiscController.js
+++ b/server/controllers/MiscController.js
@@ -44,11 +44,11 @@ class MiscController {
const files = Object.values(req.files)
const { title, author, series, folder: folderId, library: libraryId } = req.body
- const library = await Database.libraryModel.getOldById(libraryId)
+ const library = await Database.libraryModel.findByIdWithFolders(libraryId)
if (!library) {
return res.status(404).send(`Library not found with id ${libraryId}`)
}
- const folder = library.folders.find((fold) => fold.id === folderId)
+ const folder = library.libraryFolders.find((fold) => fold.id === folderId)
if (!folder) {
return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
}
@@ -63,7 +63,7 @@ class MiscController {
// before sanitizing all the directory parts to remove illegal chars and finally prepending
// the base folder path
const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map((part) => sanitizeFilename(part))
- const outputDirectory = Path.join(...[folder.fullPath, ...cleanedOutputDirectoryParts])
+ const outputDirectory = Path.join(...[folder.path, ...cleanedOutputDirectoryParts])
await fs.ensureDir(outputDirectory)
diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js
index 30688c7687..3610c2ea7f 100644
--- a/server/controllers/PodcastController.js
+++ b/server/controllers/PodcastController.js
@@ -38,13 +38,13 @@ class PodcastController {
}
const payload = req.body
- const library = await Database.libraryModel.getOldById(payload.libraryId)
+ const library = await Database.libraryModel.findByIdWithFolders(payload.libraryId)
if (!library) {
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
return res.status(404).send('Library not found')
}
- const folder = library.folders.find((fold) => fold.id === payload.folderId)
+ const folder = library.libraryFolders.find((fold) => fold.id === payload.folderId)
if (!folder) {
Logger.error(`[PodcastController] Create: Folder not found "${payload.folderId}"`)
return res.status(404).send('Folder not found')
diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js
index 5c7cc2a044..2e07c10e24 100644
--- a/server/controllers/RSSFeedController.js
+++ b/server/controllers/RSSFeedController.js
@@ -125,7 +125,7 @@ class RSSFeedController {
async openRSSFeedForSeries(req, res) {
const options = req.body || {}
- const series = await Database.seriesModel.getOldById(req.params.seriesId)
+ const series = await Database.seriesModel.findByPk(req.params.seriesId)
if (!series) return res.sendStatus(404)
// Check request body options exist
@@ -140,7 +140,7 @@ class RSSFeedController {
return res.status(400).send('Slug already in use')
}
- const seriesJson = series.toJSON()
+ const seriesJson = series.toOldJSON()
// Get books in series that have audio tracks
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js
index cfe4e6d3ea..a19ff87667 100644
--- a/server/controllers/SearchController.js
+++ b/server/controllers/SearchController.js
@@ -3,7 +3,6 @@ const Logger = require('../Logger')
const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
const AuthorFinder = require('../finders/AuthorFinder')
-const MusicFinder = require('../finders/MusicFinder')
const Database = require('../Database')
const { isValidASIN } = require('../utils')
diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js
index 54b0453855..5d761ba9fd 100644
--- a/server/controllers/SeriesController.js
+++ b/server/controllers/SeriesController.js
@@ -9,6 +9,11 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
* @property {import('../models/User')} user
*
* @typedef {Request & RequestUserObject} RequestWithUser
+ *
+ * @typedef RequestEntityObject
+ * @property {import('../models/Series')} series
+ *
+ * @typedef {RequestWithUser & RequestEntityObject} SeriesControllerRequest
*/
class SeriesController {
@@ -21,7 +26,7 @@ class SeriesController {
* TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead
* Series are not library specific so we need to know what the library id is
*
- * @param {RequestWithUser} req
+ * @param {SeriesControllerRequest} req
* @param {Response} res
*/
async findOne(req, res) {
@@ -30,7 +35,7 @@ class SeriesController {
.map((v) => v.trim())
.filter((v) => !!v)
- const seriesJson = req.series.toJSON()
+ const seriesJson = req.series.toOldJSON()
// Add progress map with isFinished flag
if (include.includes('progress')) {
@@ -54,17 +59,28 @@ class SeriesController {
}
/**
+ * TODO: Currently unused in the client, should check for duplicate name
*
- * @param {RequestWithUser} req
+ * @param {SeriesControllerRequest} req
* @param {Response} res
*/
async update(req, res) {
- const hasUpdated = req.series.update(req.body)
- if (hasUpdated) {
- await Database.updateSeries(req.series)
- SocketAuthority.emitter('series_updated', req.series.toJSON())
+ const keysToUpdate = ['name', 'description']
+ const payload = {}
+ for (const key of keysToUpdate) {
+ if (req.body[key] !== undefined && typeof req.body[key] === 'string') {
+ payload[key] = req.body[key]
+ }
+ }
+ if (!Object.keys(payload).length) {
+ return res.status(400).send('No valid fields to update')
+ }
+ req.series.set(payload)
+ if (req.series.changed()) {
+ await req.series.save()
+ SocketAuthority.emitter('series_updated', req.series.toOldJSON())
}
- res.json(req.series.toJSON())
+ res.json(req.series.toOldJSON())
}
/**
@@ -74,7 +90,7 @@ class SeriesController {
* @param {NextFunction} next
*/
async middleware(req, res, next) {
- const series = await Database.seriesModel.getOldById(req.params.id)
+ const series = await Database.seriesModel.findByPk(req.params.id)
if (!series) return res.sendStatus(404)
/**
diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js
index 32cd5a6c97..8aa9f83267 100644
--- a/server/controllers/ToolsController.js
+++ b/server/controllers/ToolsController.js
@@ -29,12 +29,17 @@ class ToolsController {
if (req.libraryItem.mediaType !== 'book') {
Logger.error(`[MiscController] encodeM4b: Invalid library item ${req.params.id}: not a book`)
- return res.status(500).send('Invalid library item: not a book')
+ return res.status(400).send('Invalid library item: not a book')
}
if (req.libraryItem.media.tracks.length <= 0) {
Logger.error(`[MiscController] encodeM4b: Invalid audiobook ${req.params.id}: no audio tracks`)
- return res.status(500).send('Invalid audiobook: no audio tracks')
+ return res.status(400).send('Invalid audiobook: no audio tracks')
+ }
+
+ if (this.abMergeManager.getPendingTaskByLibraryItemId(req.libraryItem.id)) {
+ Logger.error(`[MiscController] encodeM4b: Audiobook ${req.params.id} is already processing`)
+ return res.status(400).send('Audiobook is already processing')
}
const options = req.query || {}
@@ -73,12 +78,12 @@ class ToolsController {
async embedAudioFileMetadata(req, res) {
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
Logger.error(`[ToolsController] Invalid library item`)
- return res.sendStatus(500)
+ return res.sendStatus(400)
}
if (this.audioMetadataManager.getIsLibraryItemQueuedOrProcessing(req.libraryItem.id)) {
Logger.error(`[ToolsController] Library item (${req.libraryItem.id}) is already in queue or processing`)
- return res.status(500).send('Library item is already in queue or processing')
+ return res.status(400).send('Library item is already in queue or processing')
}
const options = {
@@ -120,12 +125,12 @@ class ToolsController {
if (libraryItem.isMissing || !libraryItem.hasAudioFiles || !libraryItem.isBook) {
Logger.error(`[ToolsController] Batch embed invalid library item (${libraryItemId})`)
- return res.sendStatus(500)
+ return res.sendStatus(400)
}
if (this.audioMetadataManager.getIsLibraryItemQueuedOrProcessing(libraryItemId)) {
Logger.error(`[ToolsController] Batch embed library item (${libraryItemId}) is already in queue or processing`)
- return res.status(500).send('Library item is already in queue or processing')
+ return res.status(400).send('Library item is already in queue or processing')
}
libraryItems.push(libraryItem)
diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js
index d1b93695c6..f895c0d014 100644
--- a/server/controllers/UserController.js
+++ b/server/controllers/UserController.js
@@ -205,9 +205,12 @@ class UserController {
async update(req, res) {
const user = req.reqUser
- if (user.type === 'root' && !req.user.isRoot) {
+ if (user.isRoot && !req.user.isRoot) {
Logger.error(`[UserController] Admin user "${req.user.username}" attempted to update root user`)
return res.sendStatus(403)
+ } else if (user.isRoot) {
+ // Root user cannot update type
+ delete req.body.type
}
const updatePayload = req.body
@@ -270,8 +273,10 @@ class UserController {
const permissions = {
...user.permissions
}
+ const defaultPermissions = Database.userModel.getDefaultPermissionsForUserType(updatePayload.type || user.type || 'user')
for (const key in updatePayload.permissions) {
- if (permissions[key] !== undefined) {
+ // Check that the key is a valid permission key or is included in the default permissions
+ if (permissions[key] !== undefined || defaultPermissions[key] !== undefined) {
if (typeof updatePayload.permissions[key] !== 'boolean') {
Logger.warn(`[UserController] update: Invalid permission value for key ${key}. Should be boolean`)
} else if (permissions[key] !== updatePayload.permissions[key]) {
diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js
index 8aef4d111d..47d1118c0f 100644
--- a/server/finders/BookFinder.js
+++ b/server/finders/BookFinder.js
@@ -202,10 +202,14 @@ class BookFinder {
* @returns {Promise}
*/
async getCustomProviderResults(title, author, isbn, providerSlug) {
- const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book', this.#providerResponseTimeout)
- if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
-
- return books
+ try {
+ const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book', this.#providerResponseTimeout)
+ if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
+ return books
+ } catch (error) {
+ Logger.error(`Error searching Custom provider '${providerSlug}':`, error)
+ return []
+ }
}
static TitleCandidates = class {
diff --git a/server/finders/MusicFinder.js b/server/finders/MusicFinder.js
deleted file mode 100644
index 3569576f66..0000000000
--- a/server/finders/MusicFinder.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const MusicBrainz = require('../providers/MusicBrainz')
-
-class MusicFinder {
- constructor() {
- this.musicBrainz = new MusicBrainz()
- }
-
- searchTrack(options) {
- return this.musicBrainz.searchTrack(options)
- }
-}
-module.exports = new MusicFinder()
\ No newline at end of file
diff --git a/server/libs/memorystore/index.js b/server/libs/memorystore/index.js
index b17e881355..d47853c023 100644
--- a/server/libs/memorystore/index.js
+++ b/server/libs/memorystore/index.js
@@ -8,89 +8,33 @@
// SOURCE: https://github.com/roccomuso/memorystore
//
-var debug = require('debug')('memorystore')
+const debug = require('debug')('memorystore')
const { LRUCache } = require('lru-cache')
-var util = require('util')
+const { Store } = require('express-session')
/**
- * One day in milliseconds.
- */
-
-var oneDay = 86400000
-
-function getTTL(options, sess, sid) {
- if (typeof options.ttl === 'number') return options.ttl
- if (typeof options.ttl === 'function') return options.ttl(options, sess, sid)
- if (options.ttl) throw new TypeError('`options.ttl` must be a number or function.')
-
- var maxAge = sess?.cookie?.maxAge || null
- return typeof maxAge === 'number' ? Math.floor(maxAge) : oneDay
-}
-
-function prune(store) {
- debug('Pruning expired entries')
- store.forEach(function (value, key) {
- store.get(key)
- })
-}
-
-var defer =
- typeof setImmediate === 'function'
- ? setImmediate
- : function (fn) {
- process.nextTick(fn.bind.apply(fn, arguments))
- }
-
-/**
- * Return the `MemoryStore` extending `express`'s session Store.
+ * An alternative memory store implementation for express session that prunes stale entries.
*
- * @param {object} express session
- * @return {Function}
- * @api public
+ * @param {number} checkPeriod stale entry pruning frequency in ms
+ * @param {number} ttl entry time to live in ms
+ * @param {number} max LRU cache max entries
*/
-
-module.exports = function (session) {
- /**
- * Express's session Store.
- */
-
- var Store = session.Store
-
- /**
- * Initialize MemoryStore with the given `options`.
- *
- * @param {Object} options
- * @api public
- */
-
- function MemoryStore(options) {
- if (!(this instanceof MemoryStore)) {
- throw new TypeError('Cannot call MemoryStore constructor as a function')
+module.exports = class MemoryStore extends Store {
+ constructor(checkPeriod, ttl, max) {
+ if (typeof checkPeriod !== 'number' || typeof ttl !== 'number' || typeof max !== 'number') {
+ throw Error('All arguments must be provided')
}
-
- options = options || {}
- Store.call(this, options)
-
- this.options = {}
- this.options.checkPeriod = options.checkPeriod
- this.options.max = options.max
- this.options.ttl = options.ttl
- this.options.dispose = options.dispose
- this.options.stale = options.stale
-
- this.serializer = options.serializer || JSON
- this.store = new LRUCache(this.options)
- debug('Init MemoryStore')
-
- this.startInterval()
+ super()
+ this.store = new LRUCache({ ttl, max })
+ let prune = () => {
+ let sizeBefore = this.store.size
+ this.store.purgeStale()
+ debug('PRUNE size changed by %i entries', sizeBefore - this.store.size)
+ }
+ setInterval(prune, Math.floor(checkPeriod)).unref()
+ debug('INIT MemoryStore constructed with checkPeriod "%i", ttl "%i", max "%i"', checkPeriod, ttl, max)
}
- /**
- * Inherit from `Store`.
- */
-
- util.inherits(MemoryStore, Store)
-
/**
* Attempt to fetch session by the given `sid`.
*
@@ -98,25 +42,19 @@ module.exports = function (session) {
* @param {Function} fn
* @api public
*/
-
- MemoryStore.prototype.get = function (sid, fn) {
- var store = this.store
-
- debug('GET "%s"', sid)
-
- var data = store.get(sid)
- if (!data) return fn()
-
- debug('GOT %s', data)
- var err = null
- var result
- try {
- result = this.serializer.parse(data)
- } catch (er) {
- err = er
+ get(sid, fn) {
+ let err = null
+ let res = null
+ const data = this.store.get(sid)
+ debug('GET %s: %s', sid, data)
+ if (data) {
+ try {
+ res = JSON.parse(data)
+ } catch (e) {
+ err = e
+ }
}
-
- fn && defer(fn, err, result)
+ fn && setImmediate(fn, err, res)
}
/**
@@ -127,48 +65,39 @@ module.exports = function (session) {
* @param {Function} fn
* @api public
*/
-
- MemoryStore.prototype.set = function (sid, sess, fn) {
- var store = this.store
-
- var ttl = getTTL(this.options, sess, sid)
+ set(sid, sess, fn) {
+ let err = null
try {
- var jsess = this.serializer.stringify(sess)
- } catch (err) {
- fn && defer(fn, err)
+ let jsess = JSON.stringify(sess)
+ debug('SET %s: %s', sid, jsess)
+ this.store.set(sid, jsess)
+ } catch (e) {
+ err = e
}
-
- store.set(sid, jsess, {
- ttl
- })
- debug('SET "%s" %s ttl:%s', sid, jsess, ttl)
- fn && defer(fn, null)
+ fn && setImmediate(fn, err)
}
/**
* Destroy the session associated with the given `sid`.
*
* @param {String} sid
+ * @param {Function} fn
* @api public
*/
-
- MemoryStore.prototype.destroy = function (sid, fn) {
- var store = this.store
-
- if (Array.isArray(sid)) {
- sid.forEach(function (s) {
- debug('DEL "%s"', s)
- store.delete(s)
- })
- } else {
- debug('DEL "%s"', sid)
- store.delete(sid)
+ destroy(sid, fn) {
+ debug('DESTROY %s', sid)
+ let err = null
+ try {
+ this.store.delete(sid)
+ } catch (e) {
+ err = e
}
- fn && defer(fn, null)
+ fn && setImmediate(fn, err)
}
/**
- * Refresh the time-to-live for the session with the given `sid`.
+ * Refresh the time-to-live for the session with the given `sid` without affecting
+ * LRU recency.
*
* @param {String} sid
* @param {Session} sess
@@ -176,128 +105,14 @@ module.exports = function (session) {
* @api public
*/
- MemoryStore.prototype.touch = function (sid, sess, fn) {
- var store = this.store
-
- var ttl = getTTL(this.options, sess, sid)
-
- debug('EXPIRE "%s" ttl:%s', sid, ttl)
- var err = null
- if (store.get(sid) !== undefined) {
- try {
- var s = this.serializer.parse(store.get(sid))
- s.cookie = sess.cookie
- store.set(sid, this.serializer.stringify(s), {
- ttl
- })
- } catch (e) {
- err = e
- }
- }
- fn && defer(fn, err)
- }
-
- /**
- * Fetch all sessions' ids
- *
- * @param {Function} fn
- * @api public
- */
-
- MemoryStore.prototype.ids = function (fn) {
- var store = this.store
-
- var Ids = store.keys()
- debug('Getting IDs: %s', Ids)
- fn && defer(fn, null, Ids)
- }
-
- /**
- * Fetch all sessions
- *
- * @param {Function} fn
- * @api public
- */
-
- MemoryStore.prototype.all = function (fn) {
- var store = this.store
- var self = this
-
- debug('Fetching all sessions')
- var err = null
- var result = {}
+ touch(sid, sess, fn) {
+ debug('TOUCH %s', sid)
+ let err = null
try {
- store.forEach(function (val, key) {
- result[key] = self.serializer.parse(val)
- })
+ this.store.has(sid, { updateAgeOnHas: true })
} catch (e) {
err = e
}
- fn && defer(fn, err, result)
- }
-
- /**
- * Delete all sessions from the store
- *
- * @param {Function} fn
- * @api public
- */
-
- MemoryStore.prototype.clear = function (fn) {
- var store = this.store
- debug('delete all sessions from the store')
- store.clear()
- fn && defer(fn, null)
- }
-
- /**
- * Get the count of all sessions in the store
- *
- * @param {Function} fn
- * @api public
- */
-
- MemoryStore.prototype.length = function (fn) {
- var store = this.store
- debug('getting length', store.size)
- fn && defer(fn, null, store.size)
- }
-
- /**
- * Start the check interval
- * @api public
- */
-
- MemoryStore.prototype.startInterval = function () {
- var self = this
- var ms = this.options.checkPeriod
- if (ms && typeof ms === 'number') {
- clearInterval(this._checkInterval)
- debug('starting periodic check for expired sessions')
- this._checkInterval = setInterval(function () {
- prune(self.store) // iterates over the entire cache proactively pruning old entries
- }, Math.floor(ms)).unref()
- }
- }
-
- /**
- * Stop the check interval
- * @api public
- */
-
- MemoryStore.prototype.stopInterval = function () {
- debug('stopping periodic check for expired sessions')
- clearInterval(this._checkInterval)
+ fn && setImmediate(fn, err)
}
-
- /**
- * Remove only expired entries from the store
- * @api public
- */
-
- MemoryStore.prototype.prune = function () {
- prune(this.store)
- }
-
- return MemoryStore
}
diff --git a/server/libs/umzug/LICENSE b/server/libs/umzug/LICENSE
new file mode 100644
index 0000000000..653d5f8190
--- /dev/null
+++ b/server/libs/umzug/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014-2017 Sequelize contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/server/libs/umzug/index.js b/server/libs/umzug/index.js
new file mode 100644
index 0000000000..d1e2e7c341
--- /dev/null
+++ b/server/libs/umzug/index.js
@@ -0,0 +1,31 @@
+'use strict'
+var __createBinding =
+ (this && this.__createBinding) ||
+ (Object.create
+ ? function (o, m, k, k2) {
+ if (k2 === undefined) k2 = k
+ var desc = Object.getOwnPropertyDescriptor(m, k)
+ if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = {
+ enumerable: true,
+ get: function () {
+ return m[k]
+ }
+ }
+ }
+ Object.defineProperty(o, k2, desc)
+ }
+ : function (o, m, k, k2) {
+ if (k2 === undefined) k2 = k
+ o[k2] = m[k]
+ })
+var __exportStar =
+ (this && this.__exportStar) ||
+ function (m, exports) {
+ for (var p in m) if (p !== 'default' && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p)
+ }
+Object.defineProperty(exports, '__esModule', { value: true })
+__exportStar(require('./umzug'), exports)
+__exportStar(require('./storage'), exports)
+__exportStar(require('./types'), exports)
+//# sourceMappingURL=index.js.map
diff --git a/server/libs/umzug/storage/contract.js b/server/libs/umzug/storage/contract.js
new file mode 100644
index 0000000000..a572faa32e
--- /dev/null
+++ b/server/libs/umzug/storage/contract.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.verifyUmzugStorage = exports.isUmzugStorage = void 0;
+function isUmzugStorage(arg) {
+ return (arg &&
+ typeof arg.logMigration === 'function' &&
+ typeof arg.unlogMigration === 'function' &&
+ typeof arg.executed === 'function');
+}
+exports.isUmzugStorage = isUmzugStorage;
+const verifyUmzugStorage = (arg) => {
+ if (!isUmzugStorage(arg)) {
+ throw new Error(`Invalid umzug storage`);
+ }
+ return arg;
+};
+exports.verifyUmzugStorage = verifyUmzugStorage;
+//# sourceMappingURL=contract.js.map
\ No newline at end of file
diff --git a/server/libs/umzug/storage/index.js b/server/libs/umzug/storage/index.js
new file mode 100644
index 0000000000..d99759cc9c
--- /dev/null
+++ b/server/libs/umzug/storage/index.js
@@ -0,0 +1,24 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __exportStar = (this && this.__exportStar) || function(m, exports) {
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+// codegen:start {preset: barrel}
+__exportStar(require("./contract"), exports);
+__exportStar(require("./json"), exports);
+__exportStar(require("./memory"), exports);
+__exportStar(require("./mongodb"), exports);
+__exportStar(require("./sequelize"), exports);
+// codegen:end
+//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/server/libs/umzug/storage/json.js b/server/libs/umzug/storage/json.js
new file mode 100644
index 0000000000..bd3a2aba7e
--- /dev/null
+++ b/server/libs/umzug/storage/json.js
@@ -0,0 +1,61 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.JSONStorage = void 0;
+const fs_1 = require("fs");
+const path = __importStar(require("path"));
+const filesystem = {
+ /** reads a file as a string or returns null if file doesn't exist */
+ async readAsync(filepath) {
+ return fs_1.promises.readFile(filepath).then(c => c.toString(), () => null);
+ },
+ /** writes a string as file contents, creating its parent directory if necessary */
+ async writeAsync(filepath, content) {
+ await fs_1.promises.mkdir(path.dirname(filepath), { recursive: true });
+ await fs_1.promises.writeFile(filepath, content);
+ },
+};
+class JSONStorage {
+ constructor(options) {
+ var _a;
+ this.path = (_a = options === null || options === void 0 ? void 0 : options.path) !== null && _a !== void 0 ? _a : path.join(process.cwd(), 'umzug.json');
+ }
+ async logMigration({ name: migrationName }) {
+ const loggedMigrations = await this.executed();
+ loggedMigrations.push(migrationName);
+ await filesystem.writeAsync(this.path, JSON.stringify(loggedMigrations, null, 2));
+ }
+ async unlogMigration({ name: migrationName }) {
+ const loggedMigrations = await this.executed();
+ const updatedMigrations = loggedMigrations.filter(name => name !== migrationName);
+ await filesystem.writeAsync(this.path, JSON.stringify(updatedMigrations, null, 2));
+ }
+ async executed() {
+ const content = await filesystem.readAsync(this.path);
+ return content ? JSON.parse(content) : [];
+ }
+}
+exports.JSONStorage = JSONStorage;
+//# sourceMappingURL=json.js.map
\ No newline at end of file
diff --git a/server/libs/umzug/storage/memory.js b/server/libs/umzug/storage/memory.js
new file mode 100644
index 0000000000..fd3ac2ec8a
--- /dev/null
+++ b/server/libs/umzug/storage/memory.js
@@ -0,0 +1,17 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.memoryStorage = void 0;
+const memoryStorage = () => {
+ let executed = [];
+ return {
+ async logMigration({ name }) {
+ executed.push(name);
+ },
+ async unlogMigration({ name }) {
+ executed = executed.filter(n => n !== name);
+ },
+ executed: async () => [...executed],
+ };
+};
+exports.memoryStorage = memoryStorage;
+//# sourceMappingURL=memory.js.map
\ No newline at end of file
diff --git a/server/libs/umzug/storage/mongodb.js b/server/libs/umzug/storage/mongodb.js
new file mode 100644
index 0000000000..111713300b
--- /dev/null
+++ b/server/libs/umzug/storage/mongodb.js
@@ -0,0 +1,31 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.MongoDBStorage = void 0;
+function isMongoDBCollectionOptions(arg) {
+ return Boolean(arg.collection);
+}
+class MongoDBStorage {
+ constructor(options) {
+ var _a, _b;
+ if (!options || (!options.collection && !options.connection)) {
+ throw new Error('MongoDB Connection or Collection required');
+ }
+ this.collection = isMongoDBCollectionOptions(options)
+ ? options.collection
+ : options.connection.collection((_a = options.collectionName) !== null && _a !== void 0 ? _a : 'migrations');
+ this.connection = options.connection; // TODO remove this
+ this.collectionName = (_b = options.collectionName) !== null && _b !== void 0 ? _b : 'migrations'; // TODO remove this
+ }
+ async logMigration({ name: migrationName }) {
+ await this.collection.insertOne({ migrationName });
+ }
+ async unlogMigration({ name: migrationName }) {
+ await this.collection.deleteOne({ migrationName });
+ }
+ async executed() {
+ const records = await this.collection.find({}).sort({ migrationName: 1 }).toArray();
+ return records.map(r => r.migrationName);
+ }
+}
+exports.MongoDBStorage = MongoDBStorage;
+//# sourceMappingURL=mongodb.js.map
\ No newline at end of file
diff --git a/server/libs/umzug/storage/sequelize.js b/server/libs/umzug/storage/sequelize.js
new file mode 100644
index 0000000000..784ca0bf72
--- /dev/null
+++ b/server/libs/umzug/storage/sequelize.js
@@ -0,0 +1,85 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.SequelizeStorage = void 0;
+const DIALECTS_WITH_CHARSET_AND_COLLATE = new Set(['mysql', 'mariadb']);
+class SequelizeStorage {
+ /**
+ Constructs Sequelize based storage. Migrations will be stored in a SequelizeMeta table using the given instance of Sequelize.
+
+ If a model is given, it will be used directly as the model for the SequelizeMeta table. Otherwise, it will be created automatically according to the given options.
+
+ If the table does not exist it will be created automatically upon the logging of the first migration.
+ */
+ constructor(options) {
+ var _a, _b, _c, _d, _e, _f;
+ if (!options || (!options.model && !options.sequelize)) {
+ throw new Error('One of "sequelize" or "model" storage option is required');
+ }
+ this.sequelize = (_a = options.sequelize) !== null && _a !== void 0 ? _a : options.model.sequelize;
+ this.columnType = (_b = options.columnType) !== null && _b !== void 0 ? _b : this.sequelize.constructor.DataTypes.STRING;
+ this.columnName = (_c = options.columnName) !== null && _c !== void 0 ? _c : 'name';
+ this.timestamps = (_d = options.timestamps) !== null && _d !== void 0 ? _d : false;
+ this.modelName = (_e = options.modelName) !== null && _e !== void 0 ? _e : 'SequelizeMeta';
+ this.tableName = options.tableName;
+ this.schema = options.schema;
+ this.model = (_f = options.model) !== null && _f !== void 0 ? _f : this.getModel();
+ }
+ getModel() {
+ var _a;
+ if (this.sequelize.isDefined(this.modelName)) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return this.sequelize.model(this.modelName);
+ }
+ const dialectName = (_a = this.sequelize.dialect) === null || _a === void 0 ? void 0 : _a.name;
+ const hasCharsetAndCollate = dialectName && DIALECTS_WITH_CHARSET_AND_COLLATE.has(dialectName);
+ return this.sequelize.define(this.modelName, {
+ [this.columnName]: {
+ type: this.columnType,
+ allowNull: false,
+ unique: true,
+ primaryKey: true,
+ autoIncrement: false,
+ },
+ }, {
+ tableName: this.tableName,
+ schema: this.schema,
+ timestamps: this.timestamps,
+ charset: hasCharsetAndCollate ? 'utf8' : undefined,
+ collate: hasCharsetAndCollate ? 'utf8_unicode_ci' : undefined,
+ });
+ }
+ async syncModel() {
+ await this.model.sync();
+ }
+ async logMigration({ name: migrationName }) {
+ await this.syncModel();
+ await this.model.create({
+ [this.columnName]: migrationName,
+ });
+ }
+ async unlogMigration({ name: migrationName }) {
+ await this.syncModel();
+ await this.model.destroy({
+ where: {
+ [this.columnName]: migrationName,
+ },
+ });
+ }
+ async executed() {
+ await this.syncModel();
+ const migrations = await this.model.findAll({ order: [[this.columnName, 'ASC']] });
+ return migrations.map(migration => {
+ const name = migration[this.columnName];
+ if (typeof name !== 'string') {
+ throw new TypeError(`Unexpected migration name type: expected string, got ${typeof name}`);
+ }
+ return name;
+ });
+ }
+ // TODO remove this
+ _model() {
+ return this.model;
+ }
+}
+exports.SequelizeStorage = SequelizeStorage;
+//# sourceMappingURL=sequelize.js.map
\ No newline at end of file
diff --git a/server/libs/umzug/templates.js b/server/libs/umzug/templates.js
new file mode 100644
index 0000000000..49d3716ce1
--- /dev/null
+++ b/server/libs/umzug/templates.js
@@ -0,0 +1,32 @@
+'use strict'
+/* eslint-disable unicorn/template-indent */
+// templates for migration file creation
+Object.defineProperty(exports, '__esModule', { value: true })
+exports.sqlDown = exports.sqlUp = exports.mjs = exports.ts = exports.js = void 0
+exports.js = `
+/** @type {import('umzug').MigrationFn} */
+exports.up = async params => {};
+
+/** @type {import('umzug').MigrationFn} */
+exports.down = async params => {};
+`.trimStart()
+exports.ts = `
+import type { MigrationFn } from 'umzug';
+
+export const up: MigrationFn = async params => {};
+export const down: MigrationFn = async params => {};
+`.trimStart()
+exports.mjs = `
+/** @type {import('umzug').MigrationFn} */
+export const up = async params => {};
+
+/** @type {import('umzug').MigrationFn} */
+export const down = async params => {};
+`.trimStart()
+exports.sqlUp = `
+-- up migration
+`.trimStart()
+exports.sqlDown = `
+-- down migration
+`.trimStart()
+//# sourceMappingURL=templates.js.map
diff --git a/server/libs/umzug/types.js b/server/libs/umzug/types.js
new file mode 100644
index 0000000000..8452b09b40
--- /dev/null
+++ b/server/libs/umzug/types.js
@@ -0,0 +1,12 @@
+'use strict'
+Object.defineProperty(exports, '__esModule', { value: true })
+exports.RerunBehavior = void 0
+exports.RerunBehavior = {
+ /** Hard error if an up migration that has already been run, or a down migration that hasn't, is encountered */
+ THROW: 'THROW',
+ /** Silently skip up migrations that have already been run, or down migrations that haven't */
+ SKIP: 'SKIP',
+ /** Re-run up migrations that have already been run, or down migrations that haven't */
+ ALLOW: 'ALLOW'
+}
+//# sourceMappingURL=types.js.map
diff --git a/server/libs/umzug/umzug.js b/server/libs/umzug/umzug.js
new file mode 100644
index 0000000000..916248750c
--- /dev/null
+++ b/server/libs/umzug/umzug.js
@@ -0,0 +1,386 @@
+'use strict'
+var __createBinding =
+ (this && this.__createBinding) ||
+ (Object.create
+ ? function (o, m, k, k2) {
+ if (k2 === undefined) k2 = k
+ var desc = Object.getOwnPropertyDescriptor(m, k)
+ if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = {
+ enumerable: true,
+ get: function () {
+ return m[k]
+ }
+ }
+ }
+ Object.defineProperty(o, k2, desc)
+ }
+ : function (o, m, k, k2) {
+ if (k2 === undefined) k2 = k
+ o[k2] = m[k]
+ })
+var __setModuleDefault =
+ (this && this.__setModuleDefault) ||
+ (Object.create
+ ? function (o, v) {
+ Object.defineProperty(o, 'default', { enumerable: true, value: v })
+ }
+ : function (o, v) {
+ o['default'] = v
+ })
+var __importStar =
+ (this && this.__importStar) ||
+ function (mod) {
+ if (mod && mod.__esModule) return mod
+ var result = {}
+ if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k)
+ __setModuleDefault(result, mod)
+ return result
+ }
+var __importDefault =
+ (this && this.__importDefault) ||
+ function (mod) {
+ return mod && mod.__esModule ? mod : { default: mod }
+ }
+var _a
+Object.defineProperty(exports, '__esModule', { value: true })
+exports.Umzug = exports.MigrationError = void 0
+const fs = __importStar(require('fs'))
+const path = __importStar(require('path'))
+const storage_1 = require('./storage')
+const templates = __importStar(require('./templates'))
+const types_1 = require('./types')
+class MigrationError extends Error {
+ // TODO [>=4.0.0] Take a `{ cause: ... }` options bag like the default `Error`, it looks like this because of verror backwards-compatibility.
+ constructor(migration, original) {
+ super(`Migration ${migration.name} (${migration.direction}) failed: ${MigrationError.errorString(original)}`, {
+ cause: original
+ })
+ this.name = 'MigrationError'
+ this.migration = migration
+ }
+ // TODO [>=4.0.0] Remove this backwards-compatibility alias
+ get info() {
+ return this.migration
+ }
+ static errorString(cause) {
+ return cause instanceof Error ? `Original error: ${cause.message}` : `Non-error value thrown. See info for full props: ${cause}`
+ }
+}
+exports.MigrationError = MigrationError
+class Umzug {
+ /** creates a new Umzug instance */
+ constructor(options) {
+ var _b
+ this.options = options
+ this.storage = (0, storage_1.verifyUmzugStorage)((_b = options.storage) !== null && _b !== void 0 ? _b : new storage_1.JSONStorage())
+ this.migrations = this.getMigrationsResolver(this.options.migrations)
+ }
+ logging(message) {
+ var _b
+ ;(_b = this.options.logger) === null || _b === void 0 ? void 0 : _b.info(message)
+ }
+ /** Get the list of migrations which have already been applied */
+ async executed() {
+ return this.runCommand('executed', async ({ context }) => {
+ const list = await this._executed(context)
+ // We do the following to not expose the `up` and `down` functions to the user
+ return list.map((m) => ({ name: m.name, path: m.path }))
+ })
+ }
+ /** Get the list of migrations which have already been applied */
+ async _executed(context) {
+ const [migrations, executedNames] = await Promise.all([this.migrations(context), this.storage.executed({ context })])
+ const executedSet = new Set(executedNames)
+ return migrations.filter((m) => executedSet.has(m.name))
+ }
+ /** Get the list of migrations which are yet to be applied */
+ async pending() {
+ return this.runCommand('pending', async ({ context }) => {
+ const list = await this._pending(context)
+ // We do the following to not expose the `up` and `down` functions to the user
+ return list.map((m) => ({ name: m.name, path: m.path }))
+ })
+ }
+ async _pending(context) {
+ const [migrations, executedNames] = await Promise.all([this.migrations(context), this.storage.executed({ context })])
+ const executedSet = new Set(executedNames)
+ return migrations.filter((m) => !executedSet.has(m.name))
+ }
+ async runCommand(command, cb) {
+ const context = await this.getContext()
+ return await cb({ context })
+ }
+ /**
+ * Apply migrations. By default, runs all pending migrations.
+ * @see MigrateUpOptions for other use cases using `to`, `migrations` and `rerun`.
+ */
+ async up(options = {}) {
+ const eligibleMigrations = async (context) => {
+ var _b
+ if (options.migrations && options.rerun === types_1.RerunBehavior.ALLOW) {
+ // Allow rerun means the specified migrations should be run even if they've run before - so get all migrations, not just pending
+ const list = await this.migrations(context)
+ return this.findMigrations(list, options.migrations)
+ }
+ if (options.migrations && options.rerun === types_1.RerunBehavior.SKIP) {
+ const executedNames = new Set((await this._executed(context)).map((m) => m.name))
+ const filteredMigrations = options.migrations.filter((m) => !executedNames.has(m))
+ return this.findMigrations(await this.migrations(context), filteredMigrations)
+ }
+ if (options.migrations) {
+ return this.findMigrations(await this._pending(context), options.migrations)
+ }
+ const allPending = await this._pending(context)
+ let sliceIndex = (_b = options.step) !== null && _b !== void 0 ? _b : allPending.length
+ if (options.to) {
+ sliceIndex = this.findNameIndex(allPending, options.to) + 1
+ }
+ return allPending.slice(0, sliceIndex)
+ }
+ return this.runCommand('up', async ({ context }) => {
+ const toBeApplied = await eligibleMigrations(context)
+ for (const m of toBeApplied) {
+ const start = Date.now()
+ const params = { name: m.name, path: m.path, context }
+ this.logging({ event: 'migrating', name: m.name })
+ try {
+ await m.up(params)
+ } catch (e) {
+ throw new MigrationError({ direction: 'up', ...params }, e)
+ }
+ await this.storage.logMigration(params)
+ const duration = (Date.now() - start) / 1000
+ this.logging({ event: 'migrated', name: m.name, durationSeconds: duration })
+ }
+ return toBeApplied.map((m) => ({ name: m.name, path: m.path }))
+ })
+ }
+ /**
+ * Revert migrations. By default, the last executed migration is reverted.
+ * @see MigrateDownOptions for other use cases using `to`, `migrations` and `rerun`.
+ */
+ async down(options = {}) {
+ const eligibleMigrations = async (context) => {
+ var _b
+ if (options.migrations && options.rerun === types_1.RerunBehavior.ALLOW) {
+ const list = await this.migrations(context)
+ return this.findMigrations(list, options.migrations)
+ }
+ if (options.migrations && options.rerun === types_1.RerunBehavior.SKIP) {
+ const pendingNames = new Set((await this._pending(context)).map((m) => m.name))
+ const filteredMigrations = options.migrations.filter((m) => !pendingNames.has(m))
+ return this.findMigrations(await this.migrations(context), filteredMigrations)
+ }
+ if (options.migrations) {
+ return this.findMigrations(await this._executed(context), options.migrations)
+ }
+ const executedReversed = (await this._executed(context)).slice().reverse()
+ let sliceIndex = (_b = options.step) !== null && _b !== void 0 ? _b : 1
+ if (options.to === 0 || options.migrations) {
+ sliceIndex = executedReversed.length
+ } else if (options.to) {
+ sliceIndex = this.findNameIndex(executedReversed, options.to) + 1
+ }
+ return executedReversed.slice(0, sliceIndex)
+ }
+ return this.runCommand('down', async ({ context }) => {
+ var _b
+ const toBeReverted = await eligibleMigrations(context)
+ for (const m of toBeReverted) {
+ const start = Date.now()
+ const params = { name: m.name, path: m.path, context }
+ this.logging({ event: 'reverting', name: m.name })
+ try {
+ await ((_b = m.down) === null || _b === void 0 ? void 0 : _b.call(m, params))
+ } catch (e) {
+ throw new MigrationError({ direction: 'down', ...params }, e)
+ }
+ await this.storage.unlogMigration(params)
+ const duration = Number.parseFloat(((Date.now() - start) / 1000).toFixed(3))
+ this.logging({ event: 'reverted', name: m.name, durationSeconds: duration })
+ }
+ return toBeReverted.map((m) => ({ name: m.name, path: m.path }))
+ })
+ }
+ async create(options) {
+ await this.runCommand('create', async ({ context }) => {
+ var _b, _c, _d, _e
+ const isoDate = new Date().toISOString()
+ const prefixes = {
+ TIMESTAMP: isoDate.replace(/\.\d{3}Z$/, '').replace(/\W/g, '.'),
+ DATE: isoDate.split('T')[0].replace(/\W/g, '.'),
+ NONE: ''
+ }
+ const prefixType = (_b = options.prefix) !== null && _b !== void 0 ? _b : 'TIMESTAMP'
+ const fileBasename = [prefixes[prefixType], options.name].filter(Boolean).join('.')
+ const allowedExtensions = options.allowExtension ? [options.allowExtension] : ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.sql']
+ const existing = await this.migrations(context)
+ const last = existing.slice(-1)[0]
+ const folder = options.folder || ((_c = this.options.create) === null || _c === void 0 ? void 0 : _c.folder) || ((last === null || last === void 0 ? void 0 : last.path) && path.dirname(last.path))
+ if (!folder) {
+ throw new Error(`Couldn't infer a directory to generate migration file in. Pass folder explicitly`)
+ }
+ const filepath = path.join(folder, fileBasename)
+ if (!options.allowConfusingOrdering) {
+ const confusinglyOrdered = existing.find((e) => e.path && e.path >= filepath)
+ if (confusinglyOrdered) {
+ throw new Error(`Can't create ${fileBasename}, since it's unclear if it should run before or after existing migration ${confusinglyOrdered.name}. Use allowConfusingOrdering to bypass this error.`)
+ }
+ }
+ const template =
+ typeof options.content === 'string'
+ ? async () => [[filepath, options.content]]
+ : // eslint-disable-next-line @typescript-eslint/unbound-method
+ (_e = (_d = this.options.create) === null || _d === void 0 ? void 0 : _d.template) !== null && _e !== void 0
+ ? _e
+ : Umzug.defaultCreationTemplate
+ const toWrite = await template(filepath)
+ if (toWrite.length === 0) {
+ toWrite.push([filepath, ''])
+ }
+ toWrite.forEach((pair) => {
+ if (!Array.isArray(pair) || pair.length !== 2) {
+ throw new Error(`Expected [filepath, content] pair. Check that the file template function returns an array of pairs.`)
+ }
+ const ext = path.extname(pair[0])
+ if (!allowedExtensions.includes(ext)) {
+ const allowStr = allowedExtensions.join(', ')
+ const message = `Extension ${ext} not allowed. Allowed extensions are ${allowStr}. See help for allowExtension to avoid this error.`
+ throw new Error(message)
+ }
+ fs.mkdirSync(path.dirname(pair[0]), { recursive: true })
+ fs.writeFileSync(pair[0], pair[1])
+ this.logging({ event: 'created', path: pair[0] })
+ })
+ if (!options.skipVerify) {
+ const [firstFilePath] = toWrite[0]
+ const pending = await this._pending(context)
+ if (!pending.some((p) => p.path && path.resolve(p.path) === path.resolve(firstFilePath))) {
+ const paths = pending.map((p) => p.path).join(', ')
+ throw new Error(`Expected ${firstFilePath} to be a pending migration but it wasn't! Pending migration paths: ${paths}. You should investigate this. Use skipVerify to bypass this error.`)
+ }
+ }
+ })
+ }
+ static defaultCreationTemplate(filepath) {
+ const ext = path.extname(filepath)
+ if ((ext === '.js' && typeof require.main === 'object') || ext === '.cjs') {
+ return [[filepath, templates.js]]
+ }
+ if (ext === '.ts' || ext === '.mts' || ext === '.cts') {
+ return [[filepath, templates.ts]]
+ }
+ if ((ext === '.js' && require.main === undefined) || ext === '.mjs') {
+ return [[filepath, templates.mjs]]
+ }
+ if (ext === '.sql') {
+ const downFilepath = path.join(path.dirname(filepath), 'down', path.basename(filepath))
+ return [
+ [filepath, templates.sqlUp],
+ [downFilepath, templates.sqlDown]
+ ]
+ }
+ return []
+ }
+ findNameIndex(migrations, name) {
+ const index = migrations.findIndex((m) => m.name === name)
+ if (index === -1) {
+ throw new Error(`Couldn't find migration to apply with name ${JSON.stringify(name)}`)
+ }
+ return index
+ }
+ findMigrations(migrations, names) {
+ const map = new Map(migrations.map((m) => [m.name, m]))
+ return names.map((name) => {
+ const migration = map.get(name)
+ if (!migration) {
+ throw new Error(`Couldn't find migration to apply with name ${JSON.stringify(name)}`)
+ }
+ return migration
+ })
+ }
+ async getContext() {
+ const { context = {} } = this.options
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return typeof context === 'function' ? context() : context
+ }
+ /** helper for parsing input migrations into a callback returning a list of ready-to-run migrations */
+ getMigrationsResolver(inputMigrations) {
+ var _b
+ if (Array.isArray(inputMigrations)) {
+ return async () => inputMigrations
+ }
+ if (typeof inputMigrations === 'function') {
+ // Lazy migrations definition, recurse.
+ return async (ctx) => {
+ const resolved = await inputMigrations(ctx)
+ return this.getMigrationsResolver(resolved)(ctx)
+ }
+ }
+ const paths = inputMigrations.files
+ const resolver = (_b = inputMigrations.resolve) !== null && _b !== void 0 ? _b : Umzug.defaultResolver
+ return async (context) => {
+ paths.sort()
+ return paths.map((unresolvedPath) => {
+ const filepath = path.resolve(unresolvedPath)
+ const name = path.basename(filepath)
+ return {
+ path: filepath,
+ ...resolver({ name, path: filepath, context })
+ }
+ })
+ }
+ }
+}
+exports.Umzug = Umzug
+_a = Umzug
+Umzug.defaultResolver = ({ name, path: filepath }) => {
+ if (!filepath) {
+ throw new Error(`Can't use default resolver for non-filesystem migrations`)
+ }
+ const ext = path.extname(filepath)
+ const languageSpecificHelp = {
+ '.ts': "TypeScript files can be required by adding `ts-node` as a dependency and calling `require('ts-node/register')` at the program entrypoint before running migrations.",
+ '.sql': 'Try writing a resolver which reads file content and executes it as a sql query.'
+ }
+ languageSpecificHelp['.cts'] = languageSpecificHelp['.ts']
+ languageSpecificHelp['.mts'] = languageSpecificHelp['.ts']
+ let loadModule
+ const jsExt = ext.replace(/\.([cm]?)ts$/, '.$1js')
+ const getModule = async () => {
+ try {
+ return await loadModule()
+ } catch (e) {
+ if ((e instanceof SyntaxError || e instanceof MissingResolverError) && ext in languageSpecificHelp) {
+ e.message += '\n\n' + languageSpecificHelp[ext]
+ }
+ throw e
+ }
+ }
+ if ((jsExt === '.js' && typeof require.main === 'object') || jsExt === '.cjs') {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ loadModule = async () => require(filepath)
+ } else if (jsExt === '.js' || jsExt === '.mjs') {
+ loadModule = async () => import(filepath)
+ } else {
+ loadModule = async () => {
+ throw new MissingResolverError(filepath)
+ }
+ }
+ return {
+ name,
+ path: filepath,
+ up: async ({ context }) => (await getModule()).up({ path: filepath, name, context }),
+ down: async ({ context }) => {
+ var _b, _c
+ return (_c = (_b = await getModule()).down) === null || _c === void 0 ? void 0 : _c.call(_b, { path: filepath, name, context })
+ }
+ }
+}
+class MissingResolverError extends Error {
+ constructor(filepath) {
+ super(`No resolver specified for file ${filepath}. See docs for guidance on how to write a custom resolver.`)
+ }
+}
+//# sourceMappingURL=umzug.js.map
diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js
index 77702d790a..d94e948987 100644
--- a/server/managers/AbMergeManager.js
+++ b/server/managers/AbMergeManager.js
@@ -53,20 +53,21 @@ class AbMergeManager {
async startAudiobookMerge(userId, libraryItem, options = {}) {
const task = new Task()
- const audiobookDirname = Path.basename(libraryItem.path)
- const targetFilename = audiobookDirname + '.m4b'
+ const audiobookBaseName = libraryItem.isFile ? Path.basename(libraryItem.path, Path.extname(libraryItem.path)) : Path.basename(libraryItem.path)
+ const targetFilename = audiobookBaseName + '.m4b'
const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id)
const tempFilepath = Path.join(itemCachePath, targetFilename)
const ffmetadataPath = Path.join(itemCachePath, 'ffmetadata.txt')
+ const libraryItemDir = libraryItem.isFile ? Path.dirname(libraryItem.path) : libraryItem.path
const taskData = {
libraryItemId: libraryItem.id,
- libraryItemPath: libraryItem.path,
+ libraryItemDir,
userId,
originalTrackPaths: libraryItem.media.tracks.map((t) => t.metadata.path),
inos: libraryItem.media.includedAudioFiles.map((f) => f.ino),
tempFilepath,
targetFilename,
- targetFilepath: Path.join(libraryItem.path, targetFilename),
+ targetFilepath: Path.join(libraryItemDir, targetFilename),
itemCachePath,
ffmetadataObject: ffmpegHelpers.getFFMetadataObject(libraryItem, 1),
chapters: libraryItem.media.chapters?.map((c) => ({ ...c })),
@@ -95,8 +96,8 @@ class AbMergeManager {
*/
async runAudiobookMerge(libraryItem, task, encodingOptions) {
// Make sure the target directory is writable
- if (!(await isWritable(libraryItem.path))) {
- Logger.error(`[AbMergeManager] Target directory is not writable: ${libraryItem.path}`)
+ if (!(await isWritable(task.data.libraryItemDir))) {
+ Logger.error(`[AbMergeManager] Target directory is not writable: ${task.data.libraryItemDir}`)
task.setFailed('Target directory is not writable')
this.removeTask(task, true)
return
diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js
index f970d5a8af..2dcbb1d445 100644
--- a/server/managers/AudioMetadataManager.js
+++ b/server/managers/AudioMetadataManager.js
@@ -7,6 +7,12 @@ const TaskManager = require('./TaskManager')
const Task = require('../objects/Task')
const fileUtils = require('../utils/fileUtils')
+/**
+ * @typedef UpdateMetadataOptions
+ * @property {boolean} [forceEmbedChapters=false] - Whether to force embed chapters.
+ * @property {boolean} [backup=false] - Whether to backup the files.
+ */
+
class AudioMetadataMangaer {
constructor() {
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
@@ -47,8 +53,8 @@ class AudioMetadataMangaer {
/**
*
* @param {string} userId
- * @param {*} libraryItem
- * @param {*} options
+ * @param {import('../objects/LibraryItem')} libraryItem
+ * @param {UpdateMetadataOptions} [options={}]
*/
async updateMetadataForItem(userId, libraryItem, options = {}) {
const forceEmbedChapters = !!options.forceEmbedChapters
@@ -67,9 +73,10 @@ class AudioMetadataMangaer {
if (audioFiles.some((a) => a.mimeType !== mimeType)) mimeType = null
// Create task
+ const libraryItemDir = libraryItem.isFile ? Path.dirname(libraryItem.path) : libraryItem.path
const taskData = {
libraryItemId: libraryItem.id,
- libraryItemPath: libraryItem.path,
+ libraryItemDir,
userId,
audioFiles: audioFiles.map((af) => ({
index: af.index,
@@ -112,10 +119,10 @@ class AudioMetadataMangaer {
Logger.info(`[AudioMetadataManager] Starting metadata embed task`, task.description)
// Ensure target directory is writable
- const targetDirWritable = await fileUtils.isWritable(task.data.libraryItemPath)
- Logger.debug(`[AudioMetadataManager] Target directory ${task.data.libraryItemPath} writable: ${targetDirWritable}`)
+ const targetDirWritable = await fileUtils.isWritable(task.data.libraryItemDir)
+ Logger.debug(`[AudioMetadataManager] Target directory ${task.data.libraryItemDir} writable: ${targetDirWritable}`)
if (!targetDirWritable) {
- Logger.error(`[AudioMetadataManager] Target directory is not writable: ${task.data.libraryItemPath}`)
+ Logger.error(`[AudioMetadataManager] Target directory is not writable: ${task.data.libraryItemDir}`)
task.setFailed('Target directory is not writable')
this.handleTaskFinished(task)
return
diff --git a/server/managers/CacheManager.js b/server/managers/CacheManager.js
index 8f810a3307..b4d2f270c2 100644
--- a/server/managers/CacheManager.js
+++ b/server/managers/CacheManager.js
@@ -124,6 +124,13 @@ class CacheManager {
await this.ensureCachePaths()
}
+ /**
+ *
+ * @param {import('express').Response} res
+ * @param {import('../models/Author')} author
+ * @param {{ format?: string, width?: number, height?: number }} options
+ * @returns
+ */
async handleAuthorCache(res, author, options = {}) {
const format = options.format || 'webp'
const width = options.width || 400
diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js
index b35cf804a8..7a8c9bd0e3 100644
--- a/server/managers/CronManager.js
+++ b/server/managers/CronManager.js
@@ -21,7 +21,8 @@ class CronManager {
/**
* Initialize library scan crons & podcast download crons
- * @param {import('../objects/Library')[]} libraries
+ *
+ * @param {import('../models/Library')[]} libraries
*/
async init(libraries) {
this.initOpenSessionCleanupCron()
@@ -46,7 +47,7 @@ class CronManager {
/**
* Initialize library scan crons
- * @param {import('../objects/Library')[]} libraries
+ * @param {import('../models/Library')[]} libraries
*/
initLibraryScanCrons(libraries) {
for (const library of libraries) {
@@ -59,12 +60,12 @@ class CronManager {
/**
* Start cron schedule for library
*
- * @param {import('../objects/Library')} _library
+ * @param {import('../models/Library')} _library
*/
startCronForLibrary(_library) {
Logger.debug(`[CronManager] Init library scan cron for ${_library.name} on schedule ${_library.settings.autoScanCronExpression}`)
const libScanCron = cron.schedule(_library.settings.autoScanCronExpression, async () => {
- const library = await Database.libraryModel.getOldById(_library.id)
+ const library = await Database.libraryModel.findByIdWithFolders(_library.id)
if (!library) {
Logger.error(`[CronManager] Library not found for scan cron ${_library.id}`)
} else {
@@ -79,11 +80,19 @@ class CronManager {
})
}
+ /**
+ *
+ * @param {import('../models/Library')} library
+ */
removeCronForLibrary(library) {
Logger.debug(`[CronManager] Removing library scan cron for ${library.name}`)
this.libraryScanCrons = this.libraryScanCrons.filter((lsc) => lsc.libraryId !== library.id)
}
+ /**
+ *
+ * @param {import('../models/Library')} library
+ */
updateLibraryScanCron(library) {
const expression = library.settings.autoScanCronExpression
const existingCron = this.libraryScanCrons.find((lsc) => lsc.libraryId === library.id)
diff --git a/server/managers/MigrationManager.js b/server/managers/MigrationManager.js
new file mode 100644
index 0000000000..53db461bfb
--- /dev/null
+++ b/server/managers/MigrationManager.js
@@ -0,0 +1,275 @@
+const { Umzug, SequelizeStorage } = require('../libs/umzug')
+const { Sequelize, DataTypes } = require('sequelize')
+const semver = require('semver')
+const path = require('path')
+const Module = require('module')
+const fs = require('../libs/fsExtra')
+const Logger = require('../Logger')
+
+class MigrationManager {
+ static MIGRATIONS_META_TABLE = 'migrationsMeta'
+
+ /**
+ * @param {import('../Database').sequelize} sequelize
+ * @param {string} [configPath]
+ */
+ constructor(sequelize, configPath = global.configPath) {
+ if (!sequelize || !(sequelize instanceof Sequelize)) throw new Error('Sequelize instance is required for MigrationManager.')
+ this.sequelize = sequelize
+ if (!configPath) throw new Error('Config path is required for MigrationManager.')
+ this.configPath = configPath
+ this.migrationsSourceDir = path.join(__dirname, '..', 'migrations')
+ this.initialized = false
+ this.migrationsDir = null
+ this.maxVersion = null
+ this.databaseVersion = null
+ this.serverVersion = null
+ this.umzug = null
+ }
+
+ /**
+ * Init version vars and copy migration files to config dir if necessary
+ *
+ * @param {string} serverVersion
+ */
+ async init(serverVersion) {
+ if (!(await fs.pathExists(this.configPath))) throw new Error(`Config path does not exist: ${this.configPath}`)
+
+ this.migrationsDir = path.join(this.configPath, 'migrations')
+
+ this.serverVersion = this.extractVersionFromTag(serverVersion)
+ if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`)
+
+ await this.fetchVersionsFromDatabase()
+ if (!this.maxVersion || !this.databaseVersion) throw new Error('Failed to fetch versions from the database.')
+
+ if (semver.gt(this.serverVersion, this.maxVersion)) {
+ try {
+ await this.copyMigrationsToConfigDir()
+ } catch (error) {
+ throw new Error('Failed to copy migrations to the config directory.', { cause: error })
+ }
+
+ try {
+ await this.updateMaxVersion()
+ } catch (error) {
+ throw new Error('Failed to update max version in the database.', { cause: error })
+ }
+ }
+
+ this.initialized = true
+ }
+
+ async runMigrations() {
+ if (!this.initialized) throw new Error('MigrationManager is not initialized. Call init() first.')
+
+ const versionCompare = semver.compare(this.serverVersion, this.databaseVersion)
+ if (versionCompare == 0) {
+ Logger.info('[MigrationManager] Database is already up to date.')
+ return
+ }
+
+ await this.initUmzug()
+ const migrations = await this.umzug.migrations()
+ const executedMigrations = (await this.umzug.executed()).map((m) => m.name)
+
+ const migrationDirection = versionCompare == 1 ? 'up' : 'down'
+
+ let migrationsToRun = []
+ migrationsToRun = this.findMigrationsToRun(migrations, executedMigrations, migrationDirection)
+
+ // Only proceed with migration if there are migrations to run
+ if (migrationsToRun.length > 0) {
+ const originalDbPath = path.join(this.configPath, 'absdatabase.sqlite')
+ const backupDbPath = path.join(this.configPath, 'absdatabase.backup.sqlite')
+ try {
+ Logger.info(`[MigrationManager] Migrating database ${migrationDirection} to version ${this.serverVersion}`)
+ Logger.info(`[MigrationManager] Migrations to run: ${migrationsToRun.join(', ')}`)
+ // Create a backup copy of the SQLite database before starting migrations
+ await fs.copy(originalDbPath, backupDbPath)
+ Logger.info('Created a backup of the original database.')
+
+ // Run migrations
+ await this.umzug[migrationDirection]({ migrations: migrationsToRun, rerun: 'ALLOW' })
+
+ // Clean up the backup
+ await fs.remove(backupDbPath)
+
+ Logger.info('[MigrationManager] Migrations successfully applied to the original database.')
+ } catch (error) {
+ Logger.error('[MigrationManager] Migration failed:', error)
+
+ await this.sequelize.close()
+
+ // Step 3: If migration fails, save the failed original and restore the backup
+ const failedDbPath = path.join(this.configPath, 'absdatabase.failed.sqlite')
+ await fs.move(originalDbPath, failedDbPath, { overwrite: true })
+ Logger.info('[MigrationManager] Saved the failed database as absdatabase.failed.sqlite.')
+
+ await fs.move(backupDbPath, originalDbPath, { overwrite: true })
+ Logger.info('[MigrationManager] Restored the original database from the backup.')
+
+ Logger.info('[MigrationManager] Migration failed. Exiting Audiobookshelf with code 1.')
+ process.exit(1)
+ }
+ } else {
+ Logger.info('[MigrationManager] No migrations to run.')
+ }
+
+ await this.updateDatabaseVersion()
+ }
+
+ 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 parent = new Umzug({
+ migrations: {
+ files,
+ resolve: (params) => {
+ // make script think it's in migrationsSourceDir
+ const migrationPath = params.path
+ const migrationName = params.name
+ const contents = fs.readFileSync(migrationPath, 'utf8')
+ const fakePath = path.join(this.migrationsSourceDir, path.basename(migrationPath))
+ const module = new Module(fakePath)
+ module.filename = fakePath
+ module.paths = Module._nodeModulePaths(this.migrationsSourceDir)
+ module._compile(contents, fakePath)
+ const script = module.exports
+ return {
+ name: migrationName,
+ path: migrationPath,
+ up: script.up,
+ down: script.down
+ }
+ }
+ },
+ context: { queryInterface: this.sequelize.getQueryInterface(), logger: Logger },
+ storage: umzugStorage,
+ logger: Logger
+ })
+
+ // Sort migrations by version
+ this.umzug = new Umzug({
+ ...parent.options,
+ migrations: async () =>
+ (await parent.migrations()).sort((a, b) => {
+ const versionA = this.extractVersionFromTag(a.name)
+ const versionB = this.extractVersionFromTag(b.name)
+ return semver.compare(versionA, versionB)
+ })
+ })
+ }
+
+ async fetchVersionsFromDatabase() {
+ await this.checkOrCreateMigrationsMetaTable()
+
+ const [{ version }] = await this.sequelize.query("SELECT value as version FROM :migrationsMeta WHERE key = 'version'", {
+ replacements: { migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
+ type: Sequelize.QueryTypes.SELECT
+ })
+ this.databaseVersion = version
+
+ const [{ maxVersion }] = await this.sequelize.query("SELECT value as maxVersion FROM :migrationsMeta WHERE key = 'maxVersion'", {
+ replacements: { migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
+ type: Sequelize.QueryTypes.SELECT
+ })
+ this.maxVersion = maxVersion
+ }
+
+ async checkOrCreateMigrationsMetaTable() {
+ const queryInterface = this.sequelize.getQueryInterface()
+ if (!(await queryInterface.tableExists(MigrationManager.MIGRATIONS_META_TABLE))) {
+ await queryInterface.createTable(MigrationManager.MIGRATIONS_META_TABLE, {
+ key: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ value: {
+ type: DataTypes.STRING,
+ allowNull: false
+ }
+ })
+ await this.sequelize.query("INSERT INTO :migrationsMeta (key, value) VALUES ('version', :version), ('maxVersion', '0.0.0')", {
+ replacements: { version: this.serverVersion, migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
+ type: Sequelize.QueryTypes.INSERT
+ })
+ }
+ }
+
+ extractVersionFromTag(tag) {
+ if (!tag) return null
+ const versionMatch = tag.match(/^v?(\d+\.\d+\.\d+)/)
+ return versionMatch ? versionMatch[1] : null
+ }
+
+ 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)
+ await Promise.all(
+ files
+ .filter((file) => path.extname(file) === '.js')
+ .map(async (file) => {
+ const sourceFile = path.join(this.migrationsSourceDir, file)
+ const targetFile = path.join(this.migrationsDir, file)
+ await fs.copy(sourceFile, targetFile) // Asynchronously copy the files
+ })
+ )
+ }
+
+ /**
+ *
+ * @param {{ name: string }[]} migrations
+ * @param {string[]} executedMigrations - names of executed migrations
+ * @param {string} direction - 'up' or 'down'
+ * @returns {string[]} - names of migrations to run
+ */
+ findMigrationsToRun(migrations, executedMigrations, direction) {
+ const migrationsToRun = migrations
+ .filter((migration) => {
+ const migrationVersion = this.extractVersionFromTag(migration.name)
+ if (direction === 'up') {
+ return semver.gt(migrationVersion, this.databaseVersion) && semver.lte(migrationVersion, this.serverVersion) && !executedMigrations.includes(migration.name)
+ } else {
+ // A down migration should be run even if the associated up migration wasn't executed before
+ return semver.lte(migrationVersion, this.databaseVersion) && semver.gt(migrationVersion, this.serverVersion)
+ }
+ })
+ .map((migration) => migration.name)
+ if (direction === 'down') {
+ return migrationsToRun.reverse()
+ } else {
+ return migrationsToRun
+ }
+ }
+
+ async updateMaxVersion() {
+ try {
+ await this.sequelize.query("UPDATE :migrationsMeta SET value = :maxVersion WHERE key = 'maxVersion'", {
+ replacements: { maxVersion: this.serverVersion, migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
+ type: Sequelize.QueryTypes.UPDATE
+ })
+ } catch (error) {
+ throw new Error('Failed to update maxVersion in the migrationsMeta table.', { cause: error })
+ }
+ this.maxVersion = this.serverVersion
+ }
+
+ async updateDatabaseVersion() {
+ try {
+ await this.sequelize.query("UPDATE :migrationsMeta SET value = :version WHERE key = 'version'", {
+ replacements: { version: this.serverVersion, migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
+ type: Sequelize.QueryTypes.UPDATE
+ })
+ } catch (error) {
+ throw new Error('Failed to update version in the migrationsMeta table.', { cause: error })
+ }
+ this.databaseVersion = this.serverVersion
+ }
+}
+
+module.exports = MigrationManager
diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js
index f8bd75114a..a59c128154 100644
--- a/server/managers/NotificationManager.js
+++ b/server/managers/NotificationManager.js
@@ -23,7 +23,7 @@ class NotificationManager {
}
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
- const library = await Database.libraryModel.getOldById(libraryItem.libraryId)
+ const library = await Database.libraryModel.findByPk(libraryItem.libraryId)
const eventData = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js
index cafd6ff451..4318841e1e 100644
--- a/server/managers/PlaybackSessionManager.js
+++ b/server/managers/PlaybackSessionManager.js
@@ -51,16 +51,16 @@ class PlaybackSessionManager {
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user?.id)
if (clientDeviceInfo?.deviceId) {
- const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId)
+ const existingDevice = await Database.deviceModel.getOldDeviceByDeviceId(clientDeviceInfo.deviceId)
if (existingDevice) {
if (existingDevice.update(deviceInfo)) {
- await Database.updateDevice(existingDevice)
+ await Database.deviceModel.updateFromOld(existingDevice)
}
return existingDevice
}
}
- await Database.createDevice(deviceInfo)
+ await Database.deviceModel.createFromOld(deviceInfo)
return deviceInfo
}
@@ -164,6 +164,7 @@ class PlaybackSessionManager {
// New session from local
session = new PlaybackSession(sessionJson)
session.deviceInfo = deviceInfo
+ session.setDuration(libraryItem, sessionJson.episodeId)
Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`)
await Database.createPlaybackSession(session)
} else {
@@ -293,37 +294,27 @@ class PlaybackSessionManager {
const newPlaybackSession = new PlaybackSession()
newPlaybackSession.setData(libraryItem, user.id, mediaPlayer, deviceInfo, userStartTime, episodeId)
- if (libraryItem.mediaType === 'video') {
- if (shouldDirectPlay) {
- Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id}`)
- newPlaybackSession.videoTrack = libraryItem.media.getVideoTrack()
- newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
- } else {
- // HLS not supported for video yet
- }
+ let audioTracks = []
+ if (shouldDirectPlay) {
+ Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`)
+ audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
+ newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
} else {
- let audioTracks = []
- if (shouldDirectPlay) {
- Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`)
- audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
- newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
- } else {
- Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`)
- const stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime)
- await stream.generatePlaylist()
- stream.start() // Start transcode
-
- audioTracks = [stream.getAudioTrack()]
- newPlaybackSession.stream = stream
- newPlaybackSession.playMethod = PlayMethod.TRANSCODE
-
- stream.on('closed', () => {
- Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}" (Device: ${newPlaybackSession.deviceDescription})`)
- newPlaybackSession.stream = null
- })
- }
- newPlaybackSession.audioTracks = audioTracks
+ Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`)
+ const stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime)
+ await stream.generatePlaylist()
+ stream.start() // Start transcode
+
+ audioTracks = [stream.getAudioTrack()]
+ newPlaybackSession.stream = stream
+ newPlaybackSession.playMethod = PlayMethod.TRANSCODE
+
+ stream.on('closed', () => {
+ Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}" (Device: ${newPlaybackSession.deviceDescription})`)
+ newPlaybackSession.stream = null
+ })
}
+ newPlaybackSession.audioTracks = audioTracks
this.sessions.push(newPlaybackSession)
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions))
diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js
index 35ce4e1f83..7716440dfa 100644
--- a/server/managers/RssFeedManager.js
+++ b/server/managers/RssFeedManager.js
@@ -25,7 +25,7 @@ class RssFeedManager {
return false
}
} else if (feedObj.entityType === 'series') {
- const series = await Database.seriesModel.getOldById(feedObj.entityId)
+ const series = await Database.seriesModel.findByPk(feedObj.entityId)
if (!series) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`)
return false
@@ -133,9 +133,9 @@ class RssFeedManager {
}
}
} else if (feed.entityType === 'series') {
- const series = await Database.seriesModel.getOldById(feed.entityId)
+ const series = await Database.seriesModel.findByPk(feed.entityId)
if (series) {
- const seriesJson = series.toJSON()
+ const seriesJson = series.toOldJSON()
// Get books in series that have audio tracks
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md
new file mode 100644
index 0000000000..2e3c295af1
--- /dev/null
+++ b/server/migrations/changelog.md
@@ -0,0 +1,7 @@
+# Migrations Changelog
+
+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 |
+| -------------- | --------------------- | ----------- |
+| | | |
diff --git a/server/migrations/readme.md b/server/migrations/readme.md
new file mode 100644
index 0000000000..5133d7a25c
--- /dev/null
+++ b/server/migrations/readme.md
@@ -0,0 +1,49 @@
+# Database Migrations
+
+This directory contains all the database migration scripts for the server.
+
+## What is a migration?
+
+A migration is a script that changes the structure of the database. This can include creating tables, adding columns, or modifying existing columns. A migration script consists of two parts: an "up" script that applies the changes to the database, and a "down" script that undoes the changes.
+
+## Guidelines for writing migrations
+
+When writing a migration, keep the following guidelines in mind:
+
+- You **_must_** name your migration script according to the following convention: `-.js`. For example, `v2.14.0-create-users-table.js`.
+
+ - `server_version` should be the version of the server that the migration was created for (this should usually be the next server release).
+ - `migration_name` should be a short description of the changes that the migration makes.
+
+- The script should export two async functions: `up` and `down`. The `up` function should contain the script that applies the changes to the database, and the `down` function should contain the script that undoes the changes. The `up` and `down` functions should accept a single object parameter with a `context` property that contains a reference to a Sequelize [`QueryInterface`](https://sequelize.org/docs/v6/other-topics/query-interface/) object, and a [Logger](https://github.com/advplyr/audiobookshelf/blob/423a2129d10c6d8aaac9e8c75941fa6283889602/server/Logger.js#L4) object for logging. A typical migration script might look like this:
+
+ ```javascript
+ async function up({ context: { queryInterface, logger } }) {
+ // Upwards migration script
+ logger.info('migrating ...');
+ ...
+ }
+
+ async function down({ context: { queryInterface, logger } }) {
+ // Downward migration script
+ logger.info('reverting ...');
+ ...
+ }
+
+ module.exports = {up, down}
+ ```
+
+- Always implement both the `up` and `down` functions.
+- The `up` and `down` functions should be idempotent (i.e., they should be safe to run multiple times).
+- Prefer using only `queryInterface` and `logger` parameters, the `sequelize` module, and node.js built-in modules in your migration scripts. You can require other modules, but be aware that they might not be available or change from they ones you tested with.
+- It's your responsibility to make sure that the down migration reverts the changes made by the up migration.
+- Log detailed information on every step of the migration. Use `Logger.info()` and `Logger.error()`.
+- Test tour migrations thoroughly before committing them.
+ - write unit tests for your migrations (see `test/server/migrations` for an example)
+ - you can force a server version change by modifying the `version` field in `package.json` on your dev environment (but don't forget to revert it back before committing)
+
+## How migrations are run
+
+Migrations are run automatically when the server starts, when the server detects that the server version has changed. Migrations are always run in server version order (from oldest to newest up migrations if the server version increased, and from newest to oldest down migrations if the server version decreased). Only the relevant migrations are run, based on the new and old server versions.
+
+This means that you can switch between server releases without having to worry about running migrations manually. The server will automatically apply the necessary migrations when it starts.
diff --git a/server/models/Author.js b/server/models/Author.js
index a49141d73b..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 oldAuthor = require('../objects/entities/Author')
+const parseNameString = require('../utils/parsers/parseNameString')
class Author extends Model {
constructor(values, options) {
@@ -26,67 +25,14 @@ class Author extends Model {
this.createdAt
}
- getOldAuthor() {
- return new oldAuthor({
- id: this.id,
- asin: this.asin,
- name: this.name,
- description: this.description,
- imagePath: this.imagePath,
- libraryId: this.libraryId,
- addedAt: this.createdAt.valueOf(),
- updatedAt: this.updatedAt.valueOf()
- })
- }
-
- static updateFromOld(oldAuthor) {
- const author = this.getFromOld(oldAuthor)
- return this.update(author, {
- where: {
- id: author.id
- }
- })
- }
-
- static createFromOld(oldAuthor) {
- const author = this.getFromOld(oldAuthor)
- return this.create(author)
- }
-
- static createBulkFromOld(oldAuthors) {
- const authors = oldAuthors.map(this.getFromOld)
- return this.bulkCreate(authors)
- }
-
- static getFromOld(oldAuthor) {
- return {
- id: oldAuthor.id,
- name: oldAuthor.name,
- lastFirst: oldAuthor.lastFirst,
- asin: oldAuthor.asin,
- description: oldAuthor.description,
- imagePath: oldAuthor.imagePath,
- libraryId: oldAuthor.libraryId
- }
- }
-
- static removeById(authorId) {
- return this.destroy({
- where: {
- id: authorId
- }
- })
- }
-
/**
- * Get oldAuthor by id
- * @param {string} authorId
- * @returns {Promise}
+ *
+ * @param {string} name
+ * @returns {string}
*/
- static async getOldById(authorId) {
- const author = await this.findByPk(authorId)
- if (!author) return null
- return author.getOldAuthor()
+ static getLastFirst(name) {
+ if (!name) return null
+ return parseNameString.nameToLastFirst(name)
}
/**
@@ -99,25 +45,22 @@ class Author extends Model {
}
/**
- * Get old author by name and libraryId. name case insensitive
+ * Get author by name and libraryId. name case insensitive
* TODO: Look for authors ignoring punctuation
*
* @param {string} authorName
* @param {string} libraryId
- * @returns {Promise}
+ * @returns {Promise}
*/
- static async getOldByNameAndLibrary(authorName, libraryId) {
- const author = (
- await this.findOne({
- where: [
- where(fn('lower', col('name')), authorName.toLowerCase()),
- {
- libraryId
- }
- ]
- })
- )?.getOldAuthor()
- return author
+ static async getByNameAndLibrary(authorName, libraryId) {
+ return this.findOne({
+ where: [
+ where(fn('lower', col('name')), authorName.toLowerCase()),
+ {
+ libraryId
+ }
+ ]
+ })
}
/**
@@ -213,5 +156,36 @@ class Author extends Model {
})
Author.belongsTo(library)
}
+
+ toOldJSON() {
+ return {
+ id: this.id,
+ asin: this.asin,
+ name: this.name,
+ description: this.description,
+ imagePath: this.imagePath,
+ libraryId: this.libraryId,
+ addedAt: this.createdAt.valueOf(),
+ updatedAt: this.updatedAt.valueOf()
+ }
+ }
+
+ /**
+ *
+ * @param {number} numBooks
+ * @returns
+ */
+ toOldJSONExpanded(numBooks = 0) {
+ const oldJson = this.toOldJSON()
+ oldJson.numBooks = numBooks
+ return oldJson
+ }
+
+ toJSONMinimal() {
+ return {
+ id: this.id,
+ name: this.name
+ }
+ }
}
module.exports = Author
diff --git a/server/models/Collection.js b/server/models/Collection.js
index dcc86e5a56..a001dc5b61 100644
--- a/server/models/Collection.js
+++ b/server/models/Collection.js
@@ -38,7 +38,7 @@ class Collection extends Model {
// Optionally include rssfeed for collection
const collectionIncludes = []
- if (include.includes('rssfeed')) {
+ if (include?.includes('rssfeed')) {
collectionIncludes.push({
model: this.sequelize.models.feed
})
@@ -115,78 +115,6 @@ class Collection extends Model {
.filter((c) => c)
}
- /**
- * Get old collection toJSONExpanded, items filtered for user permissions
- *
- * @param {import('./User')|null} user
- * @param {string[]} [include]
- * @returns {Promise} oldCollection.toJSONExpanded
- */
- async getOldJsonExpanded(user, include) {
- this.books =
- (await this.getBooks({
- include: [
- {
- model: this.sequelize.models.libraryItem
- },
- {
- model: this.sequelize.models.author,
- through: {
- attributes: []
- }
- },
- {
- model: this.sequelize.models.series,
- through: {
- attributes: ['sequence']
- }
- }
- ],
- order: [Sequelize.literal('`collectionBook.order` ASC')]
- })) || []
-
- const oldCollection = this.sequelize.models.collection.getOldCollection(this)
-
- // Filter books using user permissions
- // TODO: Handle user permission restrictions on initial query
- const books =
- this.books?.filter((b) => {
- if (user) {
- if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
- return false
- }
- if (b.explicit === true && !user.canAccessExplicitContent) {
- return false
- }
- }
- return true
- }) || []
-
- // Map to library items
- const libraryItems = books.map((b) => {
- const libraryItem = b.libraryItem
- delete b.libraryItem
- libraryItem.media = b
- return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
- })
-
- // Users with restricted permissions will not see this collection
- if (!books.length && oldCollection.books.length) {
- return null
- }
-
- const collectionExpanded = oldCollection.toJSONExpanded(libraryItems)
-
- if (include?.includes('rssfeed')) {
- const feeds = await this.getFeeds()
- if (feeds?.length) {
- collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0])
- }
- }
-
- return collectionExpanded
- }
-
/**
* Get old collection from Collection
* @param {Collection} collectionExpanded
@@ -250,36 +178,6 @@ class Collection extends Model {
return this.getOldCollection(collection)
}
- /**
- * Get old collection from current
- * @returns {Promise}
- */
- async getOld() {
- this.books =
- (await this.getBooks({
- include: [
- {
- model: this.sequelize.models.libraryItem
- },
- {
- model: this.sequelize.models.author,
- through: {
- attributes: []
- }
- },
- {
- model: this.sequelize.models.series,
- through: {
- attributes: ['sequence']
- }
- }
- ],
- order: [Sequelize.literal('`collectionBook.order` ASC')]
- })) || []
-
- return this.sequelize.models.collection.getOldCollection(this)
- }
-
/**
* Remove all collections belonging to library
* @param {string} libraryId
@@ -320,6 +218,109 @@ class Collection extends Model {
library.hasMany(Collection)
Collection.belongsTo(library)
}
+
+ /**
+ * Get old collection toJSONExpanded, items filtered for user permissions
+ *
+ * @param {import('./User')|null} user
+ * @param {string[]} [include]
+ * @returns {Promise} oldCollection.toJSONExpanded
+ */
+ async getOldJsonExpanded(user, include) {
+ this.books =
+ (await this.getBooks({
+ include: [
+ {
+ model: this.sequelize.models.libraryItem
+ },
+ {
+ model: this.sequelize.models.author,
+ through: {
+ attributes: []
+ }
+ },
+ {
+ model: this.sequelize.models.series,
+ through: {
+ attributes: ['sequence']
+ }
+ }
+ ],
+ order: [Sequelize.literal('`collectionBook.order` ASC')]
+ })) || []
+
+ // Filter books using user permissions
+ // TODO: Handle user permission restrictions on initial query
+ const books =
+ this.books?.filter((b) => {
+ if (user) {
+ if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
+ return false
+ }
+ if (b.explicit === true && !user.canAccessExplicitContent) {
+ return false
+ }
+ }
+ return true
+ }) || []
+
+ // Map to library items
+ const libraryItems = books.map((b) => {
+ const libraryItem = b.libraryItem
+ delete b.libraryItem
+ libraryItem.media = b
+ return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
+ })
+
+ // Users with restricted permissions will not see this collection
+ if (!books.length && this.books.length) {
+ return null
+ }
+
+ const collectionExpanded = this.toOldJSONExpanded(libraryItems)
+
+ if (include?.includes('rssfeed')) {
+ const feeds = await this.getFeeds()
+ if (feeds?.length) {
+ collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0])
+ }
+ }
+
+ return collectionExpanded
+ }
+
+ /**
+ *
+ * @param {string[]} libraryItemIds
+ * @returns
+ */
+ toOldJSON(libraryItemIds) {
+ return {
+ id: this.id,
+ libraryId: this.libraryId,
+ name: this.name,
+ description: this.description,
+ books: [...libraryItemIds],
+ lastUpdate: this.updatedAt.valueOf(),
+ createdAt: this.createdAt.valueOf()
+ }
+ }
+
+ /**
+ *
+ * @param {import('../objects/LibraryItem')} oldLibraryItems
+ * @returns
+ */
+ toOldJSONExpanded(oldLibraryItems) {
+ const json = this.toOldJSON(oldLibraryItems.map((li) => li.id))
+ json.books = json.books
+ .map((libraryItemId) => {
+ const book = oldLibraryItems.find((li) => li.id === libraryItemId)
+ return book ? book.toJSONExpanded() : null
+ })
+ .filter((b) => !!b)
+ return json
+ }
}
module.exports = Collection
diff --git a/server/models/CustomMetadataProvider.js b/server/models/CustomMetadataProvider.js
index 8218e41961..ca2e20a72d 100644
--- a/server/models/CustomMetadataProvider.js
+++ b/server/models/CustomMetadataProvider.js
@@ -30,28 +30,11 @@ class CustomMetadataProvider extends Model {
this.updatedAt
}
- getSlug() {
- return `custom-${this.id}`
- }
-
- /**
- * Safe for clients
- * @returns {ClientCustomMetadataProvider}
- */
- toClientJson() {
- return {
- id: this.id,
- name: this.name,
- mediaType: this.mediaType,
- slug: this.getSlug()
- }
- }
-
/**
* Get providers for client by media type
* Currently only available for "book" media type
- *
- * @param {string} mediaType
+ *
+ * @param {string} mediaType
* @returns {Promise}
*/
static async getForClientByMediaType(mediaType) {
@@ -61,13 +44,13 @@ class CustomMetadataProvider extends Model {
mediaType
}
})
- return customMetadataProviders.map(cmp => cmp.toClientJson())
+ return customMetadataProviders.map((cmp) => cmp.toClientJson())
}
/**
* Check if provider exists by slug
- *
- * @param {string} providerSlug
+ *
+ * @param {string} providerSlug
* @returns {Promise}
*/
static async checkExistsBySlug(providerSlug) {
@@ -79,25 +62,45 @@ class CustomMetadataProvider extends Model {
/**
* Initialize model
- * @param {import('../Database').sequelize} sequelize
+ * @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
- super.init({
- id: {
- type: DataTypes.UUID,
- defaultValue: DataTypes.UUIDV4,
- primaryKey: true
+ super.init(
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: DataTypes.STRING,
+ mediaType: DataTypes.STRING,
+ url: DataTypes.STRING,
+ authHeaderValue: DataTypes.STRING,
+ extraData: DataTypes.JSON
},
- name: DataTypes.STRING,
- mediaType: DataTypes.STRING,
- url: DataTypes.STRING,
- authHeaderValue: DataTypes.STRING,
- extraData: DataTypes.JSON
- }, {
- sequelize,
- modelName: 'customMetadataProvider'
- })
+ {
+ sequelize,
+ modelName: 'customMetadataProvider'
+ }
+ )
+ }
+
+ getSlug() {
+ return `custom-${this.id}`
+ }
+
+ /**
+ * Safe for clients
+ * @returns {ClientCustomMetadataProvider}
+ */
+ toClientJson() {
+ return {
+ id: this.id,
+ name: this.name,
+ mediaType: this.mediaType,
+ slug: this.getSlug()
+ }
}
}
-module.exports = CustomMetadataProvider
\ No newline at end of file
+module.exports = CustomMetadataProvider
diff --git a/server/models/Device.js b/server/models/Device.js
index 896967e4ef..8866d35789 100644
--- a/server/models/Device.js
+++ b/server/models/Device.js
@@ -29,33 +29,6 @@ class Device extends Model {
this.updatedAt
}
- getOldDevice() {
- let browserVersion = null
- let sdkVersion = null
- if (this.clientName === 'Abs Android') {
- sdkVersion = this.deviceVersion || null
- } else {
- browserVersion = this.deviceVersion || null
- }
-
- return new oldDevice({
- id: this.id,
- deviceId: this.deviceId,
- userId: this.userId,
- ipAddress: this.ipAddress,
- browserName: this.extraData.browserName || null,
- browserVersion,
- osName: this.extraData.osName || null,
- osVersion: this.extraData.osVersion || null,
- clientVersion: this.clientVersion || null,
- manufacturer: this.extraData.manufacturer || null,
- model: this.extraData.model || null,
- sdkVersion,
- deviceName: this.deviceName,
- clientName: this.clientName
- })
- }
-
static async getOldDeviceByDeviceId(deviceId) {
const device = await this.findOne({
where: {
@@ -145,6 +118,60 @@ class Device extends Model {
})
Device.belongsTo(user)
}
+
+ toOldJSON() {
+ let browserVersion = null
+ let sdkVersion = null
+ if (this.clientName === 'Abs Android') {
+ sdkVersion = this.deviceVersion || null
+ } else {
+ browserVersion = this.deviceVersion || null
+ }
+
+ return {
+ id: this.id,
+ deviceId: this.deviceId,
+ userId: this.userId,
+ ipAddress: this.ipAddress,
+ browserName: this.extraData.browserName || null,
+ browserVersion,
+ osName: this.extraData.osName || null,
+ osVersion: this.extraData.osVersion || null,
+ clientVersion: this.clientVersion || null,
+ manufacturer: this.extraData.manufacturer || null,
+ model: this.extraData.model || null,
+ sdkVersion,
+ deviceName: this.deviceName,
+ clientName: this.clientName
+ }
+ }
+
+ getOldDevice() {
+ let browserVersion = null
+ let sdkVersion = null
+ if (this.clientName === 'Abs Android') {
+ sdkVersion = this.deviceVersion || null
+ } else {
+ browserVersion = this.deviceVersion || null
+ }
+
+ return new oldDevice({
+ id: this.id,
+ deviceId: this.deviceId,
+ userId: this.userId,
+ ipAddress: this.ipAddress,
+ browserName: this.extraData.browserName || null,
+ browserVersion,
+ osName: this.extraData.osName || null,
+ osVersion: this.extraData.osVersion || null,
+ clientVersion: this.clientVersion || null,
+ manufacturer: this.extraData.manufacturer || null,
+ model: this.extraData.model || null,
+ sdkVersion,
+ deviceName: this.deviceName,
+ clientName: this.clientName
+ })
+ }
}
module.exports = Device
diff --git a/server/models/Library.js b/server/models/Library.js
index 9b42adea8f..972aa264fd 100644
--- a/server/models/Library.js
+++ b/server/models/Library.js
@@ -1,6 +1,5 @@
const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger')
-const oldLibrary = require('../objects/Library')
/**
* @typedef LibrarySettingsObject
@@ -43,6 +42,8 @@ class Library extends Model {
this.createdAt
/** @type {Date} */
this.updatedAt
+ /** @type {import('./LibraryFolder')[]|undefined} */
+ this.libraryFolders
}
/**
@@ -75,111 +76,25 @@ class Library extends Model {
}
/**
- * Get all old libraries
- * @returns {Promise}
+ *
+ * @returns {Promise}
*/
- static async getAllOldLibraries() {
- const libraries = await this.findAll({
+ static getAllWithFolders() {
+ return this.findAll({
include: this.sequelize.models.libraryFolder,
order: [['displayOrder', 'ASC']]
})
- return libraries.map((lib) => this.getOldLibrary(lib))
}
/**
- * Convert expanded Library to oldLibrary
- * @param {Library} libraryExpanded
- * @returns {oldLibrary}
- */
- static getOldLibrary(libraryExpanded) {
- const folders = libraryExpanded.libraryFolders.map((folder) => {
- return {
- id: folder.id,
- fullPath: folder.path,
- libraryId: folder.libraryId,
- addedAt: folder.createdAt.valueOf()
- }
- })
- return new oldLibrary({
- id: libraryExpanded.id,
- oldLibraryId: libraryExpanded.extraData?.oldLibraryId || null,
- name: libraryExpanded.name,
- folders,
- displayOrder: libraryExpanded.displayOrder,
- icon: libraryExpanded.icon,
- mediaType: libraryExpanded.mediaType,
- provider: libraryExpanded.provider,
- settings: libraryExpanded.settings,
- lastScan: libraryExpanded.lastScan?.valueOf() || null,
- lastScanVersion: libraryExpanded.lastScanVersion || null,
- lastScanMetadataPrecedence: libraryExpanded.extraData?.lastScanMetadataPrecedence || null,
- createdAt: libraryExpanded.createdAt.valueOf(),
- lastUpdate: libraryExpanded.updatedAt.valueOf()
- })
- }
-
- /**
- * Update library and library folders
- * @param {object} oldLibrary
- * @returns
+ *
+ * @param {string} libraryId
+ * @returns {Promise}
*/
- static async updateFromOld(oldLibrary) {
- const existingLibrary = await this.findByPk(oldLibrary.id, {
+ static findByIdWithFolders(libraryId) {
+ return this.findByPk(libraryId, {
include: this.sequelize.models.libraryFolder
})
- if (!existingLibrary) {
- Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`)
- return null
- }
-
- const library = this.getFromOld(oldLibrary)
-
- const libraryFolders = oldLibrary.folders.map((folder) => {
- return {
- id: folder.id,
- path: folder.fullPath,
- libraryId: library.id
- }
- })
- for (const libraryFolder of libraryFolders) {
- const existingLibraryFolder = existingLibrary.libraryFolders.find((lf) => lf.id === libraryFolder.id)
- if (!existingLibraryFolder) {
- await this.sequelize.models.libraryFolder.create(libraryFolder)
- } else if (existingLibraryFolder.path !== libraryFolder.path) {
- await existingLibraryFolder.update({ path: libraryFolder.path })
- }
- }
-
- const libraryFoldersRemoved = existingLibrary.libraryFolders.filter((lf) => !libraryFolders.some((_lf) => _lf.id === lf.id))
- for (const existingLibraryFolder of libraryFoldersRemoved) {
- await existingLibraryFolder.destroy()
- }
-
- return existingLibrary.update(library)
- }
-
- static getFromOld(oldLibrary) {
- const extraData = {}
- if (oldLibrary.oldLibraryId) {
- extraData.oldLibraryId = oldLibrary.oldLibraryId
- }
- if (oldLibrary.lastScanMetadataPrecedence) {
- extraData.lastScanMetadataPrecedence = oldLibrary.lastScanMetadataPrecedence
- }
- return {
- id: oldLibrary.id,
- name: oldLibrary.name,
- displayOrder: oldLibrary.displayOrder,
- icon: oldLibrary.icon || null,
- mediaType: oldLibrary.mediaType || null,
- provider: oldLibrary.provider,
- settings: oldLibrary.settings?.toJSON() || {},
- lastScan: oldLibrary.lastScan || null,
- lastScanVersion: oldLibrary.lastScanVersion || null,
- createdAt: oldLibrary.createdAt,
- updatedAt: oldLibrary.lastUpdate,
- extraData
- }
}
/**
@@ -207,20 +122,6 @@ class Library extends Model {
return libraries.map((l) => l.id)
}
- /**
- * Find Library by primary key & return oldLibrary
- * @param {string} libraryId
- * @returns {Promise} Returns null if not found
- */
- static async getOldById(libraryId) {
- if (!libraryId) return null
- const library = await this.findByPk(libraryId, {
- include: this.sequelize.models.libraryFolder
- })
- if (!library) return null
- return this.getOldLibrary(library)
- }
-
/**
* Get the largest value in the displayOrder column
* Used for setting a new libraries display order
@@ -277,6 +178,41 @@ class Library extends Model {
}
)
}
+
+ get isPodcast() {
+ return this.mediaType === 'podcast'
+ }
+ get isBook() {
+ return this.mediaType === 'book'
+ }
+ /**
+ * @returns {string[]}
+ */
+ get lastScanMetadataPrecedence() {
+ return this.extraData?.lastScanMetadataPrecedence || []
+ }
+
+ /**
+ * TODO: Update to use new model
+ */
+ toOldJSON() {
+ return {
+ id: this.id,
+ name: this.name,
+ folders: (this.libraryFolders || []).map((f) => f.toOldJSON()),
+ displayOrder: this.displayOrder,
+ icon: this.icon,
+ mediaType: this.mediaType,
+ provider: this.provider,
+ settings: {
+ ...this.settings
+ },
+ lastScan: this.lastScan?.valueOf() || null,
+ lastScanVersion: this.lastScanVersion,
+ createdAt: this.createdAt.valueOf(),
+ lastUpdate: this.updatedAt.valueOf()
+ }
+ }
}
module.exports = Library
diff --git a/server/models/LibraryFolder.js b/server/models/LibraryFolder.js
index db607547d0..71d532176f 100644
--- a/server/models/LibraryFolder.js
+++ b/server/models/LibraryFolder.js
@@ -42,6 +42,18 @@ class LibraryFolder extends Model {
})
LibraryFolder.belongsTo(library)
}
+
+ /**
+ * TODO: Update to use new model
+ */
+ toOldJSON() {
+ return {
+ id: this.id,
+ fullPath: this.path,
+ libraryId: this.libraryId,
+ addedAt: this.createdAt.valueOf()
+ }
+ }
}
module.exports = LibraryFolder
diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js
index b986ed54bd..dd07747a91 100644
--- a/server/models/LibraryItem.js
+++ b/server/models/LibraryItem.js
@@ -11,8 +11,6 @@ const LibraryFile = require('../objects/files/LibraryFile')
const Book = require('./Book')
const Podcast = require('./Podcast')
-const ShareManager = require('../managers/ShareManager')
-
/**
* @typedef LibraryFileObject
* @property {string} ino
@@ -367,7 +365,23 @@ class LibraryItem extends Model {
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
if (!areEquivalent(updatedMedia[key], existingValue, true)) {
- Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`)
+ if (key === 'chapters') {
+ // Handle logging of chapters separately because the object is large
+ const chaptersRemoved = libraryItemExpanded.media.chapters.filter((ch) => !updatedMedia.chapters.some((uch) => uch.id === ch.id))
+ if (chaptersRemoved.length) {
+ Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" chapters removed: ${chaptersRemoved.map((ch) => ch.title).join(', ')}`)
+ }
+ const chaptersAdded = updatedMedia.chapters.filter((uch) => !libraryItemExpanded.media.chapters.some((ch) => ch.id === uch.id))
+ if (chaptersAdded.length) {
+ Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" chapters added: ${chaptersAdded.map((ch) => ch.title).join(', ')}`)
+ }
+ if (!chaptersRemoved.length && !chaptersAdded.length) {
+ Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" chapters updated`)
+ }
+ } else {
+ Logger.debug(util.format(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from %j to %j`, existingValue, updatedMedia[key]))
+ }
+
hasMediaUpdates = true
}
}
@@ -559,14 +573,14 @@ class LibraryItem extends Model {
/**
* Get library items using filter and sort
- * @param {oldLibrary} library
+ * @param {import('./Library')} library
* @param {import('./User')} user
* @param {object} options
* @returns {{ libraryItems:oldLibraryItem[], count:number }}
*/
static async getByFilterAndSort(library, user, options) {
let start = Date.now()
- const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(library, user, options)
+ const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(library.id, user, options)
Logger.debug(`Loaded ${libraryItems.length} of ${count} items for libary page in ${((Date.now() - start) / 1000).toFixed(2)}s`)
return {
@@ -602,7 +616,7 @@ class LibraryItem extends Model {
/**
* Get home page data personalized shelves
- * @param {oldLibrary} library
+ * @param {import('./Library')} library
* @param {import('./User')} user
* @param {string[]} include
* @param {number} limit
@@ -775,7 +789,7 @@ class LibraryItem extends Model {
/**
* Get book library items for author, optional use user permissions
- * @param {oldAuthor} author
+ * @param {import('./Author')} author
* @param {import('./User')} user
* @returns {Promise}
*/
diff --git a/server/models/Series.js b/server/models/Series.js
index 9f8f1c561a..c57a1a116f 100644
--- a/server/models/Series.js
+++ b/server/models/Series.js
@@ -1,6 +1,6 @@
const { DataTypes, Model, where, fn, col } = require('sequelize')
-const oldSeries = require('../objects/entities/Series')
+const { getTitlePrefixAtEnd } = require('../utils/index')
class Series extends Model {
constructor(values, options) {
@@ -22,70 +22,6 @@ class Series extends Model {
this.updatedAt
}
- static async getAllOldSeries() {
- const series = await this.findAll()
- return series.map((se) => se.getOldSeries())
- }
-
- getOldSeries() {
- return new oldSeries({
- id: this.id,
- name: this.name,
- description: this.description,
- libraryId: this.libraryId,
- addedAt: this.createdAt.valueOf(),
- updatedAt: this.updatedAt.valueOf()
- })
- }
-
- static updateFromOld(oldSeries) {
- const series = this.getFromOld(oldSeries)
- return this.update(series, {
- where: {
- id: series.id
- }
- })
- }
-
- static createFromOld(oldSeries) {
- const series = this.getFromOld(oldSeries)
- return this.create(series)
- }
-
- static createBulkFromOld(oldSeriesObjs) {
- const series = oldSeriesObjs.map(this.getFromOld)
- return this.bulkCreate(series)
- }
-
- static getFromOld(oldSeries) {
- return {
- id: oldSeries.id,
- name: oldSeries.name,
- nameIgnorePrefix: oldSeries.nameIgnorePrefix,
- description: oldSeries.description,
- libraryId: oldSeries.libraryId
- }
- }
-
- static removeById(seriesId) {
- return this.destroy({
- where: {
- id: seriesId
- }
- })
- }
-
- /**
- * Get oldSeries by id
- * @param {string} seriesId
- * @returns {Promise}
- */
- static async getOldById(seriesId) {
- const series = await this.findByPk(seriesId)
- if (!series) return null
- return series.getOldSeries()
- }
-
/**
* Check if series exists
* @param {string} seriesId
@@ -96,24 +32,21 @@ class Series extends Model {
}
/**
- * Get old series by name and libraryId. name case insensitive
+ * Get series by name and libraryId. name case insensitive
*
* @param {string} seriesName
* @param {string} libraryId
- * @returns {Promise}
+ * @returns {Promise}
*/
- static async getOldByNameAndLibrary(seriesName, libraryId) {
- const series = (
- await this.findOne({
- where: [
- where(fn('lower', col('name')), seriesName.toLowerCase()),
- {
- libraryId
- }
- ]
- })
- )?.getOldSeries()
- return series
+ static async getByNameAndLibrary(seriesName, libraryId) {
+ return this.findOne({
+ where: [
+ where(fn('lower', col('name')), seriesName.toLowerCase()),
+ {
+ libraryId
+ }
+ ]
+ })
}
/**
@@ -163,6 +96,26 @@ class Series extends Model {
})
Series.belongsTo(library)
}
+
+ toOldJSON() {
+ return {
+ id: this.id,
+ name: this.name,
+ nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
+ description: this.description,
+ addedAt: this.createdAt.valueOf(),
+ updatedAt: this.updatedAt.valueOf(),
+ libraryId: this.libraryId
+ }
+ }
+
+ toJSONMinimal(sequence) {
+ return {
+ id: this.id,
+ name: this.name,
+ sequence
+ }
+ }
}
module.exports = Series
diff --git a/server/models/User.js b/server/models/User.js
index 123d8fdfa2..2dd02b68c8 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -108,6 +108,7 @@ class User extends Model {
accessAllLibraries: true,
accessAllTags: true,
accessExplicitContent: true,
+ selectedTagsNotAccessible: false,
librariesAccessible: [],
itemTagsSelected: []
}
diff --git a/server/objects/Folder.js b/server/objects/Folder.js
deleted file mode 100644
index 9ca6b21402..0000000000
--- a/server/objects/Folder.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const uuidv4 = require("uuid").v4
-
-class Folder {
- constructor(folder = null) {
- this.id = null
- this.fullPath = null
- this.libraryId = null
- this.addedAt = null
-
- if (folder) {
- this.construct(folder)
- }
- }
-
- construct(folder) {
- this.id = folder.id
- this.fullPath = folder.fullPath
- this.libraryId = folder.libraryId
- this.addedAt = folder.addedAt
- }
-
- toJSON() {
- return {
- id: this.id,
- fullPath: this.fullPath,
- libraryId: this.libraryId,
- addedAt: this.addedAt
- }
- }
-
- setData(data) {
- this.id = data.id || uuidv4()
- this.fullPath = data.fullPath
- this.libraryId = data.libraryId
- this.addedAt = Date.now()
- }
-}
-module.exports = Folder
\ No newline at end of file
diff --git a/server/objects/Library.js b/server/objects/Library.js
deleted file mode 100644
index 98b6ec393b..0000000000
--- a/server/objects/Library.js
+++ /dev/null
@@ -1,158 +0,0 @@
-const uuidv4 = require('uuid').v4
-const Folder = require('./Folder')
-const LibrarySettings = require('./settings/LibrarySettings')
-const { filePathToPOSIX } = require('../utils/fileUtils')
-
-class Library {
- constructor(library = null) {
- this.id = null
- this.oldLibraryId = null // TODO: Temp
- this.name = null
- this.folders = []
- this.displayOrder = 1
- this.icon = 'database'
- this.mediaType = 'book' // book, podcast
- this.provider = 'google'
-
- this.lastScan = 0
- this.lastScanVersion = null
- this.lastScanMetadataPrecedence = null
-
- this.settings = null
-
- this.createdAt = null
- this.lastUpdate = null
-
- if (library) {
- this.construct(library)
- }
- }
-
- get folderPaths() {
- return this.folders.map((f) => f.fullPath)
- }
- get isPodcast() {
- return this.mediaType === 'podcast'
- }
- get isMusic() {
- return this.mediaType === 'music'
- }
- get isBook() {
- return this.mediaType === 'book'
- }
-
- construct(library) {
- this.id = library.id
- this.oldLibraryId = library.oldLibraryId
- this.name = library.name
- this.folders = (library.folders || []).map((f) => new Folder(f))
- this.displayOrder = library.displayOrder || 1
- this.icon = library.icon || 'database'
- this.mediaType = library.mediaType
- this.provider = library.provider || 'google'
-
- this.settings = new LibrarySettings(library.settings)
- if (library.settings === undefined) {
- // LibrarySettings added in v2, migrate settings
- this.settings.disableWatcher = !!library.disableWatcher
- }
-
- this.lastScan = library.lastScan
- this.lastScanVersion = library.lastScanVersion
- this.lastScanMetadataPrecedence = library.lastScanMetadataPrecedence
-
- this.createdAt = library.createdAt
- this.lastUpdate = library.lastUpdate
- this.cleanOldValues() // mediaType changed for v2 and icon change for v2.2.2
- }
-
- cleanOldValues() {
- const availableIcons = ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
- if (!availableIcons.includes(this.icon)) {
- if (this.icon === 'audiobook') this.icon = 'audiobookshelf'
- else if (this.icon === 'book') this.icon = 'books-1'
- else if (this.icon === 'comic') this.icon = 'file-picture'
- else this.icon = 'database'
- }
-
- const mediaTypes = ['podcast', 'book', 'video', 'music']
- if (!this.mediaType || !mediaTypes.includes(this.mediaType)) {
- this.mediaType = 'book'
- }
- }
-
- toJSON() {
- return {
- id: this.id,
- oldLibraryId: this.oldLibraryId,
- name: this.name,
- folders: (this.folders || []).map((f) => f.toJSON()),
- displayOrder: this.displayOrder,
- icon: this.icon,
- mediaType: this.mediaType,
- provider: this.provider,
- settings: this.settings.toJSON(),
- lastScan: this.lastScan,
- lastScanVersion: this.lastScanVersion,
- createdAt: this.createdAt,
- lastUpdate: this.lastUpdate
- }
- }
-
- update(payload) {
- let hasUpdates = false
-
- const keysToCheck = ['name', 'provider', 'mediaType', 'icon']
- keysToCheck.forEach((key) => {
- if (payload[key] && payload[key] !== this[key]) {
- this[key] = payload[key]
- hasUpdates = true
- }
- })
-
- if (payload.settings && this.settings.update(payload.settings)) {
- hasUpdates = true
- }
-
- if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) {
- this.displayOrder = Number(payload.displayOrder)
- hasUpdates = true
- }
- if (payload.folders) {
- const newFolders = payload.folders.filter((f) => !f.id)
- const removedFolders = this.folders.filter((f) => !payload.folders.some((_f) => _f.id === f.id))
-
- if (removedFolders.length) {
- const removedFolderIds = removedFolders.map((f) => f.id)
- this.folders = this.folders.filter((f) => !removedFolderIds.includes(f.id))
- }
-
- if (newFolders.length) {
- newFolders.forEach((folderData) => {
- folderData.libraryId = this.id
- const newFolder = new Folder()
- newFolder.setData(folderData)
- this.folders.push(newFolder)
- })
- }
-
- if (newFolders.length || removedFolders.length) {
- hasUpdates = true
- }
- }
- if (hasUpdates) {
- this.lastUpdate = Date.now()
- }
- return hasUpdates
- }
-
- checkFullPathInLibrary(fullPath) {
- fullPath = filePathToPOSIX(fullPath)
- return this.folders.find((folder) => fullPath.startsWith(filePathToPOSIX(folder.fullPath)))
- }
-
- getFolderById(id) {
- return this.folders.find((folder) => folder.id === id)
- }
-}
-module.exports = Library
diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js
index 3b92bdccf5..0259ee4c92 100644
--- a/server/objects/LibraryItem.js
+++ b/server/objects/LibraryItem.js
@@ -1,12 +1,10 @@
-const uuidv4 = require("uuid").v4
+const uuidv4 = require('uuid').v4
const fs = require('../libs/fsExtra')
const Path = require('path')
const Logger = require('../Logger')
const LibraryFile = require('./files/LibraryFile')
const Book = require('./mediaTypes/Book')
const Podcast = require('./mediaTypes/Podcast')
-const Video = require('./mediaTypes/Video')
-const Music = require('./mediaTypes/Music')
const { areEquivalent, copyValue } = require('../utils/index')
const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
@@ -74,14 +72,10 @@ class LibraryItem {
this.media = new Book(libraryItem.media)
} else if (this.mediaType === 'podcast') {
this.media = new Podcast(libraryItem.media)
- } else if (this.mediaType === 'video') {
- this.media = new Video(libraryItem.media)
- } else if (this.mediaType === 'music') {
- this.media = new Music(libraryItem.media)
}
this.media.libraryItemId = this.id
- this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
+ this.libraryFiles = libraryItem.libraryFiles.map((f) => new LibraryFile(f))
// Migration for v2.2.23 to set ebook library files as supplementary
if (this.isBook && this.media.ebookFile) {
@@ -91,7 +85,6 @@ class LibraryItem {
}
}
}
-
}
toJSON() {
@@ -115,7 +108,7 @@ class LibraryItem {
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSON(),
- libraryFiles: this.libraryFiles.map(f => f.toJSON())
+ libraryFiles: this.libraryFiles.map((f) => f.toJSON())
}
}
@@ -165,21 +158,24 @@ class LibraryItem {
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSONExpanded(),
- libraryFiles: this.libraryFiles.map(f => f.toJSON()),
+ libraryFiles: this.libraryFiles.map((f) => f.toJSON()),
size: this.size
}
}
- get isPodcast() { return this.mediaType === 'podcast' }
- get isBook() { return this.mediaType === 'book' }
- get isMusic() { return this.mediaType === 'music' }
+ get isPodcast() {
+ return this.mediaType === 'podcast'
+ }
+ get isBook() {
+ return this.mediaType === 'book'
+ }
get size() {
let total = 0
- this.libraryFiles.forEach((lf) => total += lf.metadata.size)
+ this.libraryFiles.forEach((lf) => (total += lf.metadata.size))
return total
}
get hasAudioFiles() {
- return this.libraryFiles.some(lf => lf.fileType === 'audio')
+ return this.libraryFiles.some((lf) => lf.fileType === 'audio')
}
get hasMediaEntities() {
return this.media.hasMediaEntities
@@ -201,17 +197,16 @@ class LibraryItem {
for (const key in payload) {
if (key === 'libraryFiles') {
- this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
+ this.libraryFiles = payload.libraryFiles.map((lf) => lf.clone())
// Set cover image
- const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
- const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
+ const imageFiles = this.libraryFiles.filter((lf) => lf.fileType === 'image')
+ const coverMatch = imageFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
if (coverMatch) {
this.media.coverPath = coverMatch.metadata.path
} else if (imageFiles.length) {
this.media.coverPath = imageFiles[0].metadata.path
}
-
} else if (this[key] !== undefined && key !== 'media') {
this[key] = payload[key]
}
@@ -283,46 +278,50 @@ class LibraryItem {
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
- return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => {
- // Add metadata.json to libraryFiles array if it is new
- let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
- if (storeMetadataWithItem) {
- if (!metadataLibraryFile) {
- metadataLibraryFile = new LibraryFile()
- await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
- this.libraryFiles.push(metadataLibraryFile)
- } else {
- const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
- if (fileTimestamps) {
- metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
- metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
- metadataLibraryFile.metadata.size = fileTimestamps.size
- metadataLibraryFile.ino = fileTimestamps.ino
+ return fs
+ .writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2))
+ .then(async () => {
+ // Add metadata.json to libraryFiles array if it is new
+ let metadataLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
+ if (storeMetadataWithItem) {
+ if (!metadataLibraryFile) {
+ metadataLibraryFile = new LibraryFile()
+ await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
+ this.libraryFiles.push(metadataLibraryFile)
+ } else {
+ const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
+ if (fileTimestamps) {
+ metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
+ metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
+ metadataLibraryFile.metadata.size = fileTimestamps.size
+ metadataLibraryFile.ino = fileTimestamps.ino
+ }
+ }
+ const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
+ if (libraryItemDirTimestamps) {
+ this.mtimeMs = libraryItemDirTimestamps.mtimeMs
+ this.ctimeMs = libraryItemDirTimestamps.ctimeMs
}
}
- const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
- if (libraryItemDirTimestamps) {
- this.mtimeMs = libraryItemDirTimestamps.mtimeMs
- this.ctimeMs = libraryItemDirTimestamps.ctimeMs
- }
- }
-
- Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
- return metadataLibraryFile
- }).catch((error) => {
- Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
- return null
- }).finally(() => {
- this.isSavingMetadata = false
- })
+ Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
+
+ return metadataLibraryFile
+ })
+ .catch((error) => {
+ Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
+ return null
+ })
+ .finally(() => {
+ this.isSavingMetadata = false
+ })
}
removeLibraryFile(ino) {
if (!ino) return false
- const libraryFile = this.libraryFiles.find(lf => lf.ino === ino)
+ const libraryFile = this.libraryFiles.find((lf) => lf.ino === ino)
if (libraryFile) {
- this.libraryFiles = this.libraryFiles.filter(lf => lf.ino !== ino)
+ this.libraryFiles = this.libraryFiles.filter((lf) => lf.ino !== ino)
this.updatedAt = Date.now()
return true
}
@@ -333,15 +332,15 @@ class LibraryItem {
* Set the EBookFile from a LibraryFile
* If null then ebookFile will be removed from the book
* all ebook library files that are not primary are marked as supplementary
- *
- * @param {LibraryFile} [libraryFile]
+ *
+ * @param {LibraryFile} [libraryFile]
*/
setPrimaryEbook(ebookLibraryFile = null) {
- const ebookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile)
+ const ebookLibraryFiles = this.libraryFiles.filter((lf) => lf.isEBookFile)
for (const libraryFile of ebookLibraryFiles) {
libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino
}
this.media.setEbookFile(ebookLibraryFile)
}
}
-module.exports = LibraryItem
\ No newline at end of file
+module.exports = LibraryItem
diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js
index 4a5b7d1ef4..6950a54421 100644
--- a/server/objects/PlaybackSession.js
+++ b/server/objects/PlaybackSession.js
@@ -4,7 +4,6 @@ const serverVersion = require('../../package.json').version
const BookMetadata = require('./metadata/BookMetadata')
const PodcastMetadata = require('./metadata/PodcastMetadata')
const DeviceInfo = require('./DeviceInfo')
-const VideoMetadata = require('./metadata/VideoMetadata')
class PlaybackSession {
constructor(session) {
@@ -41,7 +40,6 @@ class PlaybackSession {
// Not saved in DB
this.lastSave = 0
this.audioTracks = []
- this.videoTrack = null
this.stream = null
// Used for share sessions
this.shareSessionId = null
@@ -84,8 +82,8 @@ class PlaybackSession {
/**
* Session data to send to clients
- * @param {[oldLibraryItem]} libraryItem optional
- * @returns {object}
+ * @param {Object} [libraryItem] - old library item
+ * @returns
*/
toJSONForClient(libraryItem) {
return {
@@ -114,7 +112,6 @@ class PlaybackSession {
startedAt: this.startedAt,
updatedAt: this.updatedAt,
audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }),
- videoTrack: this.videoTrack?.toJSON() || null,
libraryItem: libraryItem?.toJSONExpanded() || null
}
}
@@ -157,8 +154,6 @@ class PlaybackSession {
this.mediaMetadata = new BookMetadata(session.mediaMetadata)
} else if (this.mediaType === 'podcast') {
this.mediaMetadata = new PodcastMetadata(session.mediaMetadata)
- } else if (this.mediaType === 'video') {
- this.mediaMetadata = new VideoMetadata(session.mediaMetadata)
}
}
this.displayTitle = session.displayTitle || ''
@@ -224,11 +219,7 @@ class PlaybackSession {
this.displayAuthor = libraryItem.media.getPlaybackAuthor()
this.coverPath = libraryItem.media.coverPath
- if (episodeId) {
- this.duration = libraryItem.media.getEpisodeDuration(episodeId)
- } else {
- this.duration = libraryItem.media.duration
- }
+ this.setDuration(libraryItem, episodeId)
this.mediaPlayer = mediaPlayer
this.deviceInfo = deviceInfo || new DeviceInfo()
@@ -244,6 +235,14 @@ class PlaybackSession {
this.updatedAt = Date.now()
}
+ setDuration(libraryItem, episodeId) {
+ if (episodeId) {
+ this.duration = libraryItem.media.getEpisodeDuration(episodeId)
+ } else {
+ this.duration = libraryItem.media.duration
+ }
+ }
+
addListeningTime(timeListened) {
if (!timeListened || isNaN(timeListened)) return
@@ -256,11 +255,5 @@ class PlaybackSession {
this.timeListening += Number.parseFloat(timeListened)
this.updatedAt = Date.now()
}
-
- // New date since start of listening session
- checkDateRollover() {
- if (!this.date) return false
- return date.format(new Date(), 'YYYY-MM-DD') !== this.date
- }
}
module.exports = PlaybackSession
diff --git a/server/objects/entities/Author.js b/server/objects/entities/Author.js
deleted file mode 100644
index 3d7c0e3c07..0000000000
--- a/server/objects/entities/Author.js
+++ /dev/null
@@ -1,101 +0,0 @@
-const Logger = require('../../Logger')
-const uuidv4 = require("uuid").v4
-const { checkNamesAreEqual, nameToLastFirst } = require('../../utils/parsers/parseNameString')
-
-class Author {
- constructor(author) {
- this.id = null
- this.asin = null
- this.name = null
- this.description = null
- this.imagePath = null
- this.addedAt = null
- this.updatedAt = null
- this.libraryId = null
-
- if (author) {
- this.construct(author)
- }
- }
-
- construct(author) {
- this.id = author.id
- this.asin = author.asin
- this.name = author.name || ''
- this.description = author.description || null
- this.imagePath = author.imagePath
- this.addedAt = author.addedAt
- this.updatedAt = author.updatedAt
- this.libraryId = author.libraryId
- }
-
- get lastFirst() {
- if (!this.name) return ''
- return nameToLastFirst(this.name)
- }
-
- toJSON() {
- return {
- id: this.id,
- asin: this.asin,
- name: this.name,
- description: this.description,
- imagePath: this.imagePath,
- addedAt: this.addedAt,
- updatedAt: this.updatedAt,
- libraryId: this.libraryId
- }
- }
-
- toJSONExpanded(numBooks = 0) {
- const json = this.toJSON()
- json.numBooks = numBooks
- return json
- }
-
- toJSONMinimal() {
- return {
- id: this.id,
- name: this.name
- }
- }
-
- setData(data, libraryId) {
- this.id = uuidv4()
- if (!data.name) {
- Logger.error(`[Author] setData: Setting author data without a name`, data)
- }
- this.name = data.name || ''
- this.description = data.description || null
- this.asin = data.asin || null
- this.imagePath = data.imagePath || null
- this.addedAt = Date.now()
- this.updatedAt = Date.now()
- this.libraryId = libraryId
- }
-
- update(payload) {
- const json = this.toJSON()
- delete json.id
- delete json.addedAt
- delete json.updatedAt
- let hasUpdates = false
- for (const key in json) {
- if (payload[key] !== undefined && json[key] != payload[key]) {
- this[key] = payload[key]
- hasUpdates = true
- }
- }
- return hasUpdates
- }
-
- checkNameEquals(name) {
- if (!name) return false
- if (this.name === null) {
- Logger.error(`[Author] Author name is null (${this.id})`)
- return false
- }
- return checkNamesAreEqual(this.name, name)
- }
-}
-module.exports = Author
\ No newline at end of file
diff --git a/server/objects/entities/Series.js b/server/objects/entities/Series.js
deleted file mode 100644
index 59987f6f4d..0000000000
--- a/server/objects/entities/Series.js
+++ /dev/null
@@ -1,79 +0,0 @@
-const uuidv4 = require("uuid").v4
-const { getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
-
-class Series {
- constructor(series) {
- this.id = null
- this.name = null
- this.description = null
- this.addedAt = null
- this.updatedAt = null
- this.libraryId = null
-
- if (series) {
- this.construct(series)
- }
- }
-
- construct(series) {
- this.id = series.id
- this.name = series.name
- this.description = series.description || null
- this.addedAt = series.addedAt
- this.updatedAt = series.updatedAt
- this.libraryId = series.libraryId
- }
-
- get nameIgnorePrefix() {
- if (!this.name) return ''
- return getTitleIgnorePrefix(this.name)
- }
-
- toJSON() {
- return {
- id: this.id,
- name: this.name,
- nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
- description: this.description,
- addedAt: this.addedAt,
- updatedAt: this.updatedAt,
- libraryId: this.libraryId
- }
- }
-
- toJSONMinimal(sequence) {
- return {
- id: this.id,
- name: this.name,
- sequence
- }
- }
-
- setData(data, libraryId) {
- this.id = uuidv4()
- this.name = data.name
- this.description = data.description || null
- this.addedAt = Date.now()
- this.updatedAt = Date.now()
- this.libraryId = libraryId
- }
-
- update(series) {
- if (!series) return false
- const keysToUpdate = ['name', 'description']
- let hasUpdated = false
- for (const key of keysToUpdate) {
- if (series[key] !== undefined && series[key] !== this[key]) {
- this[key] = series[key]
- hasUpdated = true
- }
- }
- return hasUpdated
- }
-
- checkNameEquals(name) {
- if (!name || !this.name) return false
- return this.name.toLowerCase() == name.toLowerCase().trim()
- }
-}
-module.exports = Series
\ No newline at end of file
diff --git a/server/objects/files/LibraryFile.js b/server/objects/files/LibraryFile.js
index 395e11cc1b..8669e38767 100644
--- a/server/objects/files/LibraryFile.js
+++ b/server/objects/files/LibraryFile.js
@@ -43,14 +43,13 @@ class LibraryFile {
if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image'
if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio'
if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook'
- if (globals.SupportedVideoTypes.includes(this.metadata.format)) return 'video'
if (globals.TextFileTypes.includes(this.metadata.format)) return 'text'
if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata'
return 'unknown'
}
get isMediaFile() {
- return this.fileType === 'audio' || this.fileType === 'ebook' || this.fileType === 'video'
+ return this.fileType === 'audio' || this.fileType === 'ebook'
}
get isEBookFile() {
@@ -75,4 +74,4 @@ class LibraryFile {
this.updatedAt = Date.now()
}
}
-module.exports = LibraryFile
\ No newline at end of file
+module.exports = LibraryFile
diff --git a/server/objects/files/VideoFile.js b/server/objects/files/VideoFile.js
deleted file mode 100644
index 51fc382b6c..0000000000
--- a/server/objects/files/VideoFile.js
+++ /dev/null
@@ -1,109 +0,0 @@
-const { VideoMimeType } = require('../../utils/constants')
-const FileMetadata = require('../metadata/FileMetadata')
-
-class VideoFile {
- constructor(data) {
- this.index = null
- this.ino = null
- this.metadata = null
- this.addedAt = null
- this.updatedAt = null
-
- this.format = null
- this.duration = null
- this.bitRate = null
- this.language = null
- this.codec = null
- this.timeBase = null
- this.frameRate = null
- this.width = null
- this.height = null
- this.embeddedCoverArt = null
-
- this.invalid = false
- this.error = null
-
- if (data) {
- this.construct(data)
- }
- }
-
- toJSON() {
- return {
- index: this.index,
- ino: this.ino,
- metadata: this.metadata.toJSON(),
- addedAt: this.addedAt,
- updatedAt: this.updatedAt,
- invalid: !!this.invalid,
- error: this.error || null,
- format: this.format,
- duration: this.duration,
- bitRate: this.bitRate,
- language: this.language,
- codec: this.codec,
- timeBase: this.timeBase,
- frameRate: this.frameRate,
- width: this.width,
- height: this.height,
- embeddedCoverArt: this.embeddedCoverArt,
- mimeType: this.mimeType
- }
- }
-
- construct(data) {
- this.index = data.index
- this.ino = data.ino
- this.metadata = new FileMetadata(data.metadata || {})
- this.addedAt = data.addedAt
- this.updatedAt = data.updatedAt
- this.invalid = !!data.invalid
- this.error = data.error || null
-
- this.format = data.format
- this.duration = data.duration
- this.bitRate = data.bitRate
- this.language = data.language
- this.codec = data.codec || null
- this.timeBase = data.timeBase
- this.frameRate = data.frameRate
- this.width = data.width
- this.height = data.height
- this.embeddedCoverArt = data.embeddedCoverArt || null
- }
-
- get mimeType() {
- var format = this.metadata.format.toUpperCase()
- if (VideoMimeType[format]) {
- return VideoMimeType[format]
- } else {
- return VideoMimeType.MP4
- }
- }
-
- clone() {
- return new VideoFile(this.toJSON())
- }
-
- setDataFromProbe(libraryFile, probeData) {
- this.ino = libraryFile.ino || null
-
- this.metadata = libraryFile.metadata.clone()
- this.addedAt = Date.now()
- this.updatedAt = Date.now()
-
- const videoStream = probeData.videoStream
-
- this.format = probeData.format
- this.duration = probeData.duration
- this.bitRate = videoStream.bit_rate || probeData.bitRate || null
- this.language = probeData.language
- this.codec = videoStream.codec || null
- this.timeBase = videoStream.time_base
- this.frameRate = videoStream.frame_rate || null
- this.width = videoStream.width || null
- this.height = videoStream.height || null
- this.embeddedCoverArt = probeData.embeddedCoverArt
- }
-}
-module.exports = VideoFile
\ No newline at end of file
diff --git a/server/objects/files/VideoTrack.js b/server/objects/files/VideoTrack.js
deleted file mode 100644
index b1f1e3541e..0000000000
--- a/server/objects/files/VideoTrack.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const Path = require('path')
-const { encodeUriPath } = require('../../utils/fileUtils')
-
-class VideoTrack {
- constructor() {
- this.index = null
- this.duration = null
- this.title = null
- this.contentUrl = null
- this.mimeType = null
- this.codec = null
- this.metadata = null
- }
-
- toJSON() {
- return {
- index: this.index,
- duration: this.duration,
- title: this.title,
- contentUrl: this.contentUrl,
- mimeType: this.mimeType,
- codec: this.codec,
- metadata: this.metadata ? this.metadata.toJSON() : null
- }
- }
-
- setData(itemId, videoFile) {
- this.index = videoFile.index
- this.duration = videoFile.duration
- this.title = videoFile.metadata.filename || ''
- this.contentUrl = Path.join(`${global.RouterBasePath}/api/items/${itemId}/file/${videoFile.ino}`, encodeUriPath(videoFile.metadata.relPath))
- this.mimeType = videoFile.mimeType
- this.codec = videoFile.codec
- this.metadata = videoFile.metadata.clone()
- }
-
- setFromStream(title, duration, contentUrl) {
- this.index = 1
- this.duration = duration
- this.title = title
- this.contentUrl = contentUrl
- this.mimeType = 'application/vnd.apple.mpegurl'
- }
-}
-module.exports = VideoTrack
\ No newline at end of file
diff --git a/server/objects/mediaTypes/Music.js b/server/objects/mediaTypes/Music.js
deleted file mode 100644
index d4b8a518a3..0000000000
--- a/server/objects/mediaTypes/Music.js
+++ /dev/null
@@ -1,145 +0,0 @@
-const Logger = require('../../Logger')
-const AudioFile = require('../files/AudioFile')
-const AudioTrack = require('../files/AudioTrack')
-const MusicMetadata = require('../metadata/MusicMetadata')
-const { areEquivalent, copyValue } = require('../../utils/index')
-const { filePathToPOSIX } = require('../../utils/fileUtils')
-
-class Music {
- constructor(music) {
- this.libraryItemId = null
- this.metadata = null
- this.coverPath = null
- this.tags = []
- this.audioFile = null
-
- if (music) {
- this.construct(music)
- }
- }
-
- construct(music) {
- this.libraryItemId = music.libraryItemId
- this.metadata = new MusicMetadata(music.metadata)
- this.coverPath = music.coverPath
- this.tags = [...music.tags]
- this.audioFile = new AudioFile(music.audioFile)
- }
-
- toJSON() {
- return {
- libraryItemId: this.libraryItemId,
- metadata: this.metadata.toJSON(),
- coverPath: this.coverPath,
- tags: [...this.tags],
- audioFile: this.audioFile.toJSON(),
- }
- }
-
- toJSONMinified() {
- return {
- metadata: this.metadata.toJSONMinified(),
- coverPath: this.coverPath,
- tags: [...this.tags],
- audioFile: this.audioFile.toJSON(),
- duration: this.duration,
- size: this.size
- }
- }
-
- toJSONExpanded() {
- return {
- libraryItemId: this.libraryItemId,
- metadata: this.metadata.toJSONExpanded(),
- coverPath: this.coverPath,
- tags: [...this.tags],
- audioFile: this.audioFile.toJSON(),
- duration: this.duration,
- size: this.size
- }
- }
-
- get size() {
- return this.audioFile.metadata.size
- }
- get hasMediaEntities() {
- return !!this.audioFile
- }
- get duration() {
- return this.audioFile.duration || 0
- }
- get audioTrack() {
- const audioTrack = new AudioTrack()
- audioTrack.setData(this.libraryItemId, this.audioFile, 0)
- return audioTrack
- }
- get numTracks() {
- return 1
- }
-
- update(payload) {
- const json = this.toJSON()
- delete json.episodes // do not update media entities here
- let hasUpdates = false
- for (const key in json) {
- if (payload[key] !== undefined) {
- if (key === 'metadata') {
- if (this.metadata.update(payload.metadata)) {
- hasUpdates = true
- }
- } else if (!areEquivalent(payload[key], json[key])) {
- this[key] = copyValue(payload[key])
- Logger.debug('[Podcast] Key updated', key, this[key])
- hasUpdates = true
- }
- }
- }
- return hasUpdates
- }
-
- updateCover(coverPath) {
- coverPath = filePathToPOSIX(coverPath)
- if (this.coverPath === coverPath) return false
- this.coverPath = coverPath
- return true
- }
-
- removeFileWithInode(inode) {
- return false
- }
-
- findFileWithInode(inode) {
- return (this.audioFile && this.audioFile.ino === inode) ? this.audioFile : null
- }
-
- setData(mediaData) {
- this.metadata = new MusicMetadata()
- if (mediaData.metadata) {
- this.metadata.setData(mediaData.metadata)
- }
-
- this.coverPath = mediaData.coverPath || null
- }
-
- setAudioFile(audioFile) {
- this.audioFile = audioFile
- }
-
- // Only checks container format
- checkCanDirectPlay(payload) {
- return true
- }
-
- getDirectPlayTracklist() {
- return [this.audioTrack]
- }
-
- getPlaybackTitle() {
- return this.metadata.title
- }
-
- getPlaybackAuthor() {
- return this.metadata.artist
- }
-}
-module.exports = Music
\ No newline at end of file
diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js
index bca741a26e..c7d91d0da9 100644
--- a/server/objects/mediaTypes/Podcast.js
+++ b/server/objects/mediaTypes/Podcast.js
@@ -233,15 +233,6 @@ class Podcast {
this.episodes.push(podcastEpisode)
}
- addNewEpisodeFromAudioFile(audioFile, index) {
- const pe = new PodcastEpisode()
- pe.libraryItemId = this.libraryItemId
- pe.podcastId = this.id
- audioFile.index = 1 // Only 1 audio file per episode
- pe.setDataFromAudioFile(audioFile, index)
- this.episodes.push(pe)
- }
-
removeEpisode(episodeId) {
const episode = this.episodes.find((ep) => ep.id === episodeId)
if (episode) {
diff --git a/server/objects/mediaTypes/Video.js b/server/objects/mediaTypes/Video.js
deleted file mode 100644
index 940eab0bbc..0000000000
--- a/server/objects/mediaTypes/Video.js
+++ /dev/null
@@ -1,137 +0,0 @@
-const Logger = require('../../Logger')
-const VideoFile = require('../files/VideoFile')
-const VideoTrack = require('../files/VideoTrack')
-const VideoMetadata = require('../metadata/VideoMetadata')
-const { areEquivalent, copyValue } = require('../../utils/index')
-const { filePathToPOSIX } = require('../../utils/fileUtils')
-
-class Video {
- constructor(video) {
- this.libraryItemId = null
- this.metadata = null
- this.coverPath = null
- this.tags = []
- this.episodes = []
-
- this.autoDownloadEpisodes = false
- this.lastEpisodeCheck = 0
-
- this.lastCoverSearch = null
- this.lastCoverSearchQuery = null
-
- if (video) {
- this.construct(video)
- }
- }
-
- construct(video) {
- this.libraryItemId = video.libraryItemId
- this.metadata = new VideoMetadata(video.metadata)
- this.coverPath = video.coverPath
- this.tags = [...video.tags]
- this.videoFile = new VideoFile(video.videoFile)
- }
-
- toJSON() {
- return {
- libraryItemId: this.libraryItemId,
- metadata: this.metadata.toJSONExpanded(),
- coverPath: this.coverPath,
- tags: [...this.tags],
- videoFile: this.videoFile.toJSON()
- }
- }
-
- toJSONMinified() {
- return {
- metadata: this.metadata.toJSONMinified(),
- coverPath: this.coverPath,
- tags: [...this.tags],
- videoFile: this.videoFile.toJSON(),
- size: this.size
- }
- }
-
- toJSONExpanded() {
- return {
- libraryItemId: this.libraryItemId,
- metadata: this.metadata.toJSONExpanded(),
- coverPath: this.coverPath,
- tags: [...this.tags],
- videoFile: this.videoFile.toJSON(),
- size: this.size
- }
- }
-
- get size() {
- return this.videoFile.metadata.size
- }
- get hasMediaEntities() {
- return true
- }
- get duration() {
- return 0
- }
-
- update(payload) {
- var json = this.toJSON()
- var hasUpdates = false
- for (const key in json) {
- if (payload[key] !== undefined) {
- if (key === 'metadata') {
- if (this.metadata.update(payload.metadata)) {
- hasUpdates = true
- }
- } else if (!areEquivalent(payload[key], json[key])) {
- this[key] = copyValue(payload[key])
- Logger.debug('[Video] Key updated', key, this[key])
- hasUpdates = true
- }
- }
- }
- return hasUpdates
- }
-
- updateCover(coverPath) {
- coverPath = filePathToPOSIX(coverPath)
- if (this.coverPath === coverPath) return false
- this.coverPath = coverPath
- return true
- }
-
- removeFileWithInode(inode) {
-
- }
-
- findFileWithInode(inode) {
- return null
- }
-
- setVideoFile(videoFile) {
- this.videoFile = videoFile
- }
-
- setData(mediaMetadata) {
- this.metadata = new VideoMetadata()
- if (mediaMetadata.metadata) {
- this.metadata.setData(mediaMetadata.metadata)
- }
-
- this.coverPath = mediaMetadata.coverPath || null
- }
-
- getPlaybackTitle() {
- return this.metadata.title
- }
-
- getPlaybackAuthor() {
- return ''
- }
-
- getVideoTrack() {
- var track = new VideoTrack()
- track.setData(this.libraryItemId, this.videoFile)
- return track
- }
-}
-module.exports = Video
\ No newline at end of file
diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js
index 490b994933..6d3dae4326 100644
--- a/server/objects/metadata/BookMetadata.js
+++ b/server/objects/metadata/BookMetadata.js
@@ -6,7 +6,7 @@ class BookMetadata {
this.title = null
this.subtitle = null
this.authors = []
- this.narrators = [] // Array of strings
+ this.narrators = [] // Array of strings
this.series = []
this.genres = [] // Array of strings
this.publishedYear = null
@@ -27,9 +27,9 @@ class BookMetadata {
construct(metadata) {
this.title = metadata.title
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.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.genres = metadata.genres ? [...metadata.genres] : []
this.publishedYear = metadata.publishedYear || null
this.publishedDate = metadata.publishedDate || null
@@ -46,9 +46,9 @@ class BookMetadata {
return {
title: this.title,
subtitle: this.subtitle,
- authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
+ authors: this.authors.map((a) => ({ ...a })), // Author JSONMinimal with name and id
narrators: [...this.narrators],
- series: this.series.map(s => ({ ...s })), // Series JSONMinimal with name, id and sequence
+ series: this.series.map((s) => ({ ...s })), // Series JSONMinimal with name, id and sequence
genres: [...this.genres],
publishedYear: this.publishedYear,
publishedDate: this.publishedDate,
@@ -89,9 +89,9 @@ class BookMetadata {
title: this.title,
titleIgnorePrefix: this.titlePrefixAtEnd,
subtitle: this.subtitle,
- authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
+ authors: this.authors.map((a) => ({ ...a })), // Author JSONMinimal with name and id
narrators: [...this.narrators],
- series: this.series.map(s => ({ ...s })),
+ series: this.series.map((s) => ({ ...s })),
genres: [...this.genres],
publishedYear: this.publishedYear,
publishedDate: this.publishedDate,
@@ -111,8 +111,8 @@ class BookMetadata {
toJSONForMetadataFile() {
const json = this.toJSON()
- json.authors = json.authors.map(au => au.name)
- json.series = json.series.map(se => {
+ json.authors = json.authors.map((au) => au.name)
+ json.series = json.series.map((se) => {
if (!se.sequence) return se.name
return `${se.name} #${se.sequence}`
})
@@ -131,36 +131,31 @@ class BookMetadata {
}
get authorName() {
if (!this.authors.length) return ''
- return this.authors.map(au => au.name).join(', ')
+ return this.authors.map((au) => au.name).join(', ')
}
- get authorNameLF() { // Last, First
+ get authorNameLF() {
+ // Last, First
if (!this.authors.length) return ''
- return this.authors.map(au => parseNameString.nameToLastFirst(au.name)).join(', ')
+ return this.authors.map((au) => parseNameString.nameToLastFirst(au.name)).join(', ')
}
get seriesName() {
if (!this.series.length) return ''
- return this.series.map(se => {
- if (!se.sequence) return se.name
- return `${se.name} #${se.sequence}`
- }).join(', ')
- }
- get firstSeriesName() {
- if (!this.series.length) return ''
- return this.series[0].name
- }
- get firstSeriesSequence() {
- if (!this.series.length) return ''
- return this.series[0].sequence
+ return this.series
+ .map((se) => {
+ if (!se.sequence) return se.name
+ return `${se.name} #${se.sequence}`
+ })
+ .join(', ')
}
get narratorName() {
return this.narrators.join(', ')
}
getSeries(seriesId) {
- return this.series.find(se => se.id == seriesId)
+ return this.series.find((se) => se.id == seriesId)
}
getSeriesSequence(seriesId) {
- const series = this.series.find(se => se.id == seriesId)
+ const series = this.series.find((se) => se.id == seriesId)
if (!series) return null
return series.sequence || ''
}
@@ -180,21 +175,5 @@ class BookMetadata {
}
return hasUpdates
}
-
- // Updates author name
- updateAuthor(updatedAuthor) {
- const author = this.authors.find(au => au.id === updatedAuthor.id)
- if (!author || author.name == updatedAuthor.name) return false
- author.name = updatedAuthor.name
- return true
- }
-
- replaceAuthor(oldAuthor, newAuthor) {
- this.authors = this.authors.filter(au => au.id !== oldAuthor.id) // Remove old author
- this.authors.push({
- id: newAuthor.id,
- name: newAuthor.name
- })
- }
}
module.exports = BookMetadata
diff --git a/server/objects/metadata/MusicMetadata.js b/server/objects/metadata/MusicMetadata.js
deleted file mode 100644
index 90a887e07a..0000000000
--- a/server/objects/metadata/MusicMetadata.js
+++ /dev/null
@@ -1,307 +0,0 @@
-const Logger = require('../../Logger')
-const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
-
-class MusicMetadata {
- constructor(metadata) {
- this.title = null
- this.artists = [] // Array of strings
- this.album = null
- this.albumArtist = null
- this.genres = [] // Array of strings
- this.composer = null
- this.originalYear = null
- this.releaseDate = null
- this.releaseCountry = null
- this.releaseType = null
- this.releaseStatus = null
- this.recordLabel = null
- this.language = null
- this.explicit = false
-
- this.discNumber = null
- this.discTotal = null
- this.trackNumber = null
- this.trackTotal = null
-
- this.isrc = null
- this.musicBrainzTrackId = null
- this.musicBrainzAlbumId = null
- this.musicBrainzAlbumArtistId = null
- this.musicBrainzArtistId = null
-
- if (metadata) {
- this.construct(metadata)
- }
- }
-
- construct(metadata) {
- this.title = metadata.title
- this.artists = metadata.artists ? [...metadata.artists] : []
- this.album = metadata.album
- this.albumArtist = metadata.albumArtist
- this.genres = metadata.genres ? [...metadata.genres] : []
- this.composer = metadata.composer || null
- this.originalYear = metadata.originalYear || null
- this.releaseDate = metadata.releaseDate || null
- this.releaseCountry = metadata.releaseCountry || null
- this.releaseType = metadata.releaseType || null
- this.releaseStatus = metadata.releaseStatus || null
- this.recordLabel = metadata.recordLabel || null
- this.language = metadata.language || null
- this.explicit = !!metadata.explicit
- this.discNumber = metadata.discNumber || null
- this.discTotal = metadata.discTotal || null
- this.trackNumber = metadata.trackNumber || null
- this.trackTotal = metadata.trackTotal || null
- this.isrc = metadata.isrc || null
- this.musicBrainzTrackId = metadata.musicBrainzTrackId || null
- this.musicBrainzAlbumId = metadata.musicBrainzAlbumId || null
- this.musicBrainzAlbumArtistId = metadata.musicBrainzAlbumArtistId || null
- this.musicBrainzArtistId = metadata.musicBrainzArtistId || null
- }
-
- toJSON() {
- return {
- title: this.title,
- artists: [...this.artists],
- album: this.album,
- albumArtist: this.albumArtist,
- genres: [...this.genres],
- composer: this.composer,
- originalYear: this.originalYear,
- releaseDate: this.releaseDate,
- releaseCountry: this.releaseCountry,
- releaseType: this.releaseType,
- releaseStatus: this.releaseStatus,
- recordLabel: this.recordLabel,
- language: this.language,
- explicit: this.explicit,
- discNumber: this.discNumber,
- discTotal: this.discTotal,
- trackNumber: this.trackNumber,
- trackTotal: this.trackTotal,
- isrc: this.isrc,
- musicBrainzTrackId: this.musicBrainzTrackId,
- musicBrainzAlbumId: this.musicBrainzAlbumId,
- musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId,
- musicBrainzArtistId: this.musicBrainzArtistId
- }
- }
-
- toJSONMinified() {
- return {
- title: this.title,
- titleIgnorePrefix: this.titlePrefixAtEnd,
- artists: [...this.artists],
- album: this.album,
- albumArtist: this.albumArtist,
- genres: [...this.genres],
- composer: this.composer,
- originalYear: this.originalYear,
- releaseDate: this.releaseDate,
- releaseCountry: this.releaseCountry,
- releaseType: this.releaseType,
- releaseStatus: this.releaseStatus,
- recordLabel: this.recordLabel,
- language: this.language,
- explicit: this.explicit,
- discNumber: this.discNumber,
- discTotal: this.discTotal,
- trackNumber: this.trackNumber,
- trackTotal: this.trackTotal,
- isrc: this.isrc,
- musicBrainzTrackId: this.musicBrainzTrackId,
- musicBrainzAlbumId: this.musicBrainzAlbumId,
- musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId,
- musicBrainzArtistId: this.musicBrainzArtistId
- }
- }
-
- toJSONExpanded() {
- return this.toJSONMinified()
- }
-
- clone() {
- return new MusicMetadata(this.toJSON())
- }
-
- get titleIgnorePrefix() {
- return getTitleIgnorePrefix(this.title)
- }
-
- get titlePrefixAtEnd() {
- return getTitlePrefixAtEnd(this.title)
- }
-
- setData(mediaMetadata = {}) {
- this.title = mediaMetadata.title || null
- this.artist = mediaMetadata.artist || null
- this.album = mediaMetadata.album || null
- }
-
- update(payload) {
- const json = this.toJSON()
- let hasUpdates = false
- for (const key in json) {
- if (payload[key] !== undefined) {
- if (!areEquivalent(payload[key], json[key])) {
- this[key] = copyValue(payload[key])
- Logger.debug('[MusicMetadata] Key updated', key, this[key])
- hasUpdates = true
- }
- }
- }
- return hasUpdates
- }
-
- parseArtistsTag(artistsTag) {
- if (!artistsTag || !artistsTag.length) return []
- const separators = ['/', '//', ';']
- for (let i = 0; i < separators.length; i++) {
- if (artistsTag.includes(separators[i])) {
- return artistsTag.split(separators[i]).map(artist => artist.trim()).filter(a => !!a)
- }
- }
- return [artistsTag]
- }
-
- parseGenresTag(genreTag) {
- if (!genreTag || !genreTag.length) return []
- const separators = ['/', '//', ';']
- for (let i = 0; i < separators.length; i++) {
- if (genreTag.includes(separators[i])) {
- return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g)
- }
- }
- return [genreTag]
- }
-
- setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) {
- const MetadataMapArray = [
- {
- tag: 'tagTitle',
- key: 'title',
- },
- {
- tag: 'tagArtist',
- key: 'artists'
- },
- {
- tag: 'tagAlbumArtist',
- key: 'albumArtist'
- },
- {
- tag: 'tagAlbum',
- key: 'album',
- },
- {
- tag: 'tagPublisher',
- key: 'recordLabel'
- },
- {
- tag: 'tagComposer',
- key: 'composer'
- },
- {
- tag: 'tagDate',
- key: 'releaseDate'
- },
- {
- tag: 'tagReleaseCountry',
- key: 'releaseCountry'
- },
- {
- tag: 'tagReleaseType',
- key: 'releaseType'
- },
- {
- tag: 'tagReleaseStatus',
- key: 'releaseStatus'
- },
- {
- tag: 'tagOriginalYear',
- key: 'originalYear'
- },
- {
- tag: 'tagGenre',
- key: 'genres'
- },
- {
- tag: 'tagLanguage',
- key: 'language'
- },
- {
- tag: 'tagLanguage',
- key: 'language'
- },
- {
- tag: 'tagISRC',
- key: 'isrc'
- },
- {
- tag: 'tagMusicBrainzTrackId',
- key: 'musicBrainzTrackId'
- },
- {
- tag: 'tagMusicBrainzAlbumId',
- key: 'musicBrainzAlbumId'
- },
- {
- tag: 'tagMusicBrainzAlbumArtistId',
- key: 'musicBrainzAlbumArtistId'
- },
- {
- tag: 'tagMusicBrainzArtistId',
- key: 'musicBrainzArtistId'
- },
- {
- tag: 'trackNumber',
- key: 'trackNumber'
- },
- {
- tag: 'trackTotal',
- key: 'trackTotal'
- },
- {
- tag: 'discNumber',
- key: 'discNumber'
- },
- {
- tag: 'discTotal',
- key: 'discTotal'
- }
- ]
-
- const updatePayload = {}
-
- // Metadata is only mapped to the music track if it is empty
- MetadataMapArray.forEach((mapping) => {
- let value = audioFileMetaTags[mapping.tag]
-
- // let tagToUse = mapping.tag
- if (!value && mapping.altTag) {
- value = audioFileMetaTags[mapping.altTag]
- // tagToUse = mapping.altTag
- }
-
- if (value && (typeof value === 'string' || typeof value === 'number')) {
- value = value.toString().trim() // Trim whitespace
-
- if (mapping.key === 'artists' && (!this.artists.length || overrideExistingDetails)) {
- updatePayload.artists = this.parseArtistsTag(value)
- } else if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) {
- updatePayload.genres = this.parseGenresTag(value)
- } else if (!this[mapping.key] || overrideExistingDetails) {
- updatePayload[mapping.key] = value
- // Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`)
- }
- }
- })
-
- if (Object.keys(updatePayload).length) {
- return this.update(updatePayload)
- }
- return false
- }
-}
-module.exports = MusicMetadata
\ No newline at end of file
diff --git a/server/objects/metadata/VideoMetadata.js b/server/objects/metadata/VideoMetadata.js
deleted file mode 100644
index a2194d15e4..0000000000
--- a/server/objects/metadata/VideoMetadata.js
+++ /dev/null
@@ -1,80 +0,0 @@
-const Logger = require('../../Logger')
-const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
-
-class VideoMetadata {
- constructor(metadata) {
- this.title = null
- this.description = null
- this.explicit = false
- this.language = null
-
- if (metadata) {
- this.construct(metadata)
- }
- }
-
- construct(metadata) {
- this.title = metadata.title
- this.description = metadata.description
- this.explicit = metadata.explicit
- this.language = metadata.language || null
- }
-
- toJSON() {
- return {
- title: this.title,
- description: this.description,
- explicit: this.explicit,
- language: this.language
- }
- }
-
- toJSONMinified() {
- return {
- title: this.title,
- titleIgnorePrefix: this.titlePrefixAtEnd,
- description: this.description,
- explicit: this.explicit,
- language: this.language
- }
- }
-
- toJSONExpanded() {
- return this.toJSONMinified()
- }
-
- clone() {
- return new VideoMetadata(this.toJSON())
- }
-
- get titleIgnorePrefix() {
- return getTitleIgnorePrefix(this.title)
- }
-
- get titlePrefixAtEnd() {
- return getTitlePrefixAtEnd(this.title)
- }
-
- setData(mediaMetadata = {}) {
- this.title = mediaMetadata.title || null
- this.description = mediaMetadata.description || null
- this.explicit = !!mediaMetadata.explicit
- this.language = mediaMetadata.language || null
- }
-
- update(payload) {
- var json = this.toJSON()
- var hasUpdates = false
- for (const key in json) {
- if (payload[key] !== undefined) {
- if (!areEquivalent(payload[key], json[key])) {
- this[key] = copyValue(payload[key])
- Logger.debug('[VideoMetadata] Key updated', key, this[key])
- hasUpdates = true
- }
- }
- }
- return hasUpdates
- }
-}
-module.exports = VideoMetadata
\ No newline at end of file
diff --git a/server/objects/settings/LibrarySettings.js b/server/objects/settings/LibrarySettings.js
deleted file mode 100644
index 4369c0ff58..0000000000
--- a/server/objects/settings/LibrarySettings.js
+++ /dev/null
@@ -1,73 +0,0 @@
-const { BookCoverAspectRatio } = require('../../utils/constants')
-
-class LibrarySettings {
- constructor(settings) {
- this.coverAspectRatio = BookCoverAspectRatio.SQUARE
- this.disableWatcher = false
- this.skipMatchingMediaWithAsin = false
- this.skipMatchingMediaWithIsbn = false
- this.autoScanCronExpression = null
- this.audiobooksOnly = false
- this.epubsAllowScriptedContent = false
- this.hideSingleBookSeries = false // Do not show series that only have 1 book
- this.onlyShowLaterBooksInContinueSeries = false // Skip showing books that are earlier than the max sequence read
- this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
- this.podcastSearchRegion = 'us'
-
- if (settings) {
- this.construct(settings)
- }
- }
-
- construct(settings) {
- this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
- this.disableWatcher = !!settings.disableWatcher
- this.skipMatchingMediaWithAsin = !!settings.skipMatchingMediaWithAsin
- this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
- this.autoScanCronExpression = settings.autoScanCronExpression || null
- this.audiobooksOnly = !!settings.audiobooksOnly
- this.epubsAllowScriptedContent = !!settings.epubsAllowScriptedContent
- this.hideSingleBookSeries = !!settings.hideSingleBookSeries
- this.onlyShowLaterBooksInContinueSeries = !!settings.onlyShowLaterBooksInContinueSeries
- if (settings.metadataPrecedence) {
- this.metadataPrecedence = [...settings.metadataPrecedence]
- } else {
- // Added in v2.4.5
- this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
- }
- this.podcastSearchRegion = settings.podcastSearchRegion || 'us'
- }
-
- toJSON() {
- return {
- coverAspectRatio: this.coverAspectRatio,
- disableWatcher: this.disableWatcher,
- skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
- skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn,
- autoScanCronExpression: this.autoScanCronExpression,
- audiobooksOnly: this.audiobooksOnly,
- epubsAllowScriptedContent: this.epubsAllowScriptedContent,
- hideSingleBookSeries: this.hideSingleBookSeries,
- onlyShowLaterBooksInContinueSeries: this.onlyShowLaterBooksInContinueSeries,
- metadataPrecedence: [...this.metadataPrecedence],
- podcastSearchRegion: this.podcastSearchRegion
- }
- }
-
- update(payload) {
- let hasUpdates = false
- for (const key in payload) {
- if (key === 'metadataPrecedence') {
- if (payload[key] && Array.isArray(payload[key]) && payload[key].join() !== this[key].join()) {
- this[key] = payload[key]
- hasUpdates = true
- }
- } else if (this[key] !== payload[key]) {
- this[key] = payload[key]
- hasUpdates = true
- }
- }
- return hasUpdates
- }
-}
-module.exports = LibrarySettings
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index 54cd97c094..f44fedb475 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -1,5 +1,6 @@
const express = require('express')
const Path = require('path')
+const sequelize = require('sequelize')
const Logger = require('../Logger')
const Database = require('../Database')
@@ -32,8 +33,7 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP
const MiscController = require('../controllers/MiscController')
const ShareController = require('../controllers/ShareController')
-const Author = require('../objects/entities/Author')
-const Series = require('../objects/entities/Series')
+const { getTitleIgnorePrefix } = require('../utils/index')
class ApiRouter {
constructor(Server) {
@@ -53,6 +53,7 @@ class ApiRouter {
this.audioMetadataManager = Server.audioMetadataManager
/** @type {import('../managers/RssFeedManager')} */
this.rssFeedManager = Server.rssFeedManager
+ /** @type {import('../managers/CronManager')} */
this.cronManager = Server.cronManager
/** @type {import('../managers/NotificationManager')} */
this.notificationManager = Server.notificationManager
@@ -468,6 +469,54 @@ class ApiRouter {
}
}
+ /**
+ * Remove authors with no books and unset asin, description and imagePath
+ * Note: Other implementation is in BookScanner.checkAuthorsRemovedFromBooks (can be merged)
+ *
+ * @param {string} libraryId
+ * @param {string[]} authorIds
+ * @returns {Promise}
+ */
+ async checkRemoveAuthorsWithNoBooks(libraryId, authorIds) {
+ if (!authorIds?.length) return
+
+ const bookAuthorsToRemove = (
+ await Database.authorModel.findAll({
+ where: [
+ {
+ id: authorIds,
+ asin: {
+ [sequelize.Op.or]: [null, '']
+ },
+ description: {
+ [sequelize.Op.or]: [null, '']
+ },
+ imagePath: {
+ [sequelize.Op.or]: [null, '']
+ }
+ },
+ sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
+ ],
+ attributes: ['id', 'name'],
+ raw: true
+ })
+ ).map((au) => ({ id: au.id, name: au.name }))
+
+ if (bookAuthorsToRemove.length) {
+ await Database.authorModel.destroy({
+ where: {
+ id: bookAuthorsToRemove.map((au) => au.id)
+ }
+ })
+ bookAuthorsToRemove.forEach(({ id, name }) => {
+ Database.removeAuthorFromFilterData(libraryId, id)
+ // TODO: Clients were expecting full author in payload but its unnecessary
+ SocketAuthority.emitter('author_removed', { id, libraryId })
+ Logger.info(`[ApiRouter] Removed author "${name}" with no books`)
+ })
+ }
+ }
+
/**
* Remove an empty series & close an open RSS feed
* @param {import('../models/Series')} series
@@ -475,13 +524,15 @@ class ApiRouter {
async removeEmptySeries(series) {
await this.rssFeedManager.closeFeedForEntityId(series.id)
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
- await Database.removeSeries(series.id)
+
// Remove series from library filter data
Database.removeSeriesFromFilterData(series.libraryId, series.id)
SocketAuthority.emitter('series_removed', {
id: series.id,
libraryId: series.libraryId
})
+
+ await series.destroy()
}
async getUserListeningSessionsHelper(userId) {
@@ -566,11 +617,14 @@ class ApiRouter {
}
if (!mediaMetadata.authors[i].id) {
- let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId)
+ let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryId)
if (!author) {
- author = new Author()
- author.setData(mediaMetadata.authors[i], libraryId)
- Logger.debug(`[ApiRouter] Created new author "${author.name}"`)
+ author = await Database.authorModel.create({
+ name: authorName,
+ lastFirst: Database.authorModel.getLastFirst(authorName),
+ libraryId
+ })
+ Logger.debug(`[ApiRouter] Creating new author "${author.name}"`)
newAuthors.push(author)
// Update filter data
Database.addAuthorToFilterData(libraryId, author.name, author.id)
@@ -583,10 +637,9 @@ class ApiRouter {
// Remove authors without an id
mediaMetadata.authors = mediaMetadata.authors.filter((au) => !!au.id)
if (newAuthors.length) {
- await Database.createBulkAuthors(newAuthors)
SocketAuthority.emitter(
'authors_added',
- newAuthors.map((au) => au.toJSON())
+ newAuthors.map((au) => au.toOldJSON())
)
}
}
@@ -613,11 +666,14 @@ class ApiRouter {
}
if (!mediaMetadata.series[i].id) {
- let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId)
+ let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesName, libraryId)
if (!seriesItem) {
- seriesItem = new Series()
- seriesItem.setData(mediaMetadata.series[i], libraryId)
- Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`)
+ seriesItem = await Database.seriesModel.create({
+ name: seriesName,
+ nameIgnorePrefix: getTitleIgnorePrefix(seriesName),
+ libraryId
+ })
+ Logger.debug(`[ApiRouter] Creating new series "${seriesItem.name}"`)
newSeries.push(seriesItem)
// Update filter data
Database.addSeriesToFilterData(libraryId, seriesItem.name, seriesItem.id)
@@ -630,10 +686,9 @@ class ApiRouter {
// Remove series without an id
mediaMetadata.series = mediaMetadata.series.filter((se) => se.id)
if (newSeries.length) {
- await Database.createBulkSeries(newSeries)
SocketAuthority.emitter(
'multiple_series_added',
- newSeries.map((se) => se.toJSON())
+ newSeries.map((se) => se.toOldJSON())
)
}
}
diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js
index c12441b264..07f817a8ec 100644
--- a/server/scanner/BookScanner.js
+++ b/server/scanner/BookScanner.js
@@ -1,4 +1,4 @@
-const uuidv4 = require("uuid").v4
+const uuidv4 = require('uuid').v4
const Path = require('path')
const sequelize = require('sequelize')
const { LogLevel } = require('../utils/constants')
@@ -13,14 +13,14 @@ const AudioFile = require('../objects/files/AudioFile')
const CoverManager = require('../managers/CoverManager')
const LibraryFile = require('../objects/files/LibraryFile')
const SocketAuthority = require('../SocketAuthority')
-const fsExtra = require("../libs/fsExtra")
+const fsExtra = require('../libs/fsExtra')
const BookFinder = require('../finders/BookFinder')
-const LibraryScan = require("./LibraryScan")
+const LibraryScan = require('./LibraryScan')
const OpfFileScanner = require('./OpfFileScanner')
const NfoFileScanner = require('./NfoFileScanner')
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
-const EBookFile = require("../objects/files/EBookFile")
+const EBookFile = require('../objects/files/EBookFile')
/**
* Metadata for books pulled from files
@@ -46,13 +46,13 @@ const EBookFile = require("../objects/files/EBookFile")
*/
class BookScanner {
- constructor() { }
+ constructor() {}
/**
- * @param {import('../models/LibraryItem')} existingLibraryItem
- * @param {import('./LibraryItemScanData')} libraryItemData
+ * @param {import('../models/LibraryItem')} existingLibraryItem
+ * @param {import('./LibraryItemScanData')} libraryItemData
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
- * @param {LibraryScan} libraryScan
+ * @param {LibraryScan} libraryScan
* @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>}
*/
async rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
@@ -81,19 +81,23 @@ class BookScanner {
let hasMediaChanges = libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== media.audioFiles.length
if (hasMediaChanges) {
// Filter out audio files that were removed
- media.audioFiles = media.audioFiles.filter(af => !libraryItemData.checkAudioFileRemoved(af))
+ media.audioFiles = media.audioFiles.filter((af) => !libraryItemData.checkAudioFileRemoved(af))
// Update audio files that were modified
if (libraryItemData.audioLibraryFilesModified.length) {
- let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new))
+ let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(
+ existingLibraryItem.mediaType,
+ libraryItemData,
+ libraryItemData.audioLibraryFilesModified.map((lf) => lf.new)
+ )
media.audioFiles = media.audioFiles.map((audioFileObj) => {
- let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path)
+ let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === audioFileObj.metadata.path)
if (!matchedScannedAudioFile) {
- matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === audioFileObj.ino)
+ matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === audioFileObj.ino)
}
if (matchedScannedAudioFile) {
- scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile)
+ scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile)
const audioFile = new AudioFile(audioFileObj)
audioFile.updateFromScan(matchedScannedAudioFile)
return audioFile.toJSON()
@@ -115,7 +119,7 @@ class BookScanner {
// Add audio library files that are not already set on the book (safety check)
let audioLibraryFilesToAdd = []
for (const audioLibraryFile of libraryItemData.audioLibraryFiles) {
- if (!media.audioFiles.some(af => af.ino === audioLibraryFile.ino)) {
+ if (!media.audioFiles.some((af) => af.ino === audioLibraryFile.ino)) {
libraryScan.addLog(LogLevel.DEBUG, `Existing audio library file "${audioLibraryFile.metadata.relPath}" was not set on book "${media.title}" so setting it now`)
audioLibraryFilesToAdd.push(audioLibraryFile)
@@ -139,14 +143,14 @@ class BookScanner {
}
// Check if cover was removed
- if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) {
+ if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some((lf) => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) {
media.coverPath = null
hasMediaChanges = true
}
// Update cover if it was modified
if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) {
- let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath)
+ let coverMatch = libraryItemData.imageLibraryFilesModified.find((iFile) => iFile.old.metadata.path === media.coverPath)
if (coverMatch) {
const coverPath = coverMatch.new.metadata.path
if (coverPath !== media.coverPath) {
@@ -161,7 +165,7 @@ class BookScanner {
// Check if cover is not set and image files were found
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
// Prefer using a cover image with the name "cover" otherwise use the first image
- const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
+ const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
hasMediaChanges = true
}
@@ -174,7 +178,7 @@ class BookScanner {
// Update ebook if it was modified
if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) {
- let ebookMatch = libraryItemData.ebookLibraryFilesModified.find(eFile => eFile.old.metadata.path === media.ebookFile.metadata.path)
+ let ebookMatch = libraryItemData.ebookLibraryFilesModified.find((eFile) => eFile.old.metadata.path === media.ebookFile.metadata.path)
if (ebookMatch) {
const ebookFile = new EBookFile(ebookMatch.new)
ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase()
@@ -188,7 +192,7 @@ class BookScanner {
// Check if ebook is not set and ebooks were found
if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) {
// Prefer to use an epub ebook then fallback to the first ebook found
- let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
+ let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0]
ebookLibraryFile = ebookLibraryFile.toJSON()
// Ebook file is the same as library file except for additional `ebookFormat`
@@ -213,7 +217,7 @@ class BookScanner {
if (key === 'authors') {
// Check for authors added
for (const authorName of bookMetadata.authors) {
- if (!media.authors.some(au => au.name === authorName)) {
+ if (!media.authors.some((au) => au.name === authorName)) {
const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
if (existingAuthorId) {
await Database.bookAuthorModel.create({
@@ -225,7 +229,7 @@ class BookScanner {
} else {
const newAuthor = await Database.authorModel.create({
name: authorName,
- lastFirst: parseNameString.nameToLastFirst(authorName),
+ lastFirst: Database.authorModel.getLastFirst(authorName),
libraryId: libraryItemData.libraryId
})
await media.addAuthor(newAuthor)
@@ -247,7 +251,7 @@ class BookScanner {
} else if (key === 'series') {
// Check for series added
for (const seriesObj of bookMetadata.series) {
- const existingBookSeries = media.series.find(se => se.name === seriesObj.name)
+ const existingBookSeries = media.series.find((se) => se.name === seriesObj.name)
if (!existingBookSeries) {
const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name)
if (existingSeriesId) {
@@ -278,7 +282,7 @@ class BookScanner {
}
// Check for series removed
for (const series of media.series) {
- if (!bookMetadata.series.some(se => se.name === series.name)) {
+ if (!bookMetadata.series.some((se) => se.name === series.name)) {
await series.bookSeries.destroy()
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"`)
seriesUpdated = true
@@ -287,21 +291,21 @@ class BookScanner {
}
} else if (key === 'genres') {
const existingGenres = media.genres || []
- if (bookMetadata.genres.some(g => !existingGenres.includes(g)) || existingGenres.some(g => !bookMetadata.genres.includes(g))) {
+ if (bookMetadata.genres.some((g) => !existingGenres.includes(g)) || existingGenres.some((g) => !bookMetadata.genres.includes(g))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book genres "${existingGenres.join(',')}" => "${bookMetadata.genres.join(',')}" for book "${bookMetadata.title}"`)
media.genres = bookMetadata.genres
hasMediaChanges = true
}
} else if (key === 'tags') {
const existingTags = media.tags || []
- if (bookMetadata.tags.some(t => !existingTags.includes(t)) || existingTags.some(t => !bookMetadata.tags.includes(t))) {
+ if (bookMetadata.tags.some((t) => !existingTags.includes(t)) || existingTags.some((t) => !bookMetadata.tags.includes(t))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book tags "${existingTags.join(',')}" => "${bookMetadata.tags.join(',')}" for book "${bookMetadata.title}"`)
media.tags = bookMetadata.tags
hasMediaChanges = true
}
} else if (key === 'narrators') {
const existingNarrators = media.narrators || []
- if (bookMetadata.narrators.some(t => !existingNarrators.includes(t)) || existingNarrators.some(t => !bookMetadata.narrators.includes(t))) {
+ if (bookMetadata.narrators.some((t) => !existingNarrators.includes(t)) || existingNarrators.some((t) => !bookMetadata.narrators.includes(t))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book narrators "${existingNarrators.join(',')}" => "${bookMetadata.narrators.join(',')}" for book "${bookMetadata.title}"`)
media.narrators = bookMetadata.narrators
hasMediaChanges = true
@@ -333,17 +337,13 @@ class BookScanner {
if (authorsUpdated) {
media.authors = await media.getAuthors({
joinTableAttributes: ['createdAt'],
- order: [
- sequelize.literal(`bookAuthor.createdAt ASC`)
- ]
+ order: [sequelize.literal(`bookAuthor.createdAt ASC`)]
})
}
if (seriesUpdated) {
media.series = await media.getSeries({
joinTableAttributes: ['sequence', 'createdAt'],
- order: [
- sequelize.literal(`bookSeries.createdAt ASC`)
- ]
+ order: [sequelize.literal(`bookSeries.createdAt ASC`)]
})
}
@@ -367,7 +367,10 @@ class BookScanner {
// If no cover then search for cover if enabled in server settings
if (!media.coverPath && Database.serverSettings.scannerFindCovers) {
- const authorName = media.authors.map(au => au.name).filter(au => au).join(', ')
+ const authorName = media.authors
+ .map((au) => au.name)
+ .filter((au) => au)
+ .join(', ')
const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan)
if (coverPath) {
media.coverPath = coverPath
@@ -428,10 +431,10 @@ class BookScanner {
}
/**
- *
- * @param {import('./LibraryItemScanData')} libraryItemData
+ *
+ * @param {import('./LibraryItemScanData')} libraryItemData
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
- * @param {LibraryScan} libraryScan
+ * @param {LibraryScan} libraryScan
* @returns {Promise}
*/
async scanNewBookLibraryItem(libraryItemData, librarySettings, libraryScan) {
@@ -440,7 +443,7 @@ class BookScanner {
scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles)
// Find ebook file (prefer epub)
- let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0]
+ let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0]
// Do not add library items that have no valid audio files and no ebook file
if (!ebookLibraryFile && !scannedAudioFiles.length) {
@@ -460,7 +463,7 @@ class BookScanner {
bookMetadata.abridged = !!bookMetadata.abridged // Ensure boolean
let duration = 0
- scannedAudioFiles.forEach((af) => duration += (!isNaN(af.duration) ? Number(af.duration) : 0))
+ scannedAudioFiles.forEach((af) => (duration += !isNaN(af.duration) ? Number(af.duration) : 0))
const bookObject = {
...bookMetadata,
audioFiles: scannedAudioFiles,
@@ -482,7 +485,7 @@ class BookScanner {
author: {
libraryId: libraryItemData.libraryId,
name: authorName,
- lastFirst: parseNameString.nameToLastFirst(authorName)
+ lastFirst: Database.authorModel.getLastFirst(authorName)
}
})
}
@@ -619,11 +622,11 @@ class BookScanner {
}
/**
- *
- * @param {import('../models/Book').AudioFileObject[]} audioFiles
+ *
+ * @param {import('../models/Book').AudioFileObject[]} audioFiles
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
- * @param {import('./LibraryItemScanData')} libraryItemData
- * @param {LibraryScan} libraryScan
+ * @param {import('./LibraryItemScanData')} libraryItemData
+ * @param {LibraryScan} libraryScan
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
* @param {string} [existingLibraryItemId]
* @returns {Promise}
@@ -664,7 +667,7 @@ class BookScanner {
// Set cover from library file if one is found otherwise check audiofile
if (libraryItemData.imageLibraryFiles.length) {
- const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
+ const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
bookMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
}
@@ -673,16 +676,15 @@ class BookScanner {
return bookMetadata
}
-
static BookMetadataSourceHandler = class {
/**
- *
- * @param {Object} bookMetadata
- * @param {import('../models/Book').AudioFileObject[]} audioFiles
+ *
+ * @param {Object} bookMetadata
+ * @param {import('../models/Book').AudioFileObject[]} audioFiles
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
- * @param {import('./LibraryItemScanData')} libraryItemData
- * @param {LibraryScan} libraryScan
- * @param {string} existingLibraryItemId
+ * @param {import('./LibraryItemScanData')} libraryItemData
+ * @param {LibraryScan} libraryScan
+ * @param {string} existingLibraryItemId
*/
constructor(bookMetadata, audioFiles, ebookFileScanData, libraryItemData, libraryScan, existingLibraryItemId) {
this.bookMetadata = bookMetadata
@@ -785,8 +787,8 @@ class BookScanner {
}
/**
- *
- * @param {import('../models/LibraryItem')} libraryItem
+ *
+ * @param {import('../models/LibraryItem')} libraryItem
* @param {LibraryScan} libraryScan
* @returns {Promise}
*/
@@ -805,12 +807,12 @@ class BookScanner {
const jsonObject = {
tags: libraryItem.media.tags || [],
- chapters: libraryItem.media.chapters?.map(c => ({ ...c })) || [],
+ chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [],
title: libraryItem.media.title,
subtitle: libraryItem.media.subtitle,
- authors: libraryItem.media.authors.map(a => a.name),
+ authors: libraryItem.media.authors.map((a) => a.name),
narrators: libraryItem.media.narrators,
- series: libraryItem.media.series.map(se => {
+ series: libraryItem.media.series.map((se) => {
const sequence = se.bookSeries?.sequence || ''
if (!sequence) return se.name
return `${se.name} #${sequence}`
@@ -826,70 +828,75 @@ class BookScanner {
explicit: !!libraryItem.media.explicit,
abridged: !!libraryItem.media.abridged
}
- return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
- // Add metadata.json to libraryFiles array if it is new
- let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
- if (storeMetadataWithItem) {
- if (!metadataLibraryFile) {
- const newLibraryFile = new LibraryFile()
- await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
- metadataLibraryFile = newLibraryFile.toJSON()
- libraryItem.libraryFiles.push(metadataLibraryFile)
- } else {
- const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
- if (fileTimestamps) {
- metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
- metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
- metadataLibraryFile.metadata.size = fileTimestamps.size
- metadataLibraryFile.ino = fileTimestamps.ino
+ return fsExtra
+ .writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
+ .then(async () => {
+ // Add metadata.json to libraryFiles array if it is new
+ let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
+ if (storeMetadataWithItem) {
+ if (!metadataLibraryFile) {
+ const newLibraryFile = new LibraryFile()
+ await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
+ metadataLibraryFile = newLibraryFile.toJSON()
+ libraryItem.libraryFiles.push(metadataLibraryFile)
+ } else {
+ const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
+ if (fileTimestamps) {
+ metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
+ metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
+ metadataLibraryFile.metadata.size = fileTimestamps.size
+ metadataLibraryFile.ino = fileTimestamps.ino
+ }
+ }
+ const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
+ if (libraryItemDirTimestamps) {
+ libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
+ libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
+ let size = 0
+ libraryItem.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
+ libraryItem.size = size
}
}
- const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
- if (libraryItemDirTimestamps) {
- libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
- libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
- let size = 0
- libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
- libraryItem.size = size
- }
- }
- libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
+ libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
- return metadataLibraryFile
- }).catch((error) => {
- libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
- return null
- })
+ return metadataLibraryFile
+ })
+ .catch((error) => {
+ libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
+ return null
+ })
}
/**
* Check authors that were removed from a book and remove them if they no longer have any books
* keep authors without books that have a asin, description or imagePath
- * @param {string} libraryId
- * @param {import('./ScanLogger')} scanLogger
+ * @param {string} libraryId
+ * @param {import('./ScanLogger')} scanLogger
* @returns {Promise}
*/
async checkAuthorsRemovedFromBooks(libraryId, scanLogger) {
- const bookAuthorsToRemove = (await Database.authorModel.findAll({
- where: [
- {
- id: scanLogger.authorsRemovedFromBooks,
- asin: {
- [sequelize.Op.or]: [null, ""]
- },
- description: {
- [sequelize.Op.or]: [null, ""]
+ const bookAuthorsToRemove = (
+ await Database.authorModel.findAll({
+ where: [
+ {
+ id: scanLogger.authorsRemovedFromBooks,
+ asin: {
+ [sequelize.Op.or]: [null, '']
+ },
+ description: {
+ [sequelize.Op.or]: [null, '']
+ },
+ imagePath: {
+ [sequelize.Op.or]: [null, '']
+ }
},
- imagePath: {
- [sequelize.Op.or]: [null, ""]
- }
- },
- sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
- ],
- attributes: ['id'],
- raw: true
- })).map(au => au.id)
+ sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
+ ],
+ attributes: ['id'],
+ raw: true
+ })
+ ).map((au) => au.id)
if (bookAuthorsToRemove.length) {
await Database.authorModel.destroy({
where: {
@@ -907,21 +914,23 @@ class BookScanner {
/**
* Check series that were removed from books and remove them if they no longer have any books
- * @param {string} libraryId
- * @param {import('./ScanLogger')} scanLogger
+ * @param {string} libraryId
+ * @param {import('./ScanLogger')} scanLogger
* @returns {Promise}
*/
async checkSeriesRemovedFromBooks(libraryId, scanLogger) {
- const bookSeriesToRemove = (await Database.seriesModel.findAll({
- where: [
- {
- id: scanLogger.seriesRemovedFromBooks
- },
- sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
- ],
- attributes: ['id'],
- raw: true
- })).map(se => se.id)
+ const bookSeriesToRemove = (
+ await Database.seriesModel.findAll({
+ where: [
+ {
+ id: scanLogger.seriesRemovedFromBooks
+ },
+ sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
+ ],
+ attributes: ['id'],
+ raw: true
+ })
+ ).map((se) => se.id)
if (bookSeriesToRemove.length) {
await Database.seriesModel.destroy({
where: {
@@ -938,11 +947,11 @@ class BookScanner {
/**
* Search cover provider for matching cover
- * @param {string} libraryItemId
+ * @param {string} libraryItemId
* @param {string} libraryItemPath null if book isFile
- * @param {string} title
- * @param {string} author
- * @param {LibraryScan} libraryScan
+ * @param {string} title
+ * @param {string} author
+ * @param {LibraryScan} libraryScan
* @returns {Promise} path to downloaded cover or null if no cover found
*/
async searchForCover(libraryItemId, libraryItemPath, title, author, libraryScan) {
@@ -956,7 +965,6 @@ class BookScanner {
// If the first cover result fails, attempt to download the second
for (let i = 0; i < results.length && i < 2; i++) {
-
// Downloads and updates the book cover
const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath)
@@ -970,4 +978,4 @@ class BookScanner {
return null
}
}
-module.exports = new BookScanner()
\ No newline at end of file
+module.exports = new BookScanner()
diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js
index ddf3c66b67..5ae5c06ada 100644
--- a/server/scanner/LibraryScan.js
+++ b/server/scanner/LibraryScan.js
@@ -1,10 +1,9 @@
const Path = require('path')
-const uuidv4 = require("uuid").v4
+const uuidv4 = require('uuid').v4
const fs = require('../libs/fsExtra')
const date = require('../libs/dateAndTime')
const Logger = require('../Logger')
-const Library = require('../objects/Library')
const { LogLevel } = require('../utils/constants')
const { secondsToTimestamp, elapsedPretty } = require('../utils/index')
@@ -12,7 +11,7 @@ class LibraryScan {
constructor() {
this.id = null
this.type = null
- /** @type {import('../objects/Library')} */
+ /** @type {import('../models/Library')} */
this.library = null
this.verbose = false
@@ -33,13 +32,21 @@ class LibraryScan {
this.logs = []
}
- get libraryId() { return this.library.id }
- get libraryName() { return this.library.name }
- get libraryMediaType() { return this.library.mediaType }
- get folders() { return this.library.folders }
+ get libraryId() {
+ return this.library.id
+ }
+ get libraryName() {
+ return this.library.name
+ }
+ get libraryMediaType() {
+ return this.library.mediaType
+ }
+ get libraryFolders() {
+ return this.library.libraryFolders
+ }
get timestamp() {
- return (new Date()).toISOString()
+ return new Date().toISOString()
}
get resultStats() {
@@ -92,17 +99,22 @@ class LibraryScan {
}
}
+ /**
+ *
+ * @param {import('../models/Library')} library
+ * @param {string} type
+ */
setData(library, type = 'scan') {
this.id = uuidv4()
this.type = type
- this.library = new Library(library.toJSON()) // clone library
+ this.library = library
this.startedAt = Date.now()
}
/**
- *
- * @param {string} error
+ *
+ * @param {string} error
*/
setComplete(error = null) {
this.finishedAt = Date.now()
@@ -142,7 +154,7 @@ class LibraryScan {
const outputPath = Path.join(scanLogDir, this.logFilename)
const logLines = [JSON.stringify(this.toJSON())]
- this.logs.forEach(l => {
+ this.logs.forEach((l) => {
logLines.push(JSON.stringify(l))
})
await fs.writeFile(outputPath, logLines.join('\n') + '\n')
@@ -150,4 +162,4 @@ class LibraryScan {
Logger.info(`[LibraryScan] Scan log saved "${outputPath}"`)
}
}
-module.exports = LibraryScan
\ No newline at end of file
+module.exports = LibraryScan
diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js
index 6d72e8704c..75d18df099 100644
--- a/server/scanner/LibraryScanner.js
+++ b/server/scanner/LibraryScanner.js
@@ -26,27 +26,27 @@ class LibraryScanner {
}
/**
- * @param {string} libraryId
+ * @param {string} libraryId
* @returns {boolean}
*/
isLibraryScanning(libraryId) {
- return this.librariesScanning.some(ls => ls.id === libraryId)
+ return this.librariesScanning.some((ls) => ls.id === libraryId)
}
/**
- *
- * @param {string} libraryId
+ *
+ * @param {string} libraryId
*/
setCancelLibraryScan(libraryId) {
- const libraryScanning = this.librariesScanning.find(ls => ls.id === libraryId)
+ const libraryScanning = this.librariesScanning.find((ls) => ls.id === libraryId)
if (!libraryScanning) return
this.cancelLibraryScan[libraryId] = true
}
/**
- *
- * @param {import('../objects/Library')} library
- * @param {boolean} [forceRescan]
+ *
+ * @param {import('../models/Library')} library
+ * @param {boolean} [forceRescan]
*/
async scan(library, forceRescan = false) {
if (this.isLibraryScanning(library.id)) {
@@ -54,12 +54,12 @@ class LibraryScanner {
return
}
- if (!library.folders.length) {
+ if (!library.libraryFolders.length) {
Logger.warn(`[LibraryScanner] Library has no folders to scan "${library.name}"`)
return
}
- if (library.isBook && library.settings.metadataPrecedence.join() !== library.lastScanMetadataPrecedence?.join()) {
+ if (library.isBook && library.settings.metadataPrecedence.join() !== library.lastScanMetadataPrecedence.join()) {
const lastScanMetadataPrecedence = library.lastScanMetadataPrecedence?.join() || 'Unset'
Logger.info(`[LibraryScanner] Library metadata precedence changed since last scan. From [${lastScanMetadataPrecedence}] to [${library.settings.metadataPrecedence.join()}]`)
forceRescan = true
@@ -89,7 +89,7 @@ class LibraryScanner {
libraryScan.setComplete()
Logger.info(`[LibraryScanner] Library scan ${libraryScan.id} completed in ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}`)
- this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
+ this.librariesScanning = this.librariesScanning.filter((ls) => ls.id !== library.id)
if (canceled && !libraryScan.totalResults) {
task.setFinished('Scan canceled')
@@ -103,9 +103,12 @@ class LibraryScanner {
library.lastScan = Date.now()
library.lastScanVersion = packageJson.version
if (library.isBook) {
- library.lastScanMetadataPrecedence = library.settings.metadataPrecedence
+ const newExtraData = library.extraData || {}
+ newExtraData.lastScanMetadataPrecedence = library.settings.metadataPrecedence
+ library.extraData = newExtraData
+ library.changed('extraData', true)
}
- await Database.libraryModel.updateFromOld(library)
+ await library.save()
task.setFinished(libraryScan.scanResultsString)
TaskManager.taskFinished(task)
@@ -116,24 +119,24 @@ class LibraryScanner {
}
/**
- *
- * @param {import('./LibraryScan')} libraryScan
+ *
+ * @param {import('./LibraryScan')} libraryScan
* @param {boolean} forceRescan
* @returns {Promise} true if scan canceled
*/
async scanLibrary(libraryScan, forceRescan) {
// Make sure library filter data is set
// this is used to check for existing authors & series
- await libraryFilters.getFilterData(libraryScan.library.mediaType, libraryScan.libraryId)
+ await libraryFilters.getFilterData(libraryScan.libraryMediaType, libraryScan.libraryId)
/** @type {LibraryItemScanData[]} */
let libraryItemDataFound = []
// Scan each library folder
- for (let i = 0; i < libraryScan.folders.length; i++) {
- const folder = libraryScan.folders[i]
+ for (let i = 0; i < libraryScan.libraryFolders.length; i++) {
+ const folder = libraryScan.libraryFolders[i]
const itemDataFoundInFolder = await this.scanFolder(libraryScan.library, folder)
- libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.fullPath}"`)
+ libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.path}"`)
libraryItemDataFound = libraryItemDataFound.concat(itemDataFoundInFolder)
}
@@ -151,14 +154,10 @@ class LibraryScanner {
let oldLibraryItemsUpdated = []
for (const existingLibraryItem of existingLibraryItems) {
// First try to find matching library item with exact file path
- let libraryItemData = libraryItemDataFound.find(lid => lid.path === existingLibraryItem.path)
+ let libraryItemData = libraryItemDataFound.find((lid) => lid.path === existingLibraryItem.path)
if (!libraryItemData) {
// Fallback to finding matching library item with matching inode value
- libraryItemData = libraryItemDataFound.find(lid =>
- ItemToItemInoMatch(lid, existingLibraryItem) ||
- ItemToFileInoMatch(lid, existingLibraryItem) ||
- ItemToFileInoMatch(existingLibraryItem, lid)
- )
+ libraryItemData = libraryItemDataFound.find((lid) => ItemToItemInoMatch(lid, existingLibraryItem) || ItemToFileInoMatch(lid, existingLibraryItem) || ItemToFileInoMatch(existingLibraryItem, lid))
if (libraryItemData) {
libraryScan.addLog(LogLevel.INFO, `Library item with path "${existingLibraryItem.path}" was not found, but library item inode "${existingLibraryItem.ino}" was found at path "${libraryItemData.path}"`)
}
@@ -166,7 +165,7 @@ class LibraryScanner {
if (!libraryItemData) {
// Podcast folder can have no episodes and still be valid
- if (libraryScan.libraryMediaType === 'podcast' && await fs.pathExists(existingLibraryItem.path)) {
+ if (libraryScan.libraryMediaType === 'podcast' && (await fs.pathExists(existingLibraryItem.path))) {
libraryScan.addLog(LogLevel.INFO, `Library item "${existingLibraryItem.relPath}" folder exists but has no episodes`)
} else {
libraryScan.addLog(LogLevel.WARN, `Library Item "${existingLibraryItem.path}" (inode: ${existingLibraryItem.ino}) is missing`)
@@ -184,7 +183,7 @@ class LibraryScanner {
}
}
} else {
- libraryItemDataFound = libraryItemDataFound.filter(lidf => lidf !== libraryItemData)
+ libraryItemDataFound = libraryItemDataFound.filter((lidf) => lidf !== libraryItemData)
let libraryItemDataUpdated = await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan)
if (libraryItemDataUpdated || forceRescan) {
if (forceRescan || libraryItemData.hasLibraryFileChanges || libraryItemData.hasPathChange) {
@@ -210,7 +209,10 @@ class LibraryScanner {
// Emit item updates in chunks of 10 to client
if (oldLibraryItemsUpdated.length === 10) {
// TODO: Should only emit to clients where library item is accessible
- SocketAuthority.emitter('items_updated', oldLibraryItemsUpdated.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter(
+ 'items_updated',
+ oldLibraryItemsUpdated.map((li) => li.toJSONExpanded())
+ )
oldLibraryItemsUpdated = []
}
@@ -219,7 +221,10 @@ class LibraryScanner {
// Emit item updates to client
if (oldLibraryItemsUpdated.length) {
// TODO: Should only emit to clients where library item is accessible
- SocketAuthority.emitter('items_updated', oldLibraryItemsUpdated.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter(
+ 'items_updated',
+ oldLibraryItemsUpdated.map((li) => li.toJSONExpanded())
+ )
}
// Authors and series that were removed from books should be removed if they are now empty
@@ -228,15 +233,18 @@ class LibraryScanner {
// Update missing library items
if (libraryItemIdsMissing.length) {
libraryScan.addLog(LogLevel.INFO, `Updating ${libraryItemIdsMissing.length} library items missing`)
- await Database.libraryItemModel.update({
- isMissing: true,
- lastScan: Date.now(),
- lastScanVersion: packageJson.version
- }, {
- where: {
- id: libraryItemIdsMissing
+ await Database.libraryItemModel.update(
+ {
+ isMissing: true,
+ lastScan: Date.now(),
+ lastScanVersion: packageJson.version
+ },
+ {
+ where: {
+ id: libraryItemIdsMissing
+ }
}
- })
+ )
}
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
@@ -256,7 +264,10 @@ class LibraryScanner {
// Emit new items in chunks of 10 to client
if (newOldLibraryItems.length === 10) {
// TODO: Should only emit to clients where library item is accessible
- SocketAuthority.emitter('items_added', newOldLibraryItems.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter(
+ 'items_added',
+ newOldLibraryItems.map((li) => li.toJSONExpanded())
+ )
newOldLibraryItems = []
}
@@ -265,19 +276,22 @@ class LibraryScanner {
// Emit new items to client
if (newOldLibraryItems.length) {
// TODO: Should only emit to clients where library item is accessible
- SocketAuthority.emitter('items_added', newOldLibraryItems.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter(
+ 'items_added',
+ newOldLibraryItems.map((li) => li.toJSONExpanded())
+ )
}
}
}
/**
* Get scan data for library folder
- * @param {import('../objects/Library')} library
- * @param {import('../objects/Folder')} folder
+ * @param {import('../models/Library')} library
+ * @param {import('../models/LibraryFolder')} folder
* @returns {LibraryItemScanData[]}
*/
async scanFolder(library, folder) {
- const folderPath = fileUtils.filePathToPOSIX(folder.fullPath)
+ const folderPath = fileUtils.filePathToPOSIX(folder.path)
const pathExists = await fs.pathExists(folderPath)
if (!pathExists) {
@@ -321,27 +335,29 @@ class LibraryScanner {
continue
}
- items.push(new LibraryItemScanData({
- libraryFolderId: folder.id,
- libraryId: folder.libraryId,
- mediaType: library.mediaType,
- ino: libraryItemFolderStats.ino,
- mtimeMs: libraryItemFolderStats.mtimeMs || 0,
- ctimeMs: libraryItemFolderStats.ctimeMs || 0,
- birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
- path: libraryItemData.path,
- relPath: libraryItemData.relPath,
- isFile,
- mediaMetadata: libraryItemData.mediaMetadata || null,
- libraryFiles: fileObjs
- }))
+ items.push(
+ new LibraryItemScanData({
+ libraryFolderId: folder.id,
+ libraryId: folder.libraryId,
+ mediaType: library.mediaType,
+ ino: libraryItemFolderStats.ino,
+ mtimeMs: libraryItemFolderStats.mtimeMs || 0,
+ ctimeMs: libraryItemFolderStats.ctimeMs || 0,
+ birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
+ path: libraryItemData.path,
+ relPath: libraryItemData.relPath,
+ isFile,
+ mediaMetadata: libraryItemData.mediaMetadata || null,
+ libraryFiles: fileObjs
+ })
+ )
}
return items
}
/**
* Scan files changed from Watcher
- * @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
+ * @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
* @param {Task} pendingTask
*/
async scanFilesChanged(fileUpdates, pendingTask) {
@@ -366,7 +382,7 @@ class LibraryScanner {
for (const folderId in folderGroups) {
const libraryId = folderGroups[folderId].libraryId
- // const library = await Database.libraryModel.getOldById(libraryId)
+
const library = await Database.libraryModel.findByPk(libraryId, {
include: {
model: Database.libraryFolderModel,
@@ -381,7 +397,7 @@ class LibraryScanner {
}
const folder = library.libraryFolders[0]
- const relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
+ const relFilePaths = folderGroups[folderId].fileUpdates.map((fileUpdate) => fileUpdate.relPath)
const fileUpdateGroup = scanUtils.groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths)
if (!Object.keys(fileUpdateGroup).length) {
@@ -432,7 +448,7 @@ class LibraryScanner {
/**
* Group array of PendingFileUpdate from Watcher by folder
- * @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
+ * @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
* @returns {Record}
*/
getFileUpdatesGrouped(fileUpdates) {
@@ -453,9 +469,9 @@ class LibraryScanner {
/**
* Scan grouped paths for library folder coming from Watcher
- * @param {import('../models/Library')} library
- * @param {import('../models/LibraryFolder')} folder
- * @param {Record} fileUpdateGroup
+ * @param {import('../models/Library')} library
+ * @param {import('../models/LibraryFolder')} folder
+ * @param {Record} fileUpdateGroup
* @returns {Promise>}
*/
async scanFolderUpdates(library, folder, fileUpdateGroup) {
@@ -471,7 +487,7 @@ class LibraryScanner {
for (const itemDir in updateGroup) {
if (isSingleMediaFile(fileUpdateGroup, itemDir)) continue // Media in root path
- const itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/'))
+ const itemDirNestedFiles = fileUpdateGroup[itemDir].filter((b) => b.includes('/'))
if (!itemDirNestedFiles.length) continue
const firstNest = itemDirNestedFiles[0].split('/').shift()
@@ -523,7 +539,15 @@ class LibraryScanner {
const potentialChildDirs = [fullPath]
for (let i = 0; i < itemDirParts.length; i++) {
- potentialChildDirs.push(Path.posix.join(fileUtils.filePathToPOSIX(folder.path), itemDir.split('/').slice(0, -1 - i).join('/')))
+ potentialChildDirs.push(
+ Path.posix.join(
+ fileUtils.filePathToPOSIX(folder.path),
+ itemDir
+ .split('/')
+ .slice(0, -1 - i)
+ .join('/')
+ )
+ )
}
// Check if book dir group is already an item
@@ -535,10 +559,7 @@ class LibraryScanner {
let updatedLibraryItemDetails = {}
if (!existingLibraryItem) {
const isSingleMedia = isSingleMediaFile(fileUpdateGroup, itemDir)
- existingLibraryItem =
- await findLibraryItemByItemToItemInoMatch(library.id, fullPath) ||
- await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia) ||
- await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir])
+ existingLibraryItem = (await findLibraryItemByItemToItemInoMatch(library.id, fullPath)) || (await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia)) || (await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir]))
if (existingLibraryItem) {
// Update library item paths for scan
existingLibraryItem.path = fullPath
@@ -603,7 +624,7 @@ class LibraryScanner {
module.exports = new LibraryScanner()
function ItemToFileInoMatch(libraryItem1, libraryItem2) {
- return libraryItem1.isFile && libraryItem2.libraryFiles.some(lf => lf.ino === libraryItem1.ino)
+ return libraryItem1.isFile && libraryItem2.libraryFiles.some((lf) => lf.ino === libraryItem1.ino)
}
function ItemToItemInoMatch(libraryItem1, libraryItem2) {
@@ -611,9 +632,7 @@ function ItemToItemInoMatch(libraryItem1, libraryItem2) {
}
function hasAudioFiles(fileUpdateGroup, itemDir) {
- return isSingleMediaFile(fileUpdateGroup, itemDir) ?
- scanUtils.checkFilepathIsAudioFile(fileUpdateGroup[itemDir]) :
- fileUpdateGroup[itemDir].some(scanUtils.checkFilepathIsAudioFile)
+ return isSingleMediaFile(fileUpdateGroup, itemDir) ? scanUtils.checkFilepathIsAudioFile(fileUpdateGroup[itemDir]) : fileUpdateGroup[itemDir].some(scanUtils.checkFilepathIsAudioFile)
}
function isSingleMediaFile(fileUpdateGroup, itemDir) {
@@ -627,8 +646,7 @@ async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) {
libraryId: libraryId,
ino: ino
})
- if (existingLibraryItem)
- Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`)
+ if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
@@ -637,18 +655,20 @@ async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingle
// check if it was moved from another folder by comparing the ino to the library files
const ino = await fileUtils.getIno(fullPath)
if (!ino) return null
- const existingLibraryItem = await Database.libraryItemModel.findOneOld([
+ const existingLibraryItem = await Database.libraryItemModel.findOneOld(
+ [
+ {
+ libraryId: libraryId
+ },
+ sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode)'), {
+ [sequelize.Op.gt]: 0
+ })
+ ],
{
- libraryId: libraryId
- },
- sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode)'), {
- [sequelize.Op.gt]: 0
- })
- ], {
- inode: ino
- })
- if (existingLibraryItem)
- Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`)
+ inode: ino
+ }
+ )
+ if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
@@ -667,7 +687,6 @@ async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingle
[sequelize.Op.in]: itemFileInos
}
})
- if (existingLibraryItem)
- Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`)
+ if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
-}
\ No newline at end of file
+}
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index e0bcf4fd69..06657de228 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -1,6 +1,7 @@
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
+const { getTitleIgnorePrefix } = require('../utils/index')
// Utils
const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcastUtils')
@@ -8,14 +9,12 @@ const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcast
const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
const LibraryScan = require('./LibraryScan')
-const Author = require('../objects/entities/Author')
-const Series = require('../objects/entities/Series')
const LibraryScanner = require('./LibraryScanner')
const CoverManager = require('../managers/CoverManager')
const TaskManager = require('../managers/TaskManager')
class Scanner {
- constructor() { }
+ constructor() {}
async quickMatchLibraryItem(libraryItem, options = {}) {
var provider = options.provider || 'google'
@@ -23,9 +22,9 @@ class Scanner {
var searchAuthor = options.author || libraryItem.media.metadata.authorName
var overrideDefaults = options.overrideDefaults || false
- // Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
+ // Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
// the overrideDefaults option is not set or set to false.
- if ((overrideDefaults == false) && (Database.serverSettings.scannerPreferMatchedMetadata)) {
+ if (overrideDefaults == false && Database.serverSettings.scannerPreferMatchedMetadata) {
options.overrideCover = true
options.overrideDetails = true
}
@@ -57,7 +56,8 @@ class Scanner {
}
updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options)
- } else if (libraryItem.isPodcast) { // Podcast quick match
+ } else if (libraryItem.isPodcast) {
+ // Podcast quick match
var results = await PodcastFinder.search(searchTitle)
if (!results.length) {
return {
@@ -88,7 +88,8 @@ class Scanner {
}
if (hasUpdated) {
- if (libraryItem.isPodcast && libraryItem.media.metadata.feedUrl) { // Quick match all unmatched podcast episodes
+ if (libraryItem.isPodcast && libraryItem.media.metadata.feedUrl) {
+ // Quick match all unmatched podcast episodes
await this.quickMatchPodcastEpisodes(libraryItem, options)
}
@@ -122,12 +123,16 @@ class Scanner {
for (const key in matchDataTransformed) {
if (matchDataTransformed[key]) {
if (key === 'genres') {
- if ((!libraryItem.media.metadata.genres.length || options.overrideDetails)) {
+ if (!libraryItem.media.metadata.genres.length || options.overrideDetails) {
var genresArray = []
if (Array.isArray(matchDataTransformed[key])) genresArray = [...matchDataTransformed[key]]
- else { // Genres should always be passed in as an array but just incase handle a string
+ else {
+ // Genres should always be passed in as an array but just incase handle a string
Logger.warn(`[Scanner] quickMatch genres is not an array ${matchDataTransformed[key]}`)
- genresArray = matchDataTransformed[key].split(',').map(v => v.trim()).filter(v => !!v)
+ genresArray = matchDataTransformed[key]
+ .split(',')
+ .map((v) => v.trim())
+ .filter((v) => !!v)
}
updatePayload.metadata[key] = genresArray
}
@@ -153,27 +158,38 @@ class Scanner {
for (const key in matchData) {
if (matchData[key] && detailKeysToUpdate.includes(key)) {
if (key === 'narrator') {
- if ((!libraryItem.media.metadata.narratorName || options.overrideDetails)) {
- updatePayload.metadata.narrators = matchData[key].split(',').map(v => v.trim()).filter(v => !!v)
+ if (!libraryItem.media.metadata.narratorName || options.overrideDetails) {
+ updatePayload.metadata.narrators = matchData[key]
+ .split(',')
+ .map((v) => v.trim())
+ .filter((v) => !!v)
}
} else if (key === 'genres') {
- if ((!libraryItem.media.metadata.genres.length || options.overrideDetails)) {
+ if (!libraryItem.media.metadata.genres.length || options.overrideDetails) {
var genresArray = []
if (Array.isArray(matchData[key])) genresArray = [...matchData[key]]
- else { // Genres should always be passed in as an array but just incase handle a string
+ else {
+ // Genres should always be passed in as an array but just incase handle a string
Logger.warn(`[Scanner] quickMatch genres is not an array ${matchData[key]}`)
- genresArray = matchData[key].split(',').map(v => v.trim()).filter(v => !!v)
+ genresArray = matchData[key]
+ .split(',')
+ .map((v) => v.trim())
+ .filter((v) => !!v)
}
updatePayload.metadata[key] = genresArray
}
} else if (key === 'tags') {
- if ((!libraryItem.media.tags.length || options.overrideDetails)) {
+ if (!libraryItem.media.tags.length || options.overrideDetails) {
var tagsArray = []
if (Array.isArray(matchData[key])) tagsArray = [...matchData[key]]
- else tagsArray = matchData[key].split(',').map(v => v.trim()).filter(v => !!v)
+ else
+ tagsArray = matchData[key]
+ .split(',')
+ .map((v) => v.trim())
+ .filter((v) => !!v)
updatePayload[key] = tagsArray
}
- } else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
+ } else if (!libraryItem.media.metadata[key] || options.overrideDetails) {
updatePayload.metadata[key] = matchData[key]
}
}
@@ -182,16 +198,21 @@ class Scanner {
// Add or set author if not set
if (matchData.author && (!libraryItem.media.metadata.authorName || options.overrideDetails)) {
if (!Array.isArray(matchData.author)) {
- matchData.author = matchData.author.split(',').map(au => au.trim()).filter(au => !!au)
+ matchData.author = matchData.author
+ .split(',')
+ .map((au) => au.trim())
+ .filter((au) => !!au)
}
const authorPayload = []
for (const authorName of matchData.author) {
- let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryItem.libraryId)
+ let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryItem.libraryId)
if (!author) {
- author = new Author()
- author.setData({ name: authorName }, libraryItem.libraryId)
- await Database.createAuthor(author)
- SocketAuthority.emitter('author_added', author.toJSON())
+ author = await Database.authorModel.create({
+ name: authorName,
+ lastFirst: Database.authorModel.getLastFirst(authorName),
+ libraryId: libraryItem.libraryId
+ })
+ SocketAuthority.emitter('author_added', author.toOldJSON())
// Update filter data
Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id)
}
@@ -205,14 +226,16 @@ class Scanner {
if (!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, sequence: matchData.sequence }]
const seriesPayload = []
for (const seriesMatchItem of matchData.series) {
- let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId)
+ let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId)
if (!seriesItem) {
- seriesItem = new Series()
- seriesItem.setData({ name: seriesMatchItem.series }, libraryItem.libraryId)
- await Database.createSeries(seriesItem)
+ seriesItem = await Database.seriesModel.create({
+ name: seriesMatchItem.series,
+ nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series),
+ libraryId: libraryItem.libraryId
+ })
// Update filter data
Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id)
- SocketAuthority.emitter('series_added', seriesItem.toJSON())
+ SocketAuthority.emitter('series_added', seriesItem.toOldJSON())
}
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))
}
@@ -227,7 +250,7 @@ class Scanner {
}
async quickMatchPodcastEpisodes(libraryItem, options = {}) {
- const episodesToQuickMatch = libraryItem.media.episodes.filter(ep => !ep.enclosureUrl) // Only quick match episodes without enclosure
+ const episodesToQuickMatch = libraryItem.media.episodes.filter((ep) => !ep.enclosureUrl) // Only quick match episodes without enclosure
if (!episodesToQuickMatch.length) return false
const feed = await getPodcastFeed(libraryItem.media.metadata.feedUrl)
@@ -283,10 +306,10 @@ class Scanner {
/**
* Quick match library items
- *
- * @param {import('../objects/Library')} library
- * @param {import('../objects/LibraryItem')[]} libraryItems
- * @param {LibraryScan} libraryScan
+ *
+ * @param {import('../models/Library')} library
+ * @param {import('../objects/LibraryItem')[]} libraryItems
+ * @param {LibraryScan} libraryScan
* @returns {Promise} false if scan canceled
*/
async matchLibraryItemsChunk(library, libraryItems, libraryScan) {
@@ -294,14 +317,12 @@ class Scanner {
const libraryItem = libraryItems[i]
if (libraryItem.media.metadata.asin && library.settings.skipMatchingMediaWithAsin) {
- Logger.debug(`[Scanner] matchLibraryItems: Skipping "${libraryItem.media.metadata.title
- }" because it already has an ASIN (${i + 1} of ${libraryItems.length})`)
+ Logger.debug(`[Scanner] matchLibraryItems: Skipping "${libraryItem.media.metadata.title}" because it already has an ASIN (${i + 1} of ${libraryItems.length})`)
continue
}
if (libraryItem.media.metadata.isbn && library.settings.skipMatchingMediaWithIsbn) {
- Logger.debug(`[Scanner] matchLibraryItems: Skipping "${libraryItem.media.metadata.title
- }" because it already has an ISBN (${i + 1} of ${libraryItems.length})`)
+ Logger.debug(`[Scanner] matchLibraryItems: Skipping "${libraryItem.media.metadata.title}" because it already has an ISBN (${i + 1} of ${libraryItems.length})`)
continue
}
@@ -324,8 +345,8 @@ class Scanner {
/**
* Quick match all library items for library
- *
- * @param {import('../objects/Library')} library
+ *
+ * @param {import('../models/Library')} library
*/
async matchLibraryItems(library) {
if (library.mediaType === 'podcast') {
@@ -360,7 +381,7 @@ class Scanner {
offset += limit
hasMoreChunks = libraryItems.length === limit
- let oldLibraryItems = libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li))
+ let oldLibraryItems = libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan)
if (!shouldContinue) {
@@ -379,7 +400,7 @@ class Scanner {
}
delete LibraryScanner.cancelLibraryScan[libraryScan.libraryId]
- LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter(ls => ls.id !== library.id)
+ LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter((ls) => ls.id !== library.id)
TaskManager.taskFinished(task)
}
}
diff --git a/server/utils/constants.js b/server/utils/constants.js
index 7a21d2ddcc..cbfe65f207 100644
--- a/server/utils/constants.js
+++ b/server/utils/constants.js
@@ -51,7 +51,3 @@ module.exports.AudioMimeType = {
AWB: 'audio/amr-wb',
CAF: 'audio/x-caf'
}
-
-module.exports.VideoMimeType = {
- MP4: 'video/mp4'
-}
\ No newline at end of file
diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js
index e0f5c7da8f..3fa9f63cd6 100644
--- a/server/utils/ffmpegHelpers.js
+++ b/server/utils/ffmpegHelpers.js
@@ -299,6 +299,12 @@ async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataF
'-metadata:s:v',
'comment=Cover' // add comment metadata to cover image stream
])
+ const ext = Path.extname(coverFilePath).toLowerCase()
+ if (ext === '.webp') {
+ ffmpeg.outputOptions([
+ '-c:v mjpeg' // convert webp images to jpeg
+ ])
+ }
} else {
ffmpeg.outputOptions([
'-map 0:v?' // retain video stream from input file if exists
diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js
index e4bb53a009..b0c73d6c6e 100644
--- a/server/utils/fileUtils.js
+++ b/server/utils/fileUtils.js
@@ -131,19 +131,6 @@ async function readTextFile(path) {
}
module.exports.readTextFile = readTextFile
-function bytesPretty(bytes, decimals = 0) {
- if (bytes === 0) {
- return '0 Bytes'
- }
- const k = 1000
- var dm = decimals < 0 ? 0 : decimals
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- if (i > 2 && dm === 0) dm = 1
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
-}
-module.exports.bytesPretty = bytesPretty
-
/**
* Get array of files inside dir
* @param {string} path
diff --git a/server/utils/globals.js b/server/utils/globals.js
index b24fc76d04..877cf07a08 100644
--- a/server/utils/globals.js
+++ b/server/utils/globals.js
@@ -2,7 +2,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'],
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
- SupportedVideoTypes: ['mp4'],
TextFileTypes: ['txt', 'nfo'],
MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
}
diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js
index ad71ee3fad..664bd6e301 100644
--- a/server/utils/libraryHelpers.js
+++ b/server/utils/libraryHelpers.js
@@ -83,7 +83,7 @@ module.exports = {
* @param {*} payload
* @param {string} seriesId
* @param {import('../models/User')} user
- * @param {import('../objects/Library')} library
+ * @param {import('../models/Library')} library
* @returns {Object[]}
*/
async handleCollapseSubseries(payload, seriesId, user, library) {
diff --git a/server/utils/migrations/dbMigration.js b/server/utils/migrations/dbMigration.js
index 0a48ac60a3..8337f5aab1 100644
--- a/server/utils/migrations/dbMigration.js
+++ b/server/utils/migrations/dbMigration.js
@@ -1258,7 +1258,7 @@ async function handleOldLibraryItems(ctx) {
*/
async function handleOldLibraries(ctx) {
const oldLibraries = await oldDbFiles.loadOldData('libraries')
- const libraries = await ctx.models.library.getAllOldLibraries()
+ const libraries = await ctx.models.library.getAllWithFolders()
let librariesUpdated = 0
for (const library of libraries) {
@@ -1268,13 +1268,17 @@ async function handleOldLibraries(ctx) {
return false
}
const folderPaths = ol.folders?.map((f) => f.fullPath) || []
- return folderPaths.join(',') === library.folderPaths.join(',')
+ return folderPaths.join(',') === library.libraryFolders.map((f) => f.path).join(',')
})
if (matchingOldLibrary) {
- library.oldLibraryId = matchingOldLibrary.id
+ const newExtraData = library.extraData || {}
+ newExtraData.oldLibraryId = matchingOldLibrary.id
+ library.extraData = newExtraData
+ library.changed('extraData', true)
+
oldDbIdMap.libraries[library.oldLibraryId] = library.id
- await ctx.models.library.updateFromOld(library)
+ await library.save()
librariesUpdated++
}
}
diff --git a/server/utils/parsers/parseNameString.js b/server/utils/parsers/parseNameString.js
index c1f8ab3f5e..741beb0911 100644
--- a/server/utils/parsers/parseNameString.js
+++ b/server/utils/parsers/parseNameString.js
@@ -42,15 +42,15 @@ module.exports.parse = (nameString) => {
var splitNames = []
// Example &LF: Friedman, Milton & Friedman, Rose
if (nameString.includes('&')) {
- nameString.split('&').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
+ nameString.split('&').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
} else if (nameString.includes(' and ')) {
- nameString.split(' and ').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
+ nameString.split(' and ').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
} else if (nameString.includes(';')) {
- nameString.split(';').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
+ nameString.split(';').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
} else {
splitNames = nameString.split(',')
}
- if (splitNames.length) splitNames = splitNames.map(a => a.trim())
+ if (splitNames.length) splitNames = splitNames.map((a) => a.trim())
var names = []
@@ -84,21 +84,12 @@ module.exports.parse = (nameString) => {
}
// Filter out names that have no first and last
- names = names.filter(n => n.first_name || n.last_name)
+ names = names.filter((n) => n.first_name || n.last_name)
// Set name strings and remove duplicates
- const namesArray = [...new Set(names.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name))]
+ const namesArray = [...new Set(names.map((a) => (a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name)))]
return {
names: namesArray // Array of first last
}
}
-
-module.exports.checkNamesAreEqual = (name1, name2) => {
- if (!name1 || !name2) return false
-
- // e.g. John H. Smith will be equal to John H Smith
- name1 = String(name1).toLowerCase().trim().replace(/\./g, '')
- name2 = String(name2).toLowerCase().trim().replace(/\./g, '')
- return name1 === name2
-}
\ No newline at end of file
diff --git a/server/utils/queries/authorFilters.js b/server/utils/queries/authorFilters.js
index bd4d08925e..675915350f 100644
--- a/server/utils/queries/authorFilters.js
+++ b/server/utils/queries/authorFilters.js
@@ -73,8 +73,7 @@ module.exports = {
})
const authorMatches = []
for (const author of authors) {
- const oldAuthor = author.getOldAuthor().toJSON()
- oldAuthor.numBooks = author.dataValues.numBooks
+ const oldAuthor = author.toOldJSONExpanded(author.dataValues.numBooks)
authorMatches.push(oldAuthor)
}
return authorMatches
diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js
index 471a1c0b48..5c7b7dc0a9 100644
--- a/server/utils/queries/libraryFilters.js
+++ b/server/utils/queries/libraryFilters.js
@@ -15,12 +15,12 @@ module.exports = {
/**
* Get library items using filter and sort
- * @param {import('../../objects/Library')} library
+ * @param {string} libraryId
* @param {import('../../models/User')} user
* @param {object} options
* @returns {object} { libraryItems:LibraryItem[], count:number }
*/
- async getFilteredLibraryItems(library, user, options) {
+ async getFilteredLibraryItems(libraryId, user, options) {
const { filterBy, sortBy, sortDesc, limit, offset, collapseseries, include, mediaType } = options
let filterValue = null
@@ -33,22 +33,22 @@ module.exports = {
}
if (mediaType === 'book') {
- return libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset)
+ return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset)
} else {
- return libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset)
+ return libraryItemsPodcastFilters.getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset)
}
},
/**
* Get library items for continue listening & continue reading shelves
- * @param {import('../../objects/Library')} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
* @returns {Promise<{ items:import('../../models/LibraryItem')[], count:number }>}
*/
async getMediaItemsInProgress(library, user, include, limit) {
- if (library.mediaType === 'book') {
+ if (library.isBook) {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'in-progress', 'progress', true, false, include, limit, 0, true)
return {
items: libraryItems.map((li) => {
@@ -78,14 +78,14 @@ module.exports = {
/**
* Get library items for most recently added shelf
- * @param {import('../../objects/Library')} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
* @returns {object} { libraryItems:LibraryItem[], count:number }
*/
async getLibraryItemsMostRecentlyAdded(library, user, include, limit) {
- if (library.mediaType === 'book') {
+ if (library.isBook) {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, false, include, limit, 0)
return {
libraryItems: libraryItems.map((li) => {
@@ -126,7 +126,7 @@ module.exports = {
/**
* Get library items for continue series shelf
- * @param {import('../../objects/Library')} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
@@ -154,14 +154,15 @@ module.exports = {
/**
* Get library items or podcast episodes for the "Listen Again" and "Read Again" shelf
- * @param {import('../../objects/Library')} library
+ *
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
- * @returns {object} { items:object[], count:number }
+ * @returns {Promise<{ items:oldLibraryItem[], count:number }>}
*/
async getMediaFinished(library, user, include, limit) {
- if (library.mediaType === 'book') {
+ if (library.isBook) {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'finished', 'progress', true, false, include, limit, 0)
return {
items: libraryItems.map((li) => {
@@ -191,11 +192,11 @@ module.exports = {
/**
* Get series for recent series shelf
- * @param {import('../../objects/Library')} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
- * @returns {{ series:import('../../objects/entities/Series')[], count:number}}
+ * @returns {{ series:any[], count:number}}
*/
async getSeriesMostRecentlyAdded(library, user, include, limit) {
if (!library.isBook) return { series: [], count: 0 }
@@ -275,7 +276,7 @@ module.exports = {
const allOldSeries = []
for (const s of series) {
- const oldSeries = s.getOldSeries().toJSON()
+ const oldSeries = s.toOldJSON()
if (s.feeds?.length) {
oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified()
@@ -316,10 +317,11 @@ module.exports = {
/**
* Get most recently created authors for "Newest Authors" shelf
* Author must be linked to at least 1 book
- * @param {oldLibrary} library
+ *
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {number} limit
- * @returns {object} { authors:oldAuthor[], count:number }
+ * @returns {Promise<{ authors:oldAuthor[], count:number }>}
*/
async getNewestAuthors(library, user, limit) {
if (library.mediaType !== 'book') return { authors: [], count: 0 }
@@ -351,7 +353,7 @@ module.exports = {
return {
authors: authors.map((au) => {
const numBooks = au.books.length || 0
- return au.getOldAuthor().toJSONExpanded(numBooks)
+ return au.toOldJSONExpanded(numBooks)
}),
count
}
@@ -359,11 +361,11 @@ module.exports = {
/**
* Get book library items for the "Discover" shelf
- * @param {oldLibrary} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
- * @returns {object} {libraryItems:oldLibraryItem[], count:number}
+ * @returns {Promise<{libraryItems:oldLibraryItem[], count:number}>}
*/
async getLibraryItemsToDiscover(library, user, include, limit) {
if (library.mediaType !== 'book') return { libraryItems: [], count: 0 }
@@ -386,10 +388,10 @@ module.exports = {
/**
* Get podcast episodes most recently added
- * @param {oldLibrary} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {number} limit
- * @returns {object} {libraryItems:oldLibraryItem[], count:number}
+ * @returns {Promise<{libraryItems:oldLibraryItem[], count:number}>}
*/
async getNewestPodcastEpisodes(library, user, limit) {
if (library.mediaType !== 'podcast') return { libraryItems: [], count: 0 }
@@ -407,11 +409,11 @@ module.exports = {
/**
* Get library items for an author, optional use user permissions
- * @param {oldAuthor} author
+ * @param {import('../../models/Author')} author
* @param {import('../../models/User')} user
* @param {number} limit
* @param {number} offset
- * @returns {Promise} { libraryItems:LibraryItem[], count:number }
+ * @returns {Promise<{ libraryItems:import('../../objects/LibraryItem')[], count:number }>}
*/
async getLibraryItemsForAuthor(author, user, limit, offset) {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(author.libraryId, user, 'authors', author.id, 'addedAt', true, false, [], limit, offset)
@@ -424,7 +426,7 @@ module.exports = {
/**
* Get book library items in a collection
* @param {oldCollection} collection
- * @returns {Promise}
+ * @returns {Promise}
*/
getLibraryItemsForCollection(collection) {
return libraryItemsBookFilters.getLibraryItemsForCollection(collection)
diff --git a/server/utils/queries/libraryItemFilters.js b/server/utils/queries/libraryItemFilters.js
index 128df6fde7..7f95d0ecc4 100644
--- a/server/utils/queries/libraryItemFilters.js
+++ b/server/utils/queries/libraryItemFilters.js
@@ -173,16 +173,16 @@ module.exports = {
/**
* Search library items
* @param {import('../../models/User')} user
- * @param {import('../../objects/Library')} oldLibrary
+ * @param {import('../../models/Library')} library
* @param {string} query
* @param {number} limit
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[], podcast:object[]}}
*/
- search(user, oldLibrary, query, limit) {
- if (oldLibrary.isBook) {
- return libraryItemsBookFilters.search(user, oldLibrary, query, limit, 0)
+ search(user, library, query, limit) {
+ if (library.isBook) {
+ return libraryItemsBookFilters.search(user, library, query, limit, 0)
} else {
- return libraryItemsPodcastFilters.search(user, oldLibrary, query, limit, 0)
+ return libraryItemsPodcastFilters.search(user, library, query, limit, 0)
}
},
diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js
index da356b3ee6..ae1ccc03bc 100644
--- a/server/utils/queries/libraryItemsBookFilters.js
+++ b/server/utils/queries/libraryItemsBookFilters.js
@@ -636,7 +636,7 @@ module.exports = {
* 2. Has no books in progress
* 3. Has at least 1 unfinished book
* TODO: Reduce queries
- * @param {import('../../objects/Library')} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
@@ -911,7 +911,7 @@ module.exports = {
/**
* Get book library items in a collection
* @param {oldCollection} collection
- * @returns {Promise}
+ * @returns {Promise}
*/
async getLibraryItemsForCollection(collection) {
if (!collection?.books?.length) {
@@ -954,25 +954,25 @@ module.exports = {
/**
* Get library items for series
- * @param {import('../../objects/entities/Series')} oldSeries
+ * @param {import('../../models/Series')} series
* @param {import('../../models/User')} [user]
* @returns {Promise}
*/
- async getLibraryItemsForSeries(oldSeries, user) {
- const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, user, 'series', oldSeries.id, null, null, false, [], null, null)
+ async getLibraryItemsForSeries(series, user) {
+ const { libraryItems } = await this.getFilteredLibraryItems(series.libraryId, user, 'series', series.id, null, null, false, [], null, null)
return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
},
/**
* Search books, authors, series
* @param {import('../../models/User')} user
- * @param {import('../../objects/Library')} oldLibrary
+ * @param {import('../../models/Library')} library
* @param {string} query
* @param {number} limit
* @param {number} offset
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}}
*/
- async search(user, oldLibrary, query, limit, offset) {
+ async search(user, library, query, limit, offset) {
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
const normalizedQuery = query
@@ -1006,7 +1006,7 @@ module.exports = {
{
model: Database.libraryItemModel,
where: {
- libraryId: oldLibrary.id
+ libraryId: library.id
}
},
{
@@ -1047,7 +1047,7 @@ module.exports = {
const narratorMatches = []
const [narratorResults] = await Database.sequelize.query(`SELECT value, count(*) AS numBooks FROM books b, libraryItems li, json_each(b.narrators) WHERE json_valid(b.narrators) AND ${matchJsonValue} AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
replacements: {
- libraryId: oldLibrary.id,
+ libraryId: library.id,
limit,
offset
},
@@ -1064,7 +1064,7 @@ module.exports = {
const tagMatches = []
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.tags) WHERE json_valid(b.tags) AND ${matchJsonValue} AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
replacements: {
- libraryId: oldLibrary.id,
+ libraryId: library.id,
limit,
offset
},
@@ -1081,7 +1081,7 @@ module.exports = {
const genreMatches = []
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.genres) WHERE json_valid(b.genres) AND ${matchJsonValue} AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
replacements: {
- libraryId: oldLibrary.id,
+ libraryId: library.id,
limit,
offset
},
@@ -1101,7 +1101,7 @@ module.exports = {
[Sequelize.Op.and]: [
Sequelize.literal(matchName),
{
- libraryId: oldLibrary.id
+ libraryId: library.id
}
]
},
@@ -1130,13 +1130,13 @@ module.exports = {
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
})
seriesMatches.push({
- series: series.getOldSeries().toJSON(),
+ series: series.toOldJSON(),
books
})
}
// Search authors
- const authorMatches = await authorFilters.search(oldLibrary.id, normalizedQuery, limit, offset)
+ const authorMatches = await authorFilters.search(library.id, normalizedQuery, limit, offset)
return {
book: itemMatches,
diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js
index 1fe6dbc5cb..50163edfbd 100644
--- a/server/utils/queries/libraryItemsPodcastFilters.js
+++ b/server/utils/queries/libraryItemsPodcastFilters.js
@@ -306,13 +306,13 @@ module.exports = {
/**
* Search podcasts
* @param {import('../../models/User')} user
- * @param {import('../../objects/Library')} oldLibrary
+ * @param {import('../../models/Library')} library
* @param {string} query
* @param {number} limit
* @param {number} offset
* @returns {{podcast:object[], tags:object[]}}
*/
- async search(user, oldLibrary, query, limit, offset) {
+ async search(user, library, query, limit, offset) {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
const normalizedQuery = query
@@ -345,7 +345,7 @@ module.exports = {
{
model: Database.libraryItemModel,
where: {
- libraryId: oldLibrary.id
+ libraryId: library.id
}
}
],
@@ -372,7 +372,7 @@ module.exports = {
const tagMatches = []
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.tags) WHERE json_valid(p.tags) AND ${matchJsonValue} AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
replacements: {
- libraryId: oldLibrary.id,
+ libraryId: library.id,
limit,
offset
},
@@ -389,7 +389,7 @@ module.exports = {
const genreMatches = []
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.genres) WHERE json_valid(p.genres) AND ${matchJsonValue} AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
replacements: {
- libraryId: oldLibrary.id,
+ libraryId: library.id,
limit,
offset
},
@@ -412,12 +412,12 @@ module.exports = {
/**
* Most recent podcast episodes not finished
* @param {import('../../models/User')} user
- * @param {import('../../objects/Library')} oldLibrary
+ * @param {import('../../models/Library')} library
* @param {number} limit
* @param {number} offset
* @returns {Promise}
*/
- async getRecentEpisodes(user, oldLibrary, limit, offset) {
+ async getRecentEpisodes(user, library, limit, offset) {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
const episodes = await Database.podcastEpisodeModel.findAll({
@@ -435,7 +435,7 @@ module.exports = {
include: {
model: Database.libraryItemModel,
where: {
- libraryId: oldLibrary.id
+ libraryId: library.id
}
}
},
diff --git a/server/utils/queries/seriesFilters.js b/server/utils/queries/seriesFilters.js
index c03c13bff2..06ca254793 100644
--- a/server/utils/queries/seriesFilters.js
+++ b/server/utils/queries/seriesFilters.js
@@ -11,7 +11,7 @@ module.exports = {
/**
* Get series filtered and sorted
*
- * @param {import('../../objects/Library')} library
+ * @param {import('../../models/Library')} library
* @param {import('../../models/User')} user
* @param {string} filterBy
* @param {string} sortBy
@@ -171,7 +171,7 @@ module.exports = {
// Map series to old series
const allOldSeries = []
for (const s of series) {
- const oldSeries = s.getOldSeries().toJSON()
+ const oldSeries = s.toOldJSON()
if (s.dataValues.totalDuration) {
oldSeries.totalDuration = s.dataValues.totalDuration
diff --git a/server/utils/scandir.js b/server/utils/scandir.js
index 21c28b8cf3..ff21e814f2 100644
--- a/server/utils/scandir.js
+++ b/server/utils/scandir.js
@@ -19,8 +19,7 @@ const parseNameString = require('./parsers/parseNameString')
function isMediaFile(mediaType, ext, audiobooksOnly = false) {
if (!ext) return false
const extclean = ext.slice(1).toLowerCase()
- if (mediaType === 'podcast' || mediaType === 'music') return globals.SupportedAudioTypes.includes(extclean)
- else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean)
+ if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean)
else if (audiobooksOnly) return globals.SupportedAudioTypes.includes(extclean)
return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
}
@@ -35,29 +34,33 @@ module.exports.checkFilepathIsAudioFile = checkFilepathIsAudioFile
/**
* TODO: Function needs to be re-done
- * @param {string} mediaType
+ * @param {string} mediaType
* @param {string[]} paths array of relative file paths
* @returns {Record} map of files grouped into potential libarary item dirs
*/
function groupFilesIntoLibraryItemPaths(mediaType, paths) {
// Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir
var nonMediaFilePaths = []
- var pathsFiltered = paths.map(path => {
- return path.startsWith('/') ? path.slice(1) : path
- }).filter(path => {
- let parsedPath = Path.parse(path)
- // Is not in root dir OR is a book media file
- if (parsedPath.dir) {
- if (!isMediaFile(mediaType, parsedPath.ext, false)) { // Seperate out non-media files
- nonMediaFilePaths.push(path)
- return false
+ var pathsFiltered = paths
+ .map((path) => {
+ return path.startsWith('/') ? path.slice(1) : path
+ })
+ .filter((path) => {
+ let parsedPath = Path.parse(path)
+ // Is not in root dir OR is a book media file
+ if (parsedPath.dir) {
+ if (!isMediaFile(mediaType, parsedPath.ext, false)) {
+ // Seperate out non-media files
+ nonMediaFilePaths.push(path)
+ return false
+ }
+ return true
+ } else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) {
+ // (book media type supports single file audiobooks/ebooks in root dir)
+ return true
}
- return true
- } else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) { // (book media type supports single file audiobooks/ebooks in root dir)
- return true
- }
- return false
- })
+ return false
+ })
// Step 2: Sort by least number of directories
pathsFiltered.sort((a, b) => {
@@ -69,7 +72,9 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
// Step 3: Group files in dirs
var itemGroup = {}
pathsFiltered.forEach((path) => {
- var dirparts = Path.dirname(path).split('/').filter(p => !!p && p !== '.') // dirname returns . if no directory
+ var dirparts = Path.dirname(path)
+ .split('/')
+ .filter((p) => !!p && p !== '.') // dirname returns . if no directory
var numparts = dirparts.length
var _path = ''
@@ -82,14 +87,17 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
var dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart)
- if (itemGroup[_path]) { // Directory already has files, add file
+ if (itemGroup[_path]) {
+ // Directory already has files, add file
var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path))
itemGroup[_path].push(relpath)
return
- } else if (!dirparts.length) { // This is the last directory, create group
+ } else if (!dirparts.length) {
+ // This is the last directory, create group
itemGroup[_path] = [Path.basename(path)]
return
- } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group
+ } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) {
+ // Next directory is the last and is a CD dir, create group
itemGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))]
return
}
@@ -99,7 +107,6 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
// Step 4: Add in non-media files if they fit into item group
if (nonMediaFilePaths.length) {
-
for (const nonMediaFilePath of nonMediaFilePaths) {
const pathDir = Path.dirname(nonMediaFilePath)
const filename = Path.basename(nonMediaFilePath)
@@ -111,7 +118,8 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
for (let i = 0; i < numparts; i++) {
const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart)
- if (itemGroup[_path]) { // Directory is a group
+ if (itemGroup[_path]) {
+ // Directory is a group
const relpath = Path.posix.join(dirparts.join('/'), filename)
itemGroup[_path].push(relpath)
} else if (!dirparts.length) {
@@ -126,31 +134,22 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
/**
- * @param {string} mediaType
+ * @param {string} mediaType
* @param {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} fileItems (see recurseFiles)
- * @param {boolean} [audiobooksOnly=false]
+ * @param {boolean} [audiobooksOnly=false]
* @returns {Record} map of files grouped into potential libarary item dirs
*/
function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly = false) {
- // Handle music where every audio file is a library item
- if (mediaType === 'music') {
- const audioFileGroup = {}
- fileItems.filter(i => isMediaFile(mediaType, i.extension, audiobooksOnly)).forEach((item) => {
- audioFileGroup[item.path] = item.path
- })
- return audioFileGroup
- }
-
// Step 1: Filter out non-book-media files in root dir (with depth of 0)
- const itemsFiltered = fileItems.filter(i => {
- return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video' || mediaType === 'music') && isMediaFile(mediaType, i.extension, audiobooksOnly))
+ const itemsFiltered = fileItems.filter((i) => {
+ return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension, audiobooksOnly))
})
// Step 2: Seperate media files and other files
// - Directories without a media file will not be included
const mediaFileItems = []
const otherFileItems = []
- itemsFiltered.forEach(item => {
+ itemsFiltered.forEach((item) => {
if (isMediaFile(mediaType, item.extension, audiobooksOnly)) mediaFileItems.push(item)
else otherFileItems.push(item)
})
@@ -158,7 +157,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly
// Step 3: Group audio files in library items
const libraryItemGroup = {}
mediaFileItems.forEach((item) => {
- const dirparts = item.reldirpath.split('/').filter(p => !!p)
+ const dirparts = item.reldirpath.split('/').filter((p) => !!p)
const numparts = dirparts.length
let _path = ''
@@ -171,14 +170,17 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly
const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart)
- if (libraryItemGroup[_path]) { // Directory already has files, add file
+ if (libraryItemGroup[_path]) {
+ // Directory already has files, add file
const relpath = Path.posix.join(dirparts.join('/'), item.name)
libraryItemGroup[_path].push(relpath)
return
- } else if (!dirparts.length) { // This is the last directory, create group
+ } else if (!dirparts.length) {
+ // This is the last directory, create group
libraryItemGroup[_path] = [item.name]
return
- } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group
+ } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) {
+ // Next directory is the last and is a CD dir, create group
libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)]
return
}
@@ -196,7 +198,8 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly
for (let i = 0; i < numparts; i++) {
const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart)
- if (libraryItemGroup[_path]) { // Directory is audiobook group
+ if (libraryItemGroup[_path]) {
+ // Directory is audiobook group
const relpath = Path.posix.join(dirparts.join('/'), item.name)
libraryItemGroup[_path].push(relpath)
return
@@ -209,33 +212,35 @@ module.exports.groupFileItemsIntoLibraryItemDirs = groupFileItemsIntoLibraryItem
/**
* Get LibraryFile from filepath
- * @param {string} libraryItemPath
- * @param {string[]} files
+ * @param {string} libraryItemPath
+ * @param {string[]} files
* @returns {import('../objects/files/LibraryFile')}
*/
function buildLibraryFile(libraryItemPath, files) {
- return Promise.all(files.map(async (file) => {
- const filePath = Path.posix.join(libraryItemPath, file)
- const newLibraryFile = new LibraryFile()
- await newLibraryFile.setDataFromPath(filePath, file)
- return newLibraryFile
- }))
+ return Promise.all(
+ files.map(async (file) => {
+ const filePath = Path.posix.join(libraryItemPath, file)
+ const newLibraryFile = new LibraryFile()
+ await newLibraryFile.setDataFromPath(filePath, file)
+ return newLibraryFile
+ })
+ )
}
module.exports.buildLibraryFile = buildLibraryFile
/**
* Get details parsed from filenames
- *
- * @param {string} relPath
- * @param {boolean} parseSubtitle
+ *
+ * @param {string} relPath
+ * @param {boolean} parseSubtitle
* @returns {LibraryItemFilenameMetadata}
*/
function getBookDataFromDir(relPath, parseSubtitle = false) {
const splitDir = relPath.split('/')
var folder = splitDir.pop() // Audio files will always be in the directory named for the title
- series = (splitDir.length > 1) ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series
- author = (splitDir.length > 0) ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/
+ series = splitDir.length > 1 ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series
+ author = splitDir.length > 0 ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/
// The may contain various other pieces of metadata, these functions extract it.
var [folder, asin] = getASIN(folder)
@@ -244,7 +249,6 @@ function getBookDataFromDir(relPath, parseSubtitle = false) {
var [folder, publishedYear] = getPublishedYear(folder)
var [title, subtitle] = parseSubtitle ? getSubtitle(folder) : [folder, null]
-
return {
title,
subtitle,
@@ -260,8 +264,8 @@ module.exports.getBookDataFromDir = getBookDataFromDir
/**
* Extract narrator from folder name
- *
- * @param {string} folder
+ *
+ * @param {string} folder
* @returns {[string, string]} [folder, narrator]
*/
function getNarrator(folder) {
@@ -272,7 +276,7 @@ function getNarrator(folder) {
/**
* Extract series sequence from folder name
- *
+ *
* @example
* 'Book 2 - Title - Subtitle'
* 'Title - Subtitle - Vol 12'
@@ -283,8 +287,8 @@ function getNarrator(folder) {
* '100 - Book Title'
* '6. Title'
* '0.5 - Book Title'
- *
- * @param {string} folder
+ *
+ * @param {string} folder
* @returns {[string, string]} [folder, sequence]
*/
function getSequence(folder) {
@@ -299,7 +303,9 @@ function getSequence(folder) {
if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) {
volumeNumber = isNaN(match.groups.sequence) ? match.groups.sequence : Number(match.groups.sequence).toString()
parts[i] = match.groups.suffix
- if (!parts[i]) { parts.splice(i, 1) }
+ if (!parts[i]) {
+ parts.splice(i, 1)
+ }
break
}
}
@@ -310,8 +316,8 @@ function getSequence(folder) {
/**
* Extract published year from folder name
- *
- * @param {string} folder
+ *
+ * @param {string} folder
* @returns {[string, string]} [folder, publishedYear]
*/
function getPublishedYear(folder) {
@@ -329,8 +335,8 @@ function getPublishedYear(folder) {
/**
* Extract subtitle from folder name
- *
- * @param {string} folder
+ *
+ * @param {string} folder
* @returns {[string, string]} [folder, subtitle]
*/
function getSubtitle(folder) {
@@ -341,8 +347,8 @@ function getSubtitle(folder) {
/**
* Extract asin from folder name
- *
- * @param {string} folder
+ *
+ * @param {string} folder
* @returns {[string, string]} [folder, asin]
*/
function getASIN(folder) {
@@ -358,8 +364,8 @@ function getASIN(folder) {
}
/**
- *
- * @param {string} relPath
+ *
+ * @param {string} relPath
* @returns {LibraryItemFilenameMetadata}
*/
function getPodcastDataFromDir(relPath) {
@@ -373,10 +379,10 @@ function getPodcastDataFromDir(relPath) {
}
/**
- *
- * @param {string} libraryMediaType
- * @param {string} folderPath
- * @param {string} relPath
+ *
+ * @param {string} libraryMediaType
+ * @param {string} folderPath
+ * @param {string} relPath
* @returns {{ mediaMetadata: LibraryItemFilenameMetadata, relPath: string, path: string}}
*/
function getDataFromMediaDir(libraryMediaType, folderPath, relPath) {
@@ -386,7 +392,8 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath) {
if (libraryMediaType === 'podcast') {
mediaMetadata = getPodcastDataFromDir(relPath)
- } else { // book
+ } else {
+ // book
mediaMetadata = getBookDataFromDir(relPath, !!global.ServerSettings.scannerParseSubtitle)
}
diff --git a/test/server/Logger.test.js b/test/server/Logger.test.js
new file mode 100644
index 0000000000..43b8e9afda
--- /dev/null
+++ b/test/server/Logger.test.js
@@ -0,0 +1,285 @@
+const { expect } = require('chai')
+const sinon = require('sinon')
+const Logger = require('../../server/Logger') // Adjust the path as needed
+const { LogLevel } = require('../../server/utils/constants')
+const date = require('../../server/libs/dateAndTime')
+const util = require('util')
+
+describe('Logger', function () {
+ let consoleTraceStub
+ let consoleDebugStub
+ let consoleInfoStub
+ let consoleWarnStub
+ let consoleErrorStub
+ let consoleLogStub
+
+ beforeEach(function () {
+ // Stub the date format function to return a consistent timestamp
+ sinon.stub(date, 'format').returns('2024-09-10 12:34:56.789')
+ // Stub the source getter to return a consistent source
+ sinon.stub(Logger, 'source').get(() => 'some/source.js')
+ // Stub the console methods used in Logger
+ consoleTraceStub = sinon.stub(console, 'trace')
+ consoleDebugStub = sinon.stub(console, 'debug')
+ consoleInfoStub = sinon.stub(console, 'info')
+ consoleWarnStub = sinon.stub(console, 'warn')
+ consoleErrorStub = sinon.stub(console, 'error')
+ consoleLogStub = sinon.stub(console, 'log')
+ // Initialize the Logger's logManager as a mock object
+ Logger.logManager = {
+ logToFile: sinon.stub().resolves()
+ }
+ })
+
+ afterEach(function () {
+ sinon.restore()
+ })
+
+ describe('logging methods', function () {
+ it('should have a method for each log level defined in the static block', function () {
+ const loggerMethods = Object.keys(LogLevel).map((key) => key.toLowerCase())
+
+ loggerMethods.forEach((method) => {
+ expect(Logger).to.have.property(method).that.is.a('function')
+ })
+ })
+
+ it('should call console.trace for trace logging', function () {
+ // Arrange
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.trace('Test message')
+
+ // Assert
+ expect(consoleTraceStub.calledOnce).to.be.true
+ })
+
+ it('should call console.debug for debug logging', function () {
+ // Arrange
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.debug('Test message')
+
+ // Assert
+ expect(consoleDebugStub.calledOnce).to.be.true
+ })
+
+ it('should call console.info for info logging', function () {
+ // Arrange
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.info('Test message')
+
+ // Assert
+ expect(consoleInfoStub.calledOnce).to.be.true
+ })
+
+ it('should call console.warn for warn logging', function () {
+ // Arrange
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.warn('Test message')
+
+ // Assert
+ expect(consoleWarnStub.calledOnce).to.be.true
+ })
+
+ it('should call console.error for error logging', function () {
+ // Arrange
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.error('Test message')
+
+ // Assert
+ expect(consoleErrorStub.calledOnce).to.be.true
+ })
+
+ it('should call console.error for fatal logging', function () {
+ // Arrange
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.fatal('Test message')
+
+ // Assert
+ expect(consoleErrorStub.calledOnce).to.be.true
+ })
+
+ it('should call console.log for note logging', function () {
+ // Arrange
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.note('Test message')
+
+ // Assert
+ expect(consoleLogStub.calledOnce).to.be.true
+ })
+ })
+
+ describe('#log', function () {
+ it('should log to console and file if level is high enough', async function () {
+ // Arrange
+ const logArgs = ['Test message']
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ Logger.debug(...logArgs)
+
+ expect(consoleDebugStub.calledOnce).to.be.true
+ expect(consoleDebugStub.calledWithExactly('[2024-09-10 12:34:56.789] DEBUG:', ...logArgs)).to.be.true
+ expect(Logger.logManager.logToFile.calledOnce).to.be.true
+ expect(
+ Logger.logManager.logToFile.calledWithExactly({
+ timestamp: '2024-09-10 12:34:56.789',
+ source: 'some/source.js',
+ message: 'Test message',
+ levelName: 'DEBUG',
+ level: LogLevel.DEBUG
+ })
+ ).to.be.true
+ })
+
+ it('should not log if log level is too low', function () {
+ // Arrange
+ const logArgs = ['This log should not appear']
+ // Set log level to ERROR, so DEBUG log should be ignored
+ Logger.logLevel = LogLevel.ERROR
+
+ // Act
+ Logger.debug(...logArgs)
+
+ // Verify console.debug is not called
+ expect(consoleDebugStub.called).to.be.false
+ expect(Logger.logManager.logToFile.called).to.be.false
+ })
+
+ it('should emit log to all connected sockets with appropriate log level', async function () {
+ // Arrange
+ const socket1 = { id: '1', emit: sinon.spy() }
+ const socket2 = { id: '2', emit: sinon.spy() }
+ Logger.addSocketListener(socket1, LogLevel.DEBUG)
+ Logger.addSocketListener(socket2, LogLevel.ERROR)
+ const logArgs = ['Socket test']
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ await Logger.debug(...logArgs)
+
+ // socket1 should receive the log, but not socket2
+ expect(socket1.emit.calledOnce).to.be.true
+ expect(
+ socket1.emit.calledWithExactly('log', {
+ timestamp: '2024-09-10 12:34:56.789',
+ source: 'some/source.js',
+ message: 'Socket test',
+ levelName: 'DEBUG',
+ level: LogLevel.DEBUG
+ })
+ ).to.be.true
+
+ expect(socket2.emit.called).to.be.false
+ })
+
+ it('should log fatal messages to console and file regardless of log level', async function () {
+ // Arrange
+ const logArgs = ['Fatal error']
+ // Set log level to NOTE + 1, so nothing should be logged
+ Logger.logLevel = LogLevel.NOTE + 1
+
+ // Act
+ await Logger.fatal(...logArgs)
+
+ // Assert
+ expect(consoleErrorStub.calledOnce).to.be.true
+ expect(consoleErrorStub.calledWithExactly('[2024-09-10 12:34:56.789] FATAL:', ...logArgs)).to.be.true
+ expect(Logger.logManager.logToFile.calledOnce).to.be.true
+ expect(
+ Logger.logManager.logToFile.calledWithExactly({
+ timestamp: '2024-09-10 12:34:56.789',
+ source: 'some/source.js',
+ message: 'Fatal error',
+ levelName: 'FATAL',
+ level: LogLevel.FATAL
+ })
+ ).to.be.true
+ })
+
+ it('should log note messages to console and file regardless of log level', async function () {
+ // Arrange
+ const logArgs = ['Note message']
+ // Set log level to NOTE + 1, so nothing should be logged
+ Logger.logLevel = LogLevel.NOTE + 1
+
+ // Act
+ await Logger.note(...logArgs)
+
+ // Assert
+ expect(consoleLogStub.calledOnce).to.be.true
+ expect(consoleLogStub.calledWithExactly('[2024-09-10 12:34:56.789] NOTE:', ...logArgs)).to.be.true
+ expect(Logger.logManager.logToFile.calledOnce).to.be.true
+ expect(
+ Logger.logManager.logToFile.calledWithExactly({
+ timestamp: '2024-09-10 12:34:56.789',
+ source: 'some/source.js',
+ message: 'Note message',
+ levelName: 'NOTE',
+ level: LogLevel.NOTE
+ })
+ ).to.be.true
+ })
+
+ it('should log util.inspect(arg) for non-string objects', async function () {
+ // Arrange
+ const obj = { key: 'value' }
+ const logArgs = ['Logging object:', obj]
+ Logger.logLevel = LogLevel.TRACE
+
+ // Act
+ await Logger.debug(...logArgs)
+
+ // Assert
+ expect(consoleDebugStub.calledOnce).to.be.true
+ expect(consoleDebugStub.calledWithExactly('[2024-09-10 12:34:56.789] DEBUG:', 'Logging object:', obj)).to.be.true
+ expect(Logger.logManager.logToFile.calledOnce).to.be.true
+ expect(Logger.logManager.logToFile.firstCall.args[0].message).to.equal('Logging object: ' + util.inspect(obj))
+ })
+ })
+
+ describe('socket listeners', function () {
+ it('should add and remove socket listeners', function () {
+ // Arrange
+ const socket1 = { id: '1', emit: sinon.spy() }
+ const socket2 = { id: '2', emit: sinon.spy() }
+
+ // Act
+ Logger.addSocketListener(socket1, LogLevel.DEBUG)
+ Logger.addSocketListener(socket2, LogLevel.ERROR)
+ Logger.removeSocketListener('1')
+
+ // Assert
+ expect(Logger.socketListeners).to.have.lengthOf(1)
+ expect(Logger.socketListeners[0].id).to.equal('2')
+ })
+ })
+
+ describe('setLogLevel', function () {
+ it('should change the log level and log the new level', function () {
+ // Arrange
+ const debugSpy = sinon.spy(Logger, 'debug')
+
+ // Act
+ Logger.setLogLevel(LogLevel.WARN)
+
+ // Assert
+ expect(Logger.logLevel).to.equal(LogLevel.WARN)
+ expect(debugSpy.calledOnce).to.be.true
+ expect(debugSpy.calledWithExactly('Set Log Level to WARN')).to.be.true
+ })
+ })
+})
diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js
index 03f81f124c..c986cc986a 100644
--- a/test/server/finders/BookFinder.test.js
+++ b/test/server/finders/BookFinder.test.js
@@ -22,7 +22,7 @@ describe('TitleCandidates', () => {
})
describe('single add', () => {
- [
+ ;[
['adds candidate', 'anna karenina', ['anna karenina']],
['adds lowercased candidate', 'ANNA KARENINA', ['anna karenina']],
['adds candidate, removing redundant spaces', 'anna karenina', ['anna karenina']],
@@ -40,23 +40,27 @@ describe('TitleCandidates', () => {
['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']],
['does not add empty candidate', '', []],
['does not add spaces-only candidate', ' ', []],
- ['does not add empty variant', '1984', ['1984']],
- ].forEach(([name, title, expected]) => it(name, () => {
- titleCandidates.add(title)
- expect(titleCandidates.getCandidates()).to.deep.equal(expected)
- }))
+ ['does not add empty variant', '1984', ['1984']]
+ ].forEach(([name, title, expected]) =>
+ it(name, () => {
+ titleCandidates.add(title)
+ expect(titleCandidates.getCandidates()).to.deep.equal(expected)
+ })
+ )
})
describe('multiple adds', () => {
- [
+ ;[
['demotes digits-only candidates', ['01', 'anna karenina'], ['anna karenina', '01']],
['promotes transformed variants', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']],
['orders by position', ['title2', 'title1'], ['title2', 'title1']],
- ['dedupes candidates', ['title1', 'title1'], ['title1']],
- ].forEach(([name, titles, expected]) => it(name, () => {
- for (const title of titles) titleCandidates.add(title)
- expect(titleCandidates.getCandidates()).to.deep.equal(expected)
- }))
+ ['dedupes candidates', ['title1', 'title1'], ['title1']]
+ ].forEach(([name, titles, expected]) =>
+ it(name, () => {
+ for (const title of titles) titleCandidates.add(title)
+ expect(titleCandidates.getCandidates()).to.deep.equal(expected)
+ })
+ )
})
})
@@ -69,12 +73,12 @@ describe('TitleCandidates', () => {
})
describe('single add', () => {
- [
- ['adds a candidate', 'leo tolstoy', ['leo tolstoy']],
- ].forEach(([name, title, expected]) => it(name, () => {
- titleCandidates.add(title)
- expect(titleCandidates.getCandidates()).to.deep.equal(expected)
- }))
+ ;[['adds a candidate', 'leo tolstoy', ['leo tolstoy']]].forEach(([name, title, expected]) =>
+ it(name, () => {
+ titleCandidates.add(title)
+ expect(titleCandidates.getCandidates()).to.deep.equal(expected)
+ })
+ )
})
})
})
@@ -82,11 +86,7 @@ describe('TitleCandidates', () => {
describe('AuthorCandidates', () => {
let authorCandidates
const audnexus = {
- authorASINsRequest: sinon.stub().resolves([
- { name: 'Leo Tolstoy' },
- { name: 'Nikolai Gogol' },
- { name: 'J. K. Rowling' },
- ]),
+ authorASINsRequest: sinon.stub().resolves([{ name: 'Leo Tolstoy' }, { name: 'Nikolai Gogol' }, { name: 'J. K. Rowling' }])
}
describe('cleanAuthor is null', () => {
@@ -95,15 +95,15 @@ describe('AuthorCandidates', () => {
})
describe('no adds', () => {
- [
- ['returns empty author candidate', []],
- ].forEach(([name, expected]) => it(name, async () => {
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ;[['returns empty author candidate', []]].forEach(([name, expected]) =>
+ it(name, async () => {
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
describe('single add', () => {
- [
+ ;[
['adds recognized candidate', 'nikolai gogol', ['nikolai gogol']],
['does not add unrecognized candidate', 'fyodor dostoevsky', []],
['adds recognized author if candidate is a superstring', 'dr. nikolai gogol', ['nikolai gogol']],
@@ -112,21 +112,25 @@ describe('AuthorCandidates', () => {
['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []],
['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']],
['adds normalized recognized candidate (et al removed)', 'nikolai gogol et al.', ['nikolai gogol']],
- ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']],
- ].forEach(([name, author, expected]) => it(name, async () => {
- authorCandidates.add(author)
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']]
+ ].forEach(([name, author, expected]) =>
+ it(name, async () => {
+ authorCandidates.add(author)
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
describe('multi add', () => {
- [
+ ;[
['adds recognized author candidates', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']],
- ['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']],
- ].forEach(([name, authors, expected]) => it(name, async () => {
- for (const author of authors) authorCandidates.add(author)
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']]
+ ].forEach(([name, authors, expected]) =>
+ it(name, async () => {
+ for (const author of authors) authorCandidates.add(author)
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
})
@@ -138,21 +142,23 @@ describe('AuthorCandidates', () => {
})
describe('no adds', () => {
- [
- ['adds cleanAuthor as candidate', [cleanAuthor]],
- ].forEach(([name, expected]) => it(name, async () => {
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ;[['adds cleanAuthor as candidate', [cleanAuthor]]].forEach(([name, expected]) =>
+ it(name, async () => {
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
describe('single add', () => {
- [
+ ;[
['adds recognized candidate', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']],
- ['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]],
- ].forEach(([name, author, expected]) => it(name, async () => {
- authorCandidates.add(author)
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]]
+ ].forEach(([name, author, expected]) =>
+ it(name, async () => {
+ authorCandidates.add(author)
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
})
@@ -164,43 +170,47 @@ describe('AuthorCandidates', () => {
})
describe('no adds', () => {
- [
- ['adds cleanAuthor as candidate', [cleanAuthor]],
- ].forEach(([name, expected]) => it(name, async () => {
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ;[['adds cleanAuthor as candidate', [cleanAuthor]]].forEach(([name, expected]) =>
+ it(name, async () => {
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
describe('single add', () => {
- [
+ ;[
['adds recognized candidate and removes cleanAuthor', 'nikolai gogol', ['nikolai gogol']],
- ['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]],
- ].forEach(([name, author, expected]) => it(name, async () => {
- authorCandidates.add(author)
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]]
+ ].forEach(([name, author, expected]) =>
+ it(name, async () => {
+ authorCandidates.add(author)
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
})
describe('cleanAuthor is unrecognized and dirty', () => {
describe('no adds', () => {
- [
+ ;[
['adds aggressively cleaned cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']],
- ['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']],
- ].forEach(([name, cleanAuthor, expected]) => it(name, async () => {
- authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']]
+ ].forEach(([name, cleanAuthor, expected]) =>
+ it(name, async () => {
+ authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
describe('single add', () => {
- [
- ['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']],
- ].forEach(([name, cleanAuthor, author, expected]) => it(name, async () => {
- authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
- authorCandidates.add(author)
- expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
- }))
+ ;[['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']]].forEach(([name, cleanAuthor, author, expected]) =>
+ it(name, async () => {
+ authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
+ authorCandidates.add(author)
+ expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
+ })
+ )
})
})
})
@@ -211,16 +221,21 @@ describe('search', () => {
const u = 'unrecognized'
const r = ['book']
- const runSearchStub = sinon.stub(bookFinder, 'runSearch')
- runSearchStub.resolves([])
- runSearchStub.withArgs(t, a).resolves(r)
- runSearchStub.withArgs(t, u).resolves(r)
-
- const audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest')
- audnexusStub.resolves([{ name: a }])
+ let runSearchStub
+ let audnexusStub
beforeEach(() => {
- bookFinder.runSearch.resetHistory()
+ runSearchStub = sinon.stub(bookFinder, 'runSearch')
+ runSearchStub.resolves([])
+ runSearchStub.withArgs(t, a).resolves(r)
+ runSearchStub.withArgs(t, u).resolves(r)
+
+ audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest')
+ audnexusStub.resolves([{ name: a }])
+ })
+
+ afterEach(() => {
+ sinon.restore()
})
describe('search title is empty', () => {
@@ -238,50 +253,26 @@ describe('search', () => {
})
describe('search title contains recognized title and search author is a recognized author', () => {
- [
- [`${t} -`],
- [`${t} - ${a}`],
- [`${a} - ${t}`],
- [`${t}- ${a}`],
- [`${t} -${a}`],
- [`${t} ${a}`],
- [`${a} - ${t} (unabridged)`],
- [`${a} - ${t} (subtitle) - mp3`],
- [`${t} {narrator} - series-01 64kbps 10:00:00`],
- [`${a} - ${t} (2006) narrated by narrator [unabridged]`],
- [`${t} - ${a} 2022 mp3`],
- [`01 ${t}`],
- [`2022_${t}_HQ`],
- ].forEach(([searchTitle]) => {
+ ;[[`${t} -`], [`${t} - ${a}`], [`${a} - ${t}`], [`${t}- ${a}`], [`${t} -${a}`], [`${t} ${a}`], [`${a} - ${t} (unabridged)`], [`${a} - ${t} (subtitle) - mp3`], [`${t} {narrator} - series-01 64kbps 10:00:00`], [`${a} - ${t} (2006) narrated by narrator [unabridged]`], [`${t} - ${a} 2022 mp3`], [`01 ${t}`], [`2022_${t}_HQ`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => {
expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 2)
})
- });
-
- [
- [`s-01 - ${t} (narrator) 64kbps 10:00:00`],
- [`${a} - series 01 - ${t}`],
- ].forEach(([searchTitle]) => {
+ })
+ ;[[`s-01 - ${t} (narrator) 64kbps 10:00:00`], [`${a} - series 01 - ${t}`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => {
expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 3)
})
- });
-
- [
- [`${t}-${a}`],
- [`${t} junk`],
- ].forEach(([searchTitle]) => {
+ })
+ ;[[`${t}-${a}`], [`${t} junk`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns an empty result`, async () => {
expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal([])
})
})
describe('maxFuzzySearches = 0', () => {
- [
- [`${t} - ${a}`],
- ].forEach(([searchTitle]) => {
+ ;[[`${t} - ${a}`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => {
expect(await bookFinder.search(null, '', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([])
sinon.assert.callCount(bookFinder.runSearch, 1)
@@ -290,10 +281,7 @@ describe('search', () => {
})
describe('maxFuzzySearches = 1', () => {
- [
- [`s-01 - ${t} (narrator) 64kbps 10:00:00`],
- [`${a} - series 01 - ${t}`],
- ].forEach(([searchTitle]) => {
+ ;[[`s-01 - ${t} (narrator) 64kbps 10:00:00`], [`${a} - series 01 - ${t}`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => {
expect(await bookFinder.search(null, '', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([])
sinon.assert.callCount(bookFinder.runSearch, 2)
@@ -303,21 +291,13 @@ describe('search', () => {
})
describe('search title contains recognized title and search author is empty', () => {
- [
- [`${t} - ${a}`],
- [`${a} - ${t}`],
- ].forEach(([searchTitle]) => {
+ ;[[`${t} - ${a}`], [`${a} - ${t}`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => {
expect(await bookFinder.search(null, '', searchTitle, '')).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 2)
})
- });
-
- [
- [`${t}`],
- [`${t} - ${u}`],
- [`${u} - ${t}`]
- ].forEach(([searchTitle]) => {
+ })
+ ;[[`${t}`], [`${t} - ${u}`], [`${u} - ${t}`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '') returns an empty result`, async () => {
expect(await bookFinder.search(null, '', searchTitle, '')).to.deep.equal([])
})
@@ -325,19 +305,13 @@ describe('search', () => {
})
describe('search title contains recognized title and search author is an unrecognized author', () => {
- [
- [`${t} - ${u}`],
- [`${u} - ${t}`]
- ].forEach(([searchTitle]) => {
+ ;[[`${t} - ${u}`], [`${u} - ${t}`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => {
expect(await bookFinder.search(null, '', searchTitle, u)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 2)
})
- });
-
- [
- [`${t}`]
- ].forEach(([searchTitle]) => {
+ })
+ ;[[`${t}`]].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => {
expect(await bookFinder.search(null, '', searchTitle, u)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 1)
@@ -346,16 +320,19 @@ describe('search', () => {
})
describe('search provider results have duration', () => {
- const libraryItem = { media: { duration: 60 * 1000 } }
+ const libraryItem = { media: { duration: 60 * 1000 } }
const provider = 'audible'
const unsorted = [{ duration: 3000 }, { duration: 2000 }, { duration: 1000 }, { duration: 500 }]
const sorted = [{ duration: 1000 }, { duration: 500 }, { duration: 2000 }, { duration: 3000 }]
- runSearchStub.withArgs(t, a, provider).resolves(unsorted)
+
+ beforeEach(() => {
+ runSearchStub.withArgs(t, a, provider).resolves(unsorted)
+ })
it('returns results sorted by library item duration diff', async () => {
expect(await bookFinder.search(libraryItem, provider, t, a)).to.deep.equal(sorted)
})
-
+
it('returns unsorted results if library item is null', async () => {
expect(await bookFinder.search(null, provider, t, a)).to.deep.equal(unsorted)
})
@@ -365,10 +342,10 @@ describe('search', () => {
})
it('returns unsorted results if library item media is undefined', async () => {
- expect(await bookFinder.search({ }, provider, t, a)).to.deep.equal(unsorted)
+ expect(await bookFinder.search({}, provider, t, a)).to.deep.equal(unsorted)
})
- it ('should return a result last if it has no duration', async () => {
+ it('should return a result last if it has no duration', async () => {
const unsorted = [{}, { duration: 3000 }, { duration: 2000 }, { duration: 1000 }, { duration: 500 }]
const sorted = [{ duration: 1000 }, { duration: 500 }, { duration: 2000 }, { duration: 3000 }, {}]
runSearchStub.withArgs(t, a, provider).resolves(unsorted)
diff --git a/test/server/managers/MigrationManager.test.js b/test/server/managers/MigrationManager.test.js
new file mode 100644
index 0000000000..ae28c0d118
--- /dev/null
+++ b/test/server/managers/MigrationManager.test.js
@@ -0,0 +1,503 @@
+const { expect } = require('chai')
+const sinon = require('sinon')
+const { Sequelize } = require('sequelize')
+const fs = require('../../../server/libs/fsExtra')
+const Logger = require('../../../server/Logger')
+const MigrationManager = require('../../../server/managers/MigrationManager')
+const path = require('path')
+const { Umzug, memoryStorage } = require('../../../server/libs/umzug')
+
+describe('MigrationManager', () => {
+ let sequelizeStub
+ let umzugStub
+ let migrationManager
+ let loggerInfoStub
+ let loggerErrorStub
+ let fsCopyStub
+ let fsMoveStub
+ let fsRemoveStub
+ let fsEnsureDirStub
+ let processExitStub
+ let configPath = '/path/to/config'
+
+ const serverVersion = '1.2.0'
+
+ beforeEach(() => {
+ sequelizeStub = sinon.createStubInstance(Sequelize)
+ umzugStub = {
+ migrations: sinon.stub(),
+ executed: sinon.stub(),
+ up: sinon.stub(),
+ down: sinon.stub()
+ }
+ sequelizeStub.getQueryInterface.returns({})
+ migrationManager = new MigrationManager(sequelizeStub, configPath)
+ migrationManager.fetchVersionsFromDatabase = sinon.stub().resolves()
+ migrationManager.copyMigrationsToConfigDir = sinon.stub().resolves()
+ migrationManager.updateMaxVersion = sinon.stub().resolves()
+ migrationManager.initUmzug = sinon.stub()
+ migrationManager.umzug = umzugStub
+ loggerInfoStub = sinon.stub(Logger, 'info')
+ loggerErrorStub = sinon.stub(Logger, 'error')
+ fsCopyStub = sinon.stub(fs, 'copy').resolves()
+ fsMoveStub = sinon.stub(fs, 'move').resolves()
+ fsRemoveStub = sinon.stub(fs, 'remove').resolves()
+ fsEnsureDirStub = sinon.stub(fs, 'ensureDir').resolves()
+ fsPathExistsStub = sinon.stub(fs, 'pathExists').resolves(true)
+ processExitStub = sinon.stub(process, 'exit')
+ })
+
+ afterEach(() => {
+ sinon.restore()
+ })
+
+ describe('init', () => {
+ it('should initialize the MigrationManager', async () => {
+ // arrange
+ migrationManager.databaseVersion = '1.1.0'
+ migrationManager.maxVersion = '1.1.0'
+ migrationManager.umzug = null
+ migrationManager.configPath = __dirname
+
+ // Act
+ await migrationManager.init(serverVersion)
+
+ // Assert
+ expect(migrationManager.serverVersion).to.equal(serverVersion)
+ expect(migrationManager.sequelize).to.equal(sequelizeStub)
+ expect(migrationManager.migrationsDir).to.equal(path.join(__dirname, 'migrations'))
+ expect(migrationManager.copyMigrationsToConfigDir.calledOnce).to.be.true
+ expect(migrationManager.updateMaxVersion.calledOnce).to.be.true
+ expect(migrationManager.initialized).to.be.true
+ })
+
+ it('should throw error if serverVersion is not provided', async () => {
+ // Act
+ try {
+ const result = await migrationManager.init()
+ expect.fail('Expected init to throw an error, but it did not.')
+ } catch (error) {
+ expect(error.message).to.equal('Invalid server version: undefined. Expected a version tag like v1.2.3.')
+ }
+ })
+ })
+
+ describe('runMigrations', () => {
+ it('should run up migrations successfully', async () => {
+ // Arrange
+ migrationManager.databaseVersion = '1.1.0'
+ migrationManager.maxVersion = '1.1.0'
+ migrationManager.serverVersion = '1.2.0'
+ migrationManager.initialized = true
+
+ umzugStub.migrations.resolves([{ name: 'v1.1.0-migration.js' }, { name: 'v1.1.1-migration.js' }, { name: 'v1.2.0-migration.js' }])
+ umzugStub.executed.resolves([{ name: 'v1.1.0-migration.js' }])
+
+ // Act
+ await migrationManager.runMigrations()
+
+ // Assert
+ expect(migrationManager.initUmzug.calledOnce).to.be.true
+ expect(umzugStub.up.calledOnce).to.be.true
+ expect(umzugStub.up.calledWith({ migrations: ['v1.1.1-migration.js', 'v1.2.0-migration.js'], rerun: 'ALLOW' })).to.be.true
+ expect(fsCopyStub.calledOnce).to.be.true
+ expect(fsCopyStub.calledWith(path.join(configPath, 'absdatabase.sqlite'), path.join(configPath, 'absdatabase.backup.sqlite'))).to.be.true
+ expect(fsRemoveStub.calledOnce).to.be.true
+ expect(fsRemoveStub.calledWith(path.join(configPath, 'absdatabase.backup.sqlite'))).to.be.true
+ expect(loggerInfoStub.calledWith(sinon.match('Migrations successfully applied'))).to.be.true
+ })
+
+ it('should run down migrations successfully', async () => {
+ // Arrange
+ migrationManager.databaseVersion = '1.2.0'
+ migrationManager.maxVersion = '1.2.0'
+ migrationManager.serverVersion = '1.1.0'
+ migrationManager.initialized = true
+
+ umzugStub.migrations.resolves([{ name: 'v1.1.0-migration.js' }, { name: 'v1.1.1-migration.js' }, { name: 'v1.2.0-migration.js' }])
+ umzugStub.executed.resolves([{ name: 'v1.1.0-migration.js' }, { name: 'v1.1.1-migration.js' }, { name: 'v1.2.0-migration.js' }])
+
+ // Act
+ await migrationManager.runMigrations()
+
+ // Assert
+ expect(migrationManager.initUmzug.calledOnce).to.be.true
+ expect(umzugStub.down.calledOnce).to.be.true
+ expect(umzugStub.down.calledWith({ migrations: ['v1.2.0-migration.js', 'v1.1.1-migration.js'], rerun: 'ALLOW' })).to.be.true
+ expect(fsCopyStub.calledOnce).to.be.true
+ expect(fsCopyStub.calledWith(path.join(configPath, 'absdatabase.sqlite'), path.join(configPath, 'absdatabase.backup.sqlite'))).to.be.true
+ expect(fsRemoveStub.calledOnce).to.be.true
+ expect(fsRemoveStub.calledWith(path.join(configPath, 'absdatabase.backup.sqlite'))).to.be.true
+ expect(loggerInfoStub.calledWith(sinon.match('Migrations successfully applied'))).to.be.true
+ })
+
+ it('should log that no migrations are needed if serverVersion equals databaseVersion', async () => {
+ // Arrange
+ migrationManager.serverVersion = '1.2.0'
+ migrationManager.databaseVersion = '1.2.0'
+ migrationManager.maxVersion = '1.2.0'
+ migrationManager.initialized = true
+
+ // Act
+ await migrationManager.runMigrations()
+
+ // Assert
+ expect(umzugStub.up.called).to.be.false
+ expect(loggerInfoStub.calledWith(sinon.match('Database is already up to date.'))).to.be.true
+ })
+
+ it('should handle migration failure and restore the original database', async () => {
+ // Arrange
+ migrationManager.serverVersion = '1.2.0'
+ migrationManager.databaseVersion = '1.1.0'
+ migrationManager.maxVersion = '1.1.0'
+ migrationManager.initialized = true
+
+ umzugStub.migrations.resolves([{ name: 'v1.2.0-migration.js' }])
+ umzugStub.executed.resolves([{ name: 'v1.1.0-migration.js' }])
+ umzugStub.up.rejects(new Error('Migration failed'))
+
+ const originalDbPath = path.join(configPath, 'absdatabase.sqlite')
+ const backupDbPath = path.join(configPath, 'absdatabase.backup.sqlite')
+
+ // Act
+ await migrationManager.runMigrations()
+
+ // Assert
+ expect(migrationManager.initUmzug.calledOnce).to.be.true
+ expect(umzugStub.up.calledOnce).to.be.true
+ expect(loggerErrorStub.calledWith(sinon.match('Migration failed'))).to.be.true
+ expect(fsMoveStub.calledWith(originalDbPath, sinon.match('absdatabase.failed.sqlite'), { overwrite: true })).to.be.true
+ expect(fsMoveStub.calledWith(backupDbPath, originalDbPath, { overwrite: true })).to.be.true
+ expect(loggerInfoStub.calledWith(sinon.match('Restored the original database'))).to.be.true
+ expect(processExitStub.calledOnce).to.be.true
+ })
+ })
+
+ describe('fetchVersionsFromDatabase', () => {
+ it('should fetch versions from the migrationsMeta table', async () => {
+ // Arrange
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
+ // Create a migrationsMeta table and populate it with version and maxVersion
+ await sequelize.query('CREATE TABLE migrationsMeta (key VARCHAR(255), value VARCHAR(255))')
+ await sequelize.query("INSERT INTO migrationsMeta (key, value) VALUES ('version', '1.1.0'), ('maxVersion', '1.1.0')")
+ const migrationManager = new MigrationManager(sequelize, configPath)
+ migrationManager.checkOrCreateMigrationsMetaTable = sinon.stub().resolves()
+
+ // Act
+ await migrationManager.fetchVersionsFromDatabase()
+
+ // Assert
+ expect(migrationManager.maxVersion).to.equal('1.1.0')
+ expect(migrationManager.databaseVersion).to.equal('1.1.0')
+ })
+
+ it('should create the migrationsMeta table if it does not exist and fetch versions from it', async () => {
+ // Arrange
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
+ const migrationManager = new MigrationManager(sequelize, configPath)
+ migrationManager.serverVersion = serverVersion
+
+ // Act
+ await migrationManager.fetchVersionsFromDatabase()
+
+ // Assert
+ const tableDescription = await sequelize.getQueryInterface().describeTable('migrationsMeta')
+ expect(tableDescription).to.deep.equal({
+ key: { type: 'VARCHAR(255)', allowNull: false, defaultValue: undefined, primaryKey: false, unique: false },
+ value: { type: 'VARCHAR(255)', allowNull: false, defaultValue: undefined, primaryKey: false, unique: false }
+ })
+ expect(migrationManager.maxVersion).to.equal('0.0.0')
+ expect(migrationManager.databaseVersion).to.equal(serverVersion)
+ })
+
+ it('should throw an error if the database query fails', async () => {
+ // Arrange
+ const sequelizeStub = sinon.createStubInstance(Sequelize)
+ sequelizeStub.query.rejects(new Error('Database query failed'))
+ const migrationManager = new MigrationManager(sequelizeStub, configPath)
+ migrationManager.checkOrCreateMigrationsMetaTable = sinon.stub().resolves()
+
+ // Act
+ try {
+ await migrationManager.fetchVersionsFromDatabase()
+ expect.fail('Expected fetchVersionsFromDatabase to throw an error, but it did not.')
+ } catch (error) {
+ // Assert
+ expect(error.message).to.equal('Database query failed')
+ }
+ })
+ })
+
+ describe('updateMaxVersion', () => {
+ it('should update the maxVersion in the database', async () => {
+ // Arrange
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
+ // Create a migrationsMeta table and populate it with version and maxVersion
+ await sequelize.query('CREATE TABLE migrationsMeta (key VARCHAR(255), value VARCHAR(255))')
+ await sequelize.query("INSERT INTO migrationsMeta (key, value) VALUES ('version', '1.1.0'), ('maxVersion', '1.1.0')")
+ const migrationManager = new MigrationManager(sequelize, configPath)
+ migrationManager.serverVersion = '1.2.0'
+
+ // Act
+ await migrationManager.updateMaxVersion()
+
+ // Assert
+ const [{ maxVersion }] = await sequelize.query("SELECT value AS maxVersion FROM migrationsMeta WHERE key = 'maxVersion'", {
+ type: Sequelize.QueryTypes.SELECT
+ })
+ expect(maxVersion).to.equal('1.2.0')
+ })
+ })
+
+ describe('extractVersionFromTag', () => {
+ it('should return null if tag is not provided', () => {
+ // Arrange
+ const migrationManager = new MigrationManager(sequelizeStub, configPath)
+
+ // Act
+ const result = migrationManager.extractVersionFromTag()
+
+ // Assert
+ expect(result).to.be.null
+ })
+
+ it('should return null if tag does not match the version format', () => {
+ // Arrange
+ const migrationManager = new MigrationManager(sequelizeStub, configPath)
+ const tag = 'invalid-tag'
+
+ // Act
+ const result = migrationManager.extractVersionFromTag(tag)
+
+ // Assert
+ expect(result).to.be.null
+ })
+
+ it('should extract the version from the tag', () => {
+ // Arrange
+ const migrationManager = new MigrationManager(sequelizeStub, configPath)
+ const tag = 'v1.2.3'
+
+ // Act
+ const result = migrationManager.extractVersionFromTag(tag)
+
+ // Assert
+ expect(result).to.equal('1.2.3')
+ })
+ })
+
+ describe('copyMigrationsToConfigDir', () => {
+ it('should copy migrations to the config directory', async () => {
+ // Arrange
+ const migrationManager = new MigrationManager(sequelizeStub, configPath)
+ migrationManager.migrationsDir = path.join(configPath, 'migrations')
+ const migrationsSourceDir = path.join(__dirname, '..', '..', '..', 'server', 'migrations')
+ const targetDir = migrationManager.migrationsDir
+ const files = ['migration1.js', 'migration2.js', 'readme.md']
+
+ const readdirStub = sinon.stub(fs, 'readdir').resolves(files)
+
+ // Act
+ 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
+ expect(fsCopyStub.calledWith(path.join(migrationsSourceDir, 'migration1.js'), path.join(targetDir, 'migration1.js'))).to.be.true
+ expect(fsCopyStub.calledWith(path.join(migrationsSourceDir, 'migration2.js'), path.join(targetDir, 'migration2.js'))).to.be.true
+ })
+
+ it('should throw an error if copying the migrations fails', async () => {
+ // Arrange
+ const migrationManager = new MigrationManager(sequelizeStub, configPath)
+ migrationManager.migrationsDir = path.join(configPath, 'migrations')
+ const migrationsSourceDir = path.join(__dirname, '..', '..', '..', 'server', 'migrations')
+ const targetDir = migrationManager.migrationsDir
+ const files = ['migration1.js', 'migration2.js', 'readme.md']
+
+ const readdirStub = sinon.stub(fs, 'readdir').resolves(files)
+ fsCopyStub.restore()
+ fsCopyStub = sinon.stub(fs, 'copy').rejects()
+
+ // Act
+ try {
+ // Act
+ await migrationManager.copyMigrationsToConfigDir()
+ expect.fail('Expected copyMigrationsToConfigDir to throw an error, but it did not.')
+ } 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
+ expect(fsCopyStub.calledWith(path.join(migrationsSourceDir, 'migration1.js'), path.join(targetDir, 'migration1.js'))).to.be.true
+ expect(fsCopyStub.calledWith(path.join(migrationsSourceDir, 'migration2.js'), path.join(targetDir, 'migration2.js'))).to.be.true
+ })
+ })
+
+ describe('findMigrationsToRun', () => {
+ it('should return migrations to run when direction is "up"', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = ['v1.0.0-migration.js']
+ migrationManager.databaseVersion = '1.0.0'
+ migrationManager.serverVersion = '1.2.0'
+ const direction = 'up'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal(['v1.1.0-migration.js', 'v1.2.0-migration.js'])
+ })
+
+ it('should return migrations to run when direction is "down"', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = ['v1.2.0-migration.js', 'v1.3.0-migration.js']
+ migrationManager.databaseVersion = '1.3.0'
+ migrationManager.serverVersion = '1.2.0'
+ const direction = 'down'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal(['v1.3.0-migration.js'])
+ })
+
+ it('should return empty array when no migrations to run up', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = ['v1.0.0-migration.js', 'v1.1.0-migration.js', 'v1.2.0-migration.js', 'v1.3.0-migration.js']
+ migrationManager.databaseVersion = '1.3.0'
+ migrationManager.serverVersion = '1.4.0'
+ const direction = 'up'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal([])
+ })
+
+ it('should return empty array when no migrations to run down', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = []
+ migrationManager.databaseVersion = '1.4.0'
+ migrationManager.serverVersion = '1.3.0'
+ const direction = 'down'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal([])
+ })
+
+ it('should return down migrations to run when direction is "down" and up migration was not executed', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = []
+ migrationManager.databaseVersion = '1.3.0'
+ migrationManager.serverVersion = '1.0.0'
+ const direction = 'down'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal(['v1.3.0-migration.js', 'v1.2.0-migration.js', 'v1.1.0-migration.js'])
+ })
+
+ it('should return empty array when direction is "down" and server version is higher than database version', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = ['v1.0.0-migration.js', 'v1.1.0-migration.js', 'v1.2.0-migration.js', 'v1.3.0-migration.js']
+ migrationManager.databaseVersion = '1.0.0'
+ migrationManager.serverVersion = '1.3.0'
+ const direction = 'down'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal([])
+ })
+
+ it('should return empty array when direction is "up" and server version is lower than database version', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = ['v1.0.0-migration.js', 'v1.1.0-migration.js', 'v1.2.0-migration.js', 'v1.3.0-migration.js']
+ migrationManager.databaseVersion = '1.3.0'
+ migrationManager.serverVersion = '1.0.0'
+ const direction = 'up'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal([])
+ })
+
+ it('should return up migrations to run when server version is between migrations', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = ['v1.0.0-migration.js', 'v1.1.0-migration.js']
+ migrationManager.databaseVersion = '1.1.0'
+ migrationManager.serverVersion = '1.2.3'
+ const direction = 'up'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal(['v1.2.0-migration.js'])
+ })
+
+ it('should return down migrations to run when server version is between migrations', () => {
+ // Arrange
+ const migrations = [{ name: 'v1.0.0-migration.js' }, { name: 'v1.1.0-migration.js' }, { name: 'v1.2.0-migration.js' }, { name: 'v1.3.0-migration.js' }]
+ const executedMigrations = ['v1.0.0-migration.js', 'v1.1.0-migration.js', 'v1.2.0-migration.js']
+ migrationManager.databaseVersion = '1.2.0'
+ migrationManager.serverVersion = '1.1.3'
+ const direction = 'down'
+
+ // Act
+ const result = migrationManager.findMigrationsToRun(migrations, executedMigrations, direction)
+
+ // Assert
+ expect(result).to.deep.equal(['v1.2.0-migration.js'])
+ })
+ })
+
+ describe('initUmzug', () => {
+ it('should initialize the umzug instance with migrations in the proper order', async () => {
+ // Arrange
+ const readdirStub = sinon.stub(fs, 'readdir').resolves(['v1.0.0-migration.js', 'v1.10.0-migration.js', 'v1.2.0-migration.js', 'v1.1.0-migration.js'])
+ const readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('module.exports = { up: () => {}, down: () => {} }')
+ const umzugStorage = memoryStorage()
+ migrationManager = new MigrationManager(sequelizeStub, configPath)
+ migrationManager.migrationsDir = path.join(configPath, 'migrations')
+ const resolvedMigrationNames = ['v1.0.0-migration.js', 'v1.1.0-migration.js', 'v1.2.0-migration.js', 'v1.10.0-migration.js']
+ const resolvedMigrationPaths = resolvedMigrationNames.map((name) => path.resolve(path.join(migrationManager.migrationsDir, name)))
+
+ // Act
+ await migrationManager.initUmzug(umzugStorage)
+
+ // Assert
+ expect(readdirStub.calledOnce).to.be.true
+ expect(migrationManager.umzug).to.be.an.instanceOf(Umzug)
+ const migrations = await migrationManager.umzug.migrations()
+ expect(migrations.map((m) => m.name)).to.deep.equal(resolvedMigrationNames)
+ expect(migrations.map((m) => m.path)).to.deep.equal(resolvedMigrationPaths)
+ })
+ })
+})
diff --git a/test/server/managers/migrations/v1.0.0-migration.js b/test/server/managers/migrations/v1.0.0-migration.js
new file mode 100644
index 0000000000..102c8ad550
--- /dev/null
+++ b/test/server/managers/migrations/v1.0.0-migration.js
@@ -0,0 +1,9 @@
+async function up() {
+ console.log('v1.0.0 up')
+}
+
+async function down() {
+ console.log('v1.0.0 down')
+}
+
+module.exports = { up, down }
diff --git a/test/server/managers/migrations/v1.1.0-migration.js b/test/server/managers/migrations/v1.1.0-migration.js
new file mode 100644
index 0000000000..c4c353b43b
--- /dev/null
+++ b/test/server/managers/migrations/v1.1.0-migration.js
@@ -0,0 +1,9 @@
+async function up() {
+ console.log('v1.1.0 up')
+}
+
+async function down() {
+ console.log('v1.1.0 down')
+}
+
+module.exports = { up, down }
diff --git a/test/server/managers/migrations/v1.10.0-migration.js b/test/server/managers/migrations/v1.10.0-migration.js
new file mode 100644
index 0000000000..8c853738ce
--- /dev/null
+++ b/test/server/managers/migrations/v1.10.0-migration.js
@@ -0,0 +1,9 @@
+async function up() {
+ console.log('v1.10.0 up')
+}
+
+async function down() {
+ console.log('v1.10.0 down')
+}
+
+module.exports = { up, down }
diff --git a/test/server/managers/migrations/v1.2.0-migration.js b/test/server/managers/migrations/v1.2.0-migration.js
new file mode 100644
index 0000000000..d6033d0557
--- /dev/null
+++ b/test/server/managers/migrations/v1.2.0-migration.js
@@ -0,0 +1,9 @@
+async function up() {
+ console.log('v1.2.0 up')
+}
+
+async function down() {
+ console.log('v1.2.0 down')
+}
+
+module.exports = { up, down }
diff --git a/test/server/migrations/v0.0.1-migration_example.js b/test/server/migrations/v0.0.1-migration_example.js
new file mode 100644
index 0000000000..5af66fc43c
--- /dev/null
+++ b/test/server/migrations/v0.0.1-migration_example.js
@@ -0,0 +1,50 @@
+const { DataTypes } = require('sequelize')
+
+/**
+ * @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 is an example of an upward migration script.
+ *
+ * @param {MigrationOptions} options - an object containing the migration context.
+ * @returns {Promise} - A promise that resolves when the migration is complete.
+ */
+async function up({ context: { queryInterface, logger } }) {
+ logger.info('Running migration_example up...')
+ logger.info('Creating example_table...')
+ await queryInterface.createTable('example_table', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false
+ }
+ })
+ logger.info('example_table created.')
+ logger.info('migration_example up complete.')
+}
+
+/**
+ * This is an example of a downward migration script.
+ *
+ * @param {MigrationOptions} options - an object containing the migration context.
+ * @returns {Promise} - A promise that resolves when the migration is complete.
+ */
+async function down({ context: { queryInterface, logger } }) {
+ logger.info('Running migration_example down...')
+ logger.info('Dropping example_table...')
+ await queryInterface.dropTable('example_table')
+ logger.info('example_table dropped.')
+ logger.info('migration_example down complete.')
+}
+
+module.exports = { up, down }
diff --git a/test/server/migrations/v0.0.1-migration_example.test.js b/test/server/migrations/v0.0.1-migration_example.test.js
new file mode 100644
index 0000000000..87300c1127
--- /dev/null
+++ b/test/server/migrations/v0.0.1-migration_example.test.js
@@ -0,0 +1,53 @@
+const { expect } = require('chai')
+const sinon = require('sinon')
+const { up, down } = require('./v0.0.1-migration_example')
+const { Sequelize } = require('sequelize')
+const Logger = require('../../../server/Logger')
+
+describe('migration_example', () => {
+ let sequelize
+ let queryInterface
+ let loggerInfoStub
+
+ beforeEach(() => {
+ sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
+ queryInterface = sequelize.getQueryInterface()
+ loggerInfoStub = sinon.stub(Logger, 'info')
+ })
+
+ afterEach(() => {
+ sinon.restore()
+ })
+
+ describe('up', () => {
+ it('should create example_table', async () => {
+ await up({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(4)
+ expect(loggerInfoStub.getCall(0).calledWith(sinon.match('Running migration_example up...'))).to.be.true
+ expect(loggerInfoStub.getCall(1).calledWith(sinon.match('Creating example_table...'))).to.be.true
+ expect(loggerInfoStub.getCall(2).calledWith(sinon.match('example_table created.'))).to.be.true
+ expect(loggerInfoStub.getCall(3).calledWith(sinon.match('migration_example up complete.'))).to.be.true
+ expect(await queryInterface.showAllTables()).to.include('example_table')
+ const tableDescription = await queryInterface.describeTable('example_table')
+ expect(tableDescription).to.deep.equal({
+ id: { type: 'INTEGER', allowNull: true, defaultValue: undefined, primaryKey: true, unique: false },
+ name: { type: 'VARCHAR(255)', allowNull: false, defaultValue: undefined, primaryKey: false, unique: false }
+ })
+ })
+ })
+
+ describe('down', () => {
+ it('should drop example_table', async () => {
+ await up({ context: { queryInterface, logger: Logger } })
+ await down({ context: { queryInterface, logger: Logger } })
+
+ expect(loggerInfoStub.callCount).to.equal(8)
+ expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Running migration_example down...'))).to.be.true
+ expect(loggerInfoStub.getCall(5).calledWith(sinon.match('Dropping example_table...'))).to.be.true
+ expect(loggerInfoStub.getCall(6).calledWith(sinon.match('example_table dropped.'))).to.be.true
+ expect(loggerInfoStub.getCall(7).calledWith(sinon.match('migration_example down complete.'))).to.be.true
+ expect(await queryInterface.showAllTables()).not.to.include('example_table')
+ })
+ })
+})