Merge remote-tracking branch 'remotes/upstream/master'

This commit is contained in:
Toni Barth 2025-02-24 07:52:53 +01:00
commit ee46db6329
29 changed files with 465 additions and 90 deletions

View file

@ -568,6 +568,18 @@ export default {
} }
} }
}, },
routeToBookshelfIfLastIssueRemoved() {
if (this.totalEntities === 0) {
const currentRouteQuery = this.$route.query
if (currentRouteQuery?.filter && currentRouteQuery.filter === 'issues') {
this.$nextTick(() => {
console.log('Last issue removed. Redirecting to library bookshelf')
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
})
}
}
},
libraryItemRemoved(libraryItem) { libraryItemRemoved(libraryItem) {
if (this.entityName === 'items' || this.entityName === 'series-books') { if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id) var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
@ -578,6 +590,7 @@ export default {
this.executeRebuild() this.executeRebuild()
} }
} }
this.routeToBookshelfIfLastIssueRemoved()
}, },
libraryItemsAdded(libraryItems) { libraryItemsAdded(libraryItems) {
console.log('items added', libraryItems) console.log('items added', libraryItems)

View file

@ -13,7 +13,7 @@
</div> </div>
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5"> <div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
<span class="material-symbols text-sm">person</span> <span class="material-symbols text-sm">person</span>
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div> <div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">{{ podcastAuthor }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate"> <div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div> </div>

View file

@ -5,8 +5,8 @@
<ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center"> <ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center">
<widgets-loading-spinner /> <widgets-loading-spinner />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-else text="Activities" direction="bottom" class="flex items-center"> <ui-tooltip v-else :text="$strings.LabelActivities" direction="bottom" class="flex items-center">
<span class="material-symbols text-1.5xl" aria-label="Activities" role="button">notifications</span> <span class="material-symbols text-1.5xl" :aria-label="$strings.LabelActivities" role="button">notifications</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" /> <div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" />

View file

@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.19.4", "version": "2.19.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.19.4", "version": "2.19.5",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",

View file

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.19.4", "version": "2.19.5",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",

View file

@ -122,7 +122,7 @@ export default {
}, },
scheduleDescription() { scheduleDescription() {
if (!this.cronExpression) return '' if (!this.cronExpression) return ''
const parsed = this.$parseCronExpression(this.cronExpression) const parsed = this.$parseCronExpression(this.cronExpression, this)
return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}` return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}`
}, },
nextBackupDate() { nextBackupDate() {

View file

@ -67,7 +67,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
</div> </div>
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2"> <div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" /> <ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" :label="$strings.LabelCoverProvider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div> </div>
<div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">

View file

@ -107,6 +107,19 @@ Vue.prototype.$formatNumber = (num) => {
return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num) return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num)
} }
/**
* Get the days of the week for the current language
* Starts with Sunday
* @returns {string[]}
*/
Vue.prototype.$getDaysOfWeek = () => {
const days = []
for (let i = 0; i < 7; i++) {
days.push(new Date(2025, 0, 5 + i).toLocaleString(Vue.prototype.$languageCodes.current, { weekday: 'long' }))
}
return days
}
const translations = { const translations = {
[defaultCode]: enUsStrings [defaultCode]: enUsStrings
} }
@ -148,6 +161,7 @@ async function loadi18n(code) {
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale) Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
this?.$eventBus?.$emit('change-lang', code) this?.$eventBus?.$emit('change-lang', code)
return true return true
} }

View file

@ -93,7 +93,7 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = t
return strs.join(' ') return strs.join(' ')
} }
Vue.prototype.$parseCronExpression = (expression) => { Vue.prototype.$parseCronExpression = (expression, context) => {
if (!expression) return null if (!expression) return null
const pieces = expression.split(' ') const pieces = expression.split(' ')
if (pieces.length !== 5) { if (pieces.length !== 5) {
@ -102,31 +102,31 @@ Vue.prototype.$parseCronExpression = (expression) => {
const commonPatterns = [ const commonPatterns = [
{ {
text: 'Every 12 hours', text: context.$strings.LabelIntervalEvery12Hours,
value: '0 */12 * * *' value: '0 */12 * * *'
}, },
{ {
text: 'Every 6 hours', text: context.$strings.LabelIntervalEvery6Hours,
value: '0 */6 * * *' value: '0 */6 * * *'
}, },
{ {
text: 'Every 2 hours', text: context.$strings.LabelIntervalEvery2Hours,
value: '0 */2 * * *' value: '0 */2 * * *'
}, },
{ {
text: 'Every hour', text: context.$strings.LabelIntervalEveryHour,
value: '0 * * * *' value: '0 * * * *'
}, },
{ {
text: 'Every 30 minutes', text: context.$strings.LabelIntervalEvery30Minutes,
value: '*/30 * * * *' value: '*/30 * * * *'
}, },
{ {
text: 'Every 15 minutes', text: context.$strings.LabelIntervalEvery15Minutes,
value: '*/15 * * * *' value: '*/15 * * * *'
}, },
{ {
text: 'Every minute', text: context.$strings.LabelIntervalEveryMinute,
value: '* * * * *' value: '* * * * *'
} }
] ]
@ -147,7 +147,7 @@ Vue.prototype.$parseCronExpression = (expression) => {
return null return null
} }
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] const weekdays = context.$getDaysOfWeek()
var weekdayText = 'day' var weekdayText = 'day'
if (pieces[4] !== '*') if (pieces[4] !== '*')
weekdayText = pieces[4] weekdayText = pieces[4]
@ -156,7 +156,7 @@ Vue.prototype.$parseCronExpression = (expression) => {
.join(', ') .join(', ')
return { return {
description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}` description: context.$getString('MessageScheduleRunEveryWeekdayAtTime', [weekdayText, `${pieces[1]}:${pieces[0].padStart(2, '0')}`])
} }
} }

View file

@ -43,7 +43,7 @@
"ButtonLatest": "Апошняе", "ButtonLatest": "Апошняе",
"ButtonLibrary": "Бібліятэка", "ButtonLibrary": "Бібліятэка",
"ButtonLogout": "Выйсці", "ButtonLogout": "Выйсці",
"ButtonLookup": "", "ButtonLookup": "Пошук",
"ButtonManageTracks": "Кіраванне дарожкамі", "ButtonManageTracks": "Кіраванне дарожкамі",
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў", "ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў", "ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
@ -159,6 +159,15 @@
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне", "HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
"HeaderNotifications": "Апавяшчэнні", "HeaderNotifications": "Апавяшчэнні",
"HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання", "HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання",
"HeaderOpenRSSFeed": "Адкрыць RSS-стужку",
"HeaderPlaylist": "Плэйліст",
"HeaderPlaylistItems": "Элементы плэйліста",
"HeaderRSSFeedGeneral": "Падрабязнасці RSS",
"HeaderRSSFeedIsOpen": "RSS-стужка адкрыта",
"HeaderRSSFeeds": "RSS-стужкі",
"HeaderRemoveEpisode": "Выдаліць эпізод",
"HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў",
"HeaderSavedMediaProgress": "Захаваны прагрэс медыя",
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў", "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў",
"HeaderSettings": "Налады", "HeaderSettings": "Налады",
"HeaderSettingsDisplay": "Дысплей", "HeaderSettingsDisplay": "Дысплей",
@ -166,50 +175,167 @@
"HeaderSettingsGeneral": "Агульныя", "HeaderSettingsGeneral": "Агульныя",
"HeaderSettingsScanner": "Сканер", "HeaderSettingsScanner": "Сканер",
"HeaderSettingsWebClient": "Вэб-кліент", "HeaderSettingsWebClient": "Вэб-кліент",
"HeaderSleepTimer": "Таймер сну",
"HeaderStatsMinutesListeningChart": "Хвіліны праслухоўвання (апошнія 7 дзён)",
"HeaderStatsTop10Authors": "10 лепшых аўтараў", "HeaderStatsTop10Authors": "10 лепшых аўтараў",
"HeaderStatsTop5Genres": "5 лепшых жанраў", "HeaderStatsTop5Genres": "5 лепшых жанраў",
"HeaderTableOfContents": "Змест", "HeaderTableOfContents": "Змест",
"HeaderTools": "Інструменты", "HeaderTools": "Інструменты",
"HeaderUpdateAccount": "Абнавіць уліковы запіс", "HeaderUpdateAccount": "Абнавіць уліковы запіс",
"HeaderYourStats": "Ваша статыстыка",
"LabelAccountType": "Тып уліковага запіса", "LabelAccountType": "Тып уліковага запіса",
"LabelAccountTypeAdmin": "Адміністратар", "LabelAccountTypeAdmin": "Адміністратар",
"LabelAccountTypeGuest": "Госць", "LabelAccountTypeGuest": "Госць",
"LabelAccountTypeUser": "Карыстальнік", "LabelAccountTypeUser": "Карыстальнік",
"LabelAddToPlaylist": "Дадаць у плэйліст",
"LabelAddedDate": "Дададзена {0}",
"LabelAll": "Усе",
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)", "LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
"LabelAudioChannels": "Аўдыёканалы (1 або 2)", "LabelAudioChannels": "Аўдыёканалы (1 або 2)",
"LabelAudioCodec": "Аўдыёкодэк", "LabelAudioCodec": "Аўдыёкодэк",
"LabelAuthor": "Аўтар",
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)",
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)",
"LabelAuthors": "Аўтары",
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", "LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў", "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
"LabelBooks": "Кнігі",
"LabelChapters": "Раздзелы",
"LabelClosePlayer": "Зачыніць прайгравальнік",
"LabelCollapseSeries": "Згарнуць серыі",
"LabelComplete": "Завершана",
"LabelContinueListening": "Працягваць слухаць", "LabelContinueListening": "Працягваць слухаць",
"LabelContinueReading": "Працягнуць чытанне",
"LabelContinueSeries": "Працягнуць серыі",
"LabelDescription": "Апісанне",
"LabelDiscover": "Знайсці",
"LabelDownload": "Спампаваць", "LabelDownload": "Спампаваць",
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў", "LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
"LabelDownloadable": "Спампоўваецца", "LabelDownloadable": "Спампоўваецца",
"LabelDuration": "Працягласць",
"LabelEbook": "Электронная кніга",
"LabelEbooks": "Электронныя кнігі",
"LabelEnable": "Уключыць",
"LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:", "LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:",
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.", "LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:", "LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
"LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.", "LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
"LabelEnd": "Канец",
"LabelEndOfChapter": "Канец раздзела",
"LabelEpisode": "Эпізод",
"LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай",
"LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі",
"LabelFeedURL": "URL стужкі",
"LabelFile": "Файл",
"LabelFileBirthtime": "Час стварэння файла",
"LabelFileModified": "Час змянення файла",
"LabelFilename": "Імя файла",
"LabelFinished": "Скончана",
"LabelFolder": "Тэчка",
"LabelFontBoldness": "Таўшчыня шрыфта",
"LabelFontScale": "Памер шрыфту",
"LabelGenre": "Жанр",
"LabelGenres": "Жанры",
"LabelHasEbook": "Мае электронную кнігу",
"LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу",
"LabelHost": "Хост",
"LabelInProgress": "У працэсе",
"LabelIncomplete": "Незавершана",
"LabelLanguage": "Мова",
"LabelLayoutSinglePage": "Аднабаковы",
"LabelLineSpacing": "Міжрадковы інтэрвал",
"LabelListenAgain": "Паслухаць зноў",
"LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.", "LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.",
"LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку", "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку",
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.", "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
"LabelMediaPlayer": "Медыяплэер",
"LabelMediaType": "Тып медыя",
"LabelMissing": "Адсутнічае",
"LabelMore": "Больш",
"LabelMoreInfo": "Больш інфармацыі",
"LabelName": "Імя",
"LabelNarrator": "Чытальнік",
"LabelNarrators": "Чытальнікі",
"LabelOpenRSSFeed": "Адкрыць RSS-стужку",
"LabelPermissionsDownload": "Можна спампаваць", "LabelPermissionsDownload": "Можна спампаваць",
"LabelPreventIndexing": "Прадухіліць індэксацыю вашай стужкі каталогамі падкастаў iTunes і Google",
"LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка",
"LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка",
"LabelRSSFeedOpen": "RSS-стужка адкрытая",
"LabelRSSFeedPreventIndexing": "Прадухіліць індэксацыю",
"LabelRSSFeedURL": "URL RSS-стужкі",
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць", "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць",
"LabelRecentSeries": "Апошнія серыі",
"LabelSeries": "Серыі",
"LabelSetEbookAsPrimary": "Зрабіць асноўным",
"LabelSetEbookAsSupplementary": "Зрабіць дадатковым",
"LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.",
"LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як скончаны, калі",
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.", "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
"LabelShowAll": "Паказаць усё",
"LabelSize": "Памер",
"LabelStatsAudioTracks": "Аўдыядарожкі", "LabelStatsAudioTracks": "Аўдыядарожкі",
"LabelTracks": "Дарожкі", "LabelTracks": "Дарожкі",
"MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак",
"MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?",
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?", "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
"MessageDownloadingEpisode": "Спампоўка эпізоду", "MessageDownloadingEpisode": "Спампоўка эпізоду",
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", "MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
"MessageFeedURLWillBe": "URL стужкі будзе {0}",
"MessageNoChapters": "Няма раздзелаў",
"MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак", "MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак",
"MessageNoDownloadsQueued": "Няма спамповак у чарзе", "MessageNoDownloadsQueued": "Няма спамповак у чарзе",
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання", "MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
"MessageNoMediaProgress": "Няма прагрэсу медыя",
"MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі",
"MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага OPML-файла. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.",
"MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення",
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"", "MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
"MessageTaskOpmlImportDescription": "Стварэнне падкастаў з {0} RSS-стужак",
"MessageTaskOpmlImportFeed": "Імпарт стужкі з OPML",
"MessageTaskOpmlImportFeedDescription": "Імпартаванне RSS-стужкі \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Не ўдалося атрымаць стужку падкаста",
"MessageTaskOpmlImportFeedPodcastDescription": "Стварэнне падкаста \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху",
"MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст",
"MessageTaskOpmlParseNoneFound": "У OPML-файле не знойдзена стужак",
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
"NoteUploaderFoldersWithMediaFiles": "Тэчкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.",
"NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца", "NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца",
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены", "ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана", "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі", "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
"ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана",
"ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як Завершаны",
"ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае",
"ToastItemMarkedAsNotFinishedSuccess": "Элемент пазначаны як Незавершаны",
"ToastItemUpdateSuccess": "Элемент абноўлены",
"ToastLibraryCreateFailed": "Не ўдалося стварыць бібліятэку",
"ToastLibraryCreateSuccess": "Бібліятэка \"{0}\" створана",
"ToastLibraryDeleteFailed": "Не ўдалося выдаліць бібліятэку",
"ToastLibraryDeleteSuccess": "Бібліятэка выдалена",
"ToastLibraryScanFailedToStart": "Не ўдалося запусціць сканаванне",
"ToastLibraryScanStarted": "Сканаванне бібліятэкі запушчана",
"ToastLibraryUpdateSuccess": "Бібліятэка \"{0}\" абноўлена",
"ToastMatchAllAuthorsFailed": "Не ўдалося знайсці адпаведнасць для ўсіх аўтараў",
"ToastMetadataFilesRemovedError": "Памылка пры выдаленні metadata.{0} файлаў",
"ToastMetadataFilesRemovedNoneFound": "У бібліятэцы не знойдзены metadata.{0} файлаў",
"ToastMetadataFilesRemovedNoneRemoved": "Не выдалена metadata.{0} файлаў",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} файлаў выдалена",
"ToastMustHaveAtLeastOnePath": "Павінен быць хаця б адзін шлях",
"ToastNameEmailRequired": "Імя і электронная пошта абавязковыя",
"ToastNameRequired": "Імя абавязковае",
"ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"", "ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"",
"ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны", "ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны",
"ToastNoRSSFeed": "У падкаста няма RSS-стужкі",
"ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста",
"ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў",
"ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі",
"ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку",
"ToastRSSFeedCloseSuccess": "RSS-стужка закрыта",
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым", "ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара" "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
} }

View file

@ -217,6 +217,7 @@
"LabelAccountTypeAdmin": "Správce", "LabelAccountTypeAdmin": "Správce",
"LabelAccountTypeGuest": "Host", "LabelAccountTypeGuest": "Host",
"LabelAccountTypeUser": "Uživatel", "LabelAccountTypeUser": "Uživatel",
"LabelActivities": "Aktivity",
"LabelActivity": "Aktivita", "LabelActivity": "Aktivita",
"LabelAddToCollection": "Přidat do kolekce", "LabelAddToCollection": "Přidat do kolekce",
"LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce", "LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce",
@ -389,6 +390,7 @@
"LabelIntervalEvery6Hours": "Každých 6 hodin", "LabelIntervalEvery6Hours": "Každých 6 hodin",
"LabelIntervalEveryDay": "Každý den", "LabelIntervalEveryDay": "Každý den",
"LabelIntervalEveryHour": "Každou hodinu", "LabelIntervalEveryHour": "Každou hodinu",
"LabelIntervalEveryMinute": "Každou minutu",
"LabelInvert": "Invertovat", "LabelInvert": "Invertovat",
"LabelItem": "Položka", "LabelItem": "Položka",
"LabelJumpBackwardAmount": "Přeskočit zpět o", "LabelJumpBackwardAmount": "Přeskočit zpět o",
@ -484,6 +486,7 @@
"LabelPersonalYearReview": "Váš přehled roku ({0})", "LabelPersonalYearReview": "Váš přehled roku ({0})",
"LabelPhotoPathURL": "Cesta k fotografii/URL", "LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlayMethod": "Metoda přehrávání", "LabelPlayMethod": "Metoda přehrávání",
"LabelPlaybackRateIncrementDecrement": "Velikost kroku pro změnu rychlosti přehrávání",
"LabelPlayerChapterNumberMarker": "{0} z {1}", "LabelPlayerChapterNumberMarker": "{0} z {1}",
"LabelPlaylists": "Seznamy skladeb", "LabelPlaylists": "Seznamy skladeb",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
@ -706,6 +709,7 @@
"MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné", "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.", "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", "MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
"MessageBookshelfNoCollectionsHelp": "Kolekce jsou veřejné. Mohou je zobrazit všichni uživatelé s přístupem do knihovny.",
"MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály", "MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály",
"MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"", "MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Žádné výsledky pro dotaz", "MessageBookshelfNoResultsForQuery": "Žádné výsledky pro dotaz",
@ -816,6 +820,7 @@
"MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy", "MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace", "MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb", "MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessageNoUserPlaylistsHelp": "Seznamy skladeb jsou soukromé. Zobrazit je může pouze uživatel, který je vytvořil.",
"MessageNotYetImplemented": "Ještě není implementováno", "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.", "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", "MessageOr": "nebo",

View file

@ -219,7 +219,8 @@
"LabelAccountTypeAdmin": "Admin", "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Gast", "LabelAccountTypeGuest": "Gast",
"LabelAccountTypeUser": "Benutzer", "LabelAccountTypeUser": "Benutzer",
"LabelActivity": "Aktivitäten", "LabelActivities": "Aktivitäten",
"LabelActivity": "Aktivität",
"LabelAddToCollection": "Zur Sammlung hinzufügen", "LabelAddToCollection": "Zur Sammlung hinzufügen",
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
@ -283,6 +284,7 @@
"LabelContinueSeries": "Serien fortsetzen", "LabelContinueSeries": "Serien fortsetzen",
"LabelCover": "Titelbild", "LabelCover": "Titelbild",
"LabelCoverImageURL": "URL des Titelbildes", "LabelCoverImageURL": "URL des Titelbildes",
"LabelCoverProvider": "Titelbildanbieter",
"LabelCreatedAt": "Erstellt am", "LabelCreatedAt": "Erstellt am",
"LabelCronExpression": "Cron-Ausdruck", "LabelCronExpression": "Cron-Ausdruck",
"LabelCurrent": "Aktuell", "LabelCurrent": "Aktuell",
@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Alle 6 Stunden", "LabelIntervalEvery6Hours": "Alle 6 Stunden",
"LabelIntervalEveryDay": "Jeden Tag", "LabelIntervalEveryDay": "Jeden Tag",
"LabelIntervalEveryHour": "Jede Stunde", "LabelIntervalEveryHour": "Jede Stunde",
"LabelIntervalEveryMinute": "Jede Minute",
"LabelInvert": "Umkehren", "LabelInvert": "Umkehren",
"LabelItem": "Medium", "LabelItem": "Medium",
"LabelJumpBackwardAmount": "Zurückspringen Zeit", "LabelJumpBackwardAmount": "Zurückspringen Zeit",
@ -844,6 +847,7 @@
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am", "MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am",
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.", "MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
"MessageScheduleLibraryScanNote": "Für die meisten Nutzer wird empfohlen, diese Funktion deaktiviert zu lassen und stattdessen die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung erkennt automatisch Änderungen in deinen Bibliotheksordnern. Da die Ordnerüberwachung jedoch nicht mit jedem Dateisystem (z.B. NFS) funktioniert, können alternativ hier geplante Bibliotheks-Scans aktiviert werden.", "MessageScheduleLibraryScanNote": "Für die meisten Nutzer wird empfohlen, diese Funktion deaktiviert zu lassen und stattdessen die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung erkennt automatisch Änderungen in deinen Bibliotheksordnern. Da die Ordnerüberwachung jedoch nicht mit jedem Dateisystem (z.B. NFS) funktioniert, können alternativ hier geplante Bibliotheks-Scans aktiviert werden.",
"MessageScheduleRunEveryWeekdayAtTime": "Immer {0} um {1} ausführen",
"MessageSearchResultsFor": "Suchergebnisse für", "MessageSearchResultsFor": "Suchergebnisse für",
"MessageSelected": "{0} ausgewählt", "MessageSelected": "{0} ausgewählt",
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden", "MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",

View file

@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Admin", "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Guest", "LabelAccountTypeGuest": "Guest",
"LabelAccountTypeUser": "User", "LabelAccountTypeUser": "User",
"LabelActivities": "Activities",
"LabelActivity": "Activity", "LabelActivity": "Activity",
"LabelAddToCollection": "Add to Collection", "LabelAddToCollection": "Add to Collection",
"LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection",
@ -283,6 +284,7 @@
"LabelContinueSeries": "Continue Series", "LabelContinueSeries": "Continue Series",
"LabelCover": "Cover", "LabelCover": "Cover",
"LabelCoverImageURL": "Cover Image URL", "LabelCoverImageURL": "Cover Image URL",
"LabelCoverProvider": "Cover Provider",
"LabelCreatedAt": "Created At", "LabelCreatedAt": "Created At",
"LabelCronExpression": "Cron Expression", "LabelCronExpression": "Cron Expression",
"LabelCurrent": "Current", "LabelCurrent": "Current",
@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEvery6Hours": "Every 6 hours",
"LabelIntervalEveryDay": "Every day", "LabelIntervalEveryDay": "Every day",
"LabelIntervalEveryHour": "Every hour", "LabelIntervalEveryHour": "Every hour",
"LabelIntervalEveryMinute": "Every minute",
"LabelInvert": "Invert", "LabelInvert": "Invert",
"LabelItem": "Item", "LabelItem": "Item",
"LabelJumpBackwardAmount": "Jump backward amount", "LabelJumpBackwardAmount": "Jump backward amount",
@ -845,6 +848,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "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.<br /><br />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.<br /><br />All clients using your server will be automatically refreshed.", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />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.<br /><br />All clients using your server will be automatically refreshed.",
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.", "MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.",
"MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}",
"MessageSearchResultsFor": "Search results for", "MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected", "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached", "MessageServerCouldNotBeReached": "Server could not be reached",

View file

@ -707,7 +707,7 @@
"MessageBackupsLocationEditNote": "Remarque: Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes", "MessageBackupsLocationEditNote": "Remarque: Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
"MessageBackupsLocationNoEditNote": "Remarque: lemplacement de sauvegarde est défini via une variable denvironnement et ne peut pas être modifié ici.", "MessageBackupsLocationNoEditNote": "Remarque: lemplacement de sauvegarde est défini via une variable denvironnement et ne peut pas être modifié ici.",
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide", "MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
"MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. les champs avec des valeurs multiples seront fusionnés", "MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. Les champs avec des valeurs multiples seront fusionnés.",
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance décraser les couvertures et/ou métadonnées existantes.", "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance décraser les couvertures et/ou métadonnées existantes.",
"MessageBookshelfNoCollections": "Vous navez pas encore de collections", "MessageBookshelfNoCollections": "Vous navez pas encore de collections",
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert", "MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert",

View file

@ -16,7 +16,7 @@
"ButtonCancel": "Odustani", "ButtonCancel": "Odustani",
"ButtonCancelEncode": "Otkaži kodiranje", "ButtonCancelEncode": "Otkaži kodiranje",
"ButtonChangeRootPassword": "Promijeni zaporku root korisnika", "ButtonChangeRootPassword": "Promijeni zaporku root korisnika",
"ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove epizode", "ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove nastavke",
"ButtonChooseAFolder": "Odaberi mapu", "ButtonChooseAFolder": "Odaberi mapu",
"ButtonChooseFiles": "Odaberi datoteke", "ButtonChooseFiles": "Odaberi datoteke",
"ButtonClearFilter": "Poništi filter", "ButtonClearFilter": "Poništi filter",
@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Administrator", "LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gost", "LabelAccountTypeGuest": "Gost",
"LabelAccountTypeUser": "Korisnik", "LabelAccountTypeUser": "Korisnik",
"LabelActivities": "Aktivnosti",
"LabelActivity": "Aktivnost", "LabelActivity": "Aktivnost",
"LabelAddToCollection": "Dodaj u zbirku", "LabelAddToCollection": "Dodaj u zbirku",
"LabelAddToCollectionBatch": "Dodaj {0} knjiga u zbirku", "LabelAddToCollectionBatch": "Dodaj {0} knjiga u zbirku",
@ -283,6 +284,7 @@
"LabelContinueSeries": "Nastavi serijal", "LabelContinueSeries": "Nastavi serijal",
"LabelCover": "Naslovnica", "LabelCover": "Naslovnica",
"LabelCoverImageURL": "URL naslovnice", "LabelCoverImageURL": "URL naslovnice",
"LabelCoverProvider": "Pružatelj naslovnica",
"LabelCreatedAt": "Izrađen", "LabelCreatedAt": "Izrađen",
"LabelCronExpression": "Cron izraz", "LabelCronExpression": "Cron izraz",
"LabelCurrent": "Trenutan", "LabelCurrent": "Trenutan",
@ -355,7 +357,7 @@
"LabelFileModifiedDate": "Izmijenjeno {0}", "LabelFileModifiedDate": "Izmijenjeno {0}",
"LabelFilename": "Naziv datoteke", "LabelFilename": "Naziv datoteke",
"LabelFilterByUser": "Filtriraj po korisniku", "LabelFilterByUser": "Filtriraj po korisniku",
"LabelFindEpisodes": "Pronađi epizode", "LabelFindEpisodes": "Pronađi nastavke",
"LabelFinished": "Dovršeno", "LabelFinished": "Dovršeno",
"LabelFolder": "Mapa", "LabelFolder": "Mapa",
"LabelFolders": "Mape", "LabelFolders": "Mape",
@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Svakih 6 sati", "LabelIntervalEvery6Hours": "Svakih 6 sati",
"LabelIntervalEveryDay": "Svaki dan", "LabelIntervalEveryDay": "Svaki dan",
"LabelIntervalEveryHour": "Svaki sat", "LabelIntervalEveryHour": "Svaki sat",
"LabelIntervalEveryMinute": "Svaku minutu",
"LabelInvert": "Obrni", "LabelInvert": "Obrni",
"LabelItem": "Stavka", "LabelItem": "Stavka",
"LabelJumpBackwardAmount": "Dužina skoka unatrag", "LabelJumpBackwardAmount": "Dužina skoka unatrag",
@ -400,8 +403,8 @@
"LabelLanguages": "Jezici", "LabelLanguages": "Jezici",
"LabelLastBookAdded": "Zadnja dodana knjiga", "LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja ažurirana knjiga", "LabelLastBookUpdated": "Zadnja ažurirana knjiga",
"LabelLastSeen": "Zadnji puta viđen", "LabelLastSeen": "Zadnje gledano",
"LabelLastTime": "Zadnje vrijeme", "LabelLastTime": "Vrijeme zadnjeg slušanja",
"LabelLastUpdate": "Zadnje ažuriranje", "LabelLastUpdate": "Zadnje ažuriranje",
"LabelLayout": "Prikaz", "LabelLayout": "Prikaz",
"LabelLayoutSinglePage": "Jedna stranica", "LabelLayoutSinglePage": "Jedna stranica",
@ -418,7 +421,7 @@
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", "LabelLookForNewEpisodesAfterDate": "Traži nove nastavke nakon ovog datuma",
"LabelLowestPriority": "Najniži prioritet", "LabelLowestPriority": "Najniži prioritet",
"LabelMatchExistingUsersBy": "Prepoznaj postojeće korisnike pomoću", "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", "LabelMatchExistingUsersByDescription": "Rabi se za povezivanje postojećih korisnika. Nakon što se spoje, korisnike se prepoznaje temeljem jedinstvene oznake vašeg pružatelja SSO usluga",
@ -447,7 +450,7 @@
"LabelNew": "Novo", "LabelNew": "Novo",
"LabelNewPassword": "Nova zaporka", "LabelNewPassword": "Nova zaporka",
"LabelNewestAuthors": "Najnoviji autori", "LabelNewestAuthors": "Najnoviji autori",
"LabelNewestEpisodes": "Najnovije epizode", "LabelNewestEpisodes": "Najnoviji nastavci",
"LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije", "LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije",
"LabelNextScheduledRun": "Sljedeće zakazano izvođenje", "LabelNextScheduledRun": "Sljedeće zakazano izvođenje",
"LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka", "LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka",
@ -845,6 +848,7 @@
"MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu", "MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu",
"MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.<br /><br />Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati. <br /><br />Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.", "MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.<br /><br />Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati. <br /><br />Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.",
"MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom i ostaviti postavku promatrača mape aktiviranom. Promatrač mapa će automatski otkriti promjene u mapama vaše knjižnice. Promatrač mapa ne radi na svakom datotečnom sustavu (kao što je NFS) pa se umjesto njega mogu koristiti planirana pretraživanja knjižnice.", "MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom i ostaviti postavku promatrača mape aktiviranom. Promatrač mapa će automatski otkriti promjene u mapama vaše knjižnice. Promatrač mapa ne radi na svakom datotečnom sustavu (kao što je NFS) pa se umjesto njega mogu koristiti planirana pretraživanja knjižnice.",
"MessageScheduleRunEveryWeekdayAtTime": "Pokreni svaki {0} u {1}",
"MessageSearchResultsFor": "Rezultati pretrage za", "MessageSearchResultsFor": "Rezultati pretrage za",
"MessageSelected": "{0} odabrano", "MessageSelected": "{0} odabrano",
"MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju", "MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju",

View file

@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Amministratore", "LabelAccountTypeAdmin": "Amministratore",
"LabelAccountTypeGuest": "Ospite", "LabelAccountTypeGuest": "Ospite",
"LabelAccountTypeUser": "Utente", "LabelAccountTypeUser": "Utente",
"LabelActivities": "Attività",
"LabelActivity": "Attività", "LabelActivity": "Attività",
"LabelAddToCollection": "Aggiungi alla Raccolta", "LabelAddToCollection": "Aggiungi alla Raccolta",
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta", "LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
@ -283,6 +284,7 @@
"LabelContinueSeries": "Continua serie", "LabelContinueSeries": "Continua serie",
"LabelCover": "Copertina", "LabelCover": "Copertina",
"LabelCoverImageURL": "Indirizzo della cover URL", "LabelCoverImageURL": "Indirizzo della cover URL",
"LabelCoverProvider": "Cover Provider",
"LabelCreatedAt": "Creato A", "LabelCreatedAt": "Creato A",
"LabelCronExpression": "Espressione Cron", "LabelCronExpression": "Espressione Cron",
"LabelCurrent": "Attuale", "LabelCurrent": "Attuale",
@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Ogni 6 ore", "LabelIntervalEvery6Hours": "Ogni 6 ore",
"LabelIntervalEveryDay": "Ogni Giorno", "LabelIntervalEveryDay": "Ogni Giorno",
"LabelIntervalEveryHour": "Ogni ora", "LabelIntervalEveryHour": "Ogni ora",
"LabelIntervalEveryMinute": "Ogni minuto",
"LabelInvert": "Inverti", "LabelInvert": "Inverti",
"LabelItem": "Oggetti", "LabelItem": "Oggetti",
"LabelJumpBackwardAmount": "secondi di avvolgimento", "LabelJumpBackwardAmount": "secondi di avvolgimento",

View file

@ -10,6 +10,8 @@
"ButtonApplyChapters": "Zatwierdź rozdziały", "ButtonApplyChapters": "Zatwierdź rozdziały",
"ButtonAuthors": "Autorzy", "ButtonAuthors": "Autorzy",
"ButtonBack": "Wstecz", "ButtonBack": "Wstecz",
"ButtonBatchEditPopulateFromExisting": "Powiel z poprzednich",
"ButtonBatchEditPopulateMapDetails": "Powiel szczegóły mapy",
"ButtonBrowseForFolder": "Wyszukaj folder", "ButtonBrowseForFolder": "Wyszukaj folder",
"ButtonCancel": "Anuluj", "ButtonCancel": "Anuluj",
"ButtonCancelEncode": "Anuluj enkodowanie", "ButtonCancelEncode": "Anuluj enkodowanie",
@ -31,6 +33,7 @@
"ButtonEditPodcast": "Edytuj podcast", "ButtonEditPodcast": "Edytuj podcast",
"ButtonEnable": "Włącz", "ButtonEnable": "Włącz",
"ButtonFireAndFail": "Fail start", "ButtonFireAndFail": "Fail start",
"ButtonFireOnTest": "Uruchom po zdarzeniu testowym",
"ButtonForceReScan": "Wymuś ponowne skanowanie", "ButtonForceReScan": "Wymuś ponowne skanowanie",
"ButtonFullPath": "Pełna ścieżka", "ButtonFullPath": "Pełna ścieżka",
"ButtonHide": "Ukryj", "ButtonHide": "Ukryj",
@ -87,6 +90,8 @@
"ButtonSaveTracklist": "Zapisz listę odtwarzania", "ButtonSaveTracklist": "Zapisz listę odtwarzania",
"ButtonScan": "Zeskanuj", "ButtonScan": "Zeskanuj",
"ButtonScanLibrary": "Skanuj bibliotekę", "ButtonScanLibrary": "Skanuj bibliotekę",
"ButtonScrollLeft": "Przewiń w lewo",
"ButtonScrollRight": "Przewiń w prawo",
"ButtonSearch": "Szukaj", "ButtonSearch": "Szukaj",
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu", "ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
"ButtonSeries": "Seria", "ButtonSeries": "Seria",
@ -155,13 +160,14 @@
"HeaderMapDetails": "Szczegóły mapowania", "HeaderMapDetails": "Szczegóły mapowania",
"HeaderMatch": "Dopasuj", "HeaderMatch": "Dopasuj",
"HeaderMetadataOrderOfPrecedence": "Kolejność metadanych", "HeaderMetadataOrderOfPrecedence": "Kolejność metadanych",
"HeaderMetadataToEmbed": "Osadź metadane", "HeaderMetadataToEmbed": "Metadane do osadzenia",
"HeaderNewAccount": "Nowe konto", "HeaderNewAccount": "Nowe konto",
"HeaderNewLibrary": "Nowa biblioteka", "HeaderNewLibrary": "Nowa biblioteka",
"HeaderNotificationCreate": "Utwórz powiadomienie", "HeaderNotificationCreate": "Utwórz powiadomienie",
"HeaderNotificationUpdate": "Zaktualizuj powiadomienie", "HeaderNotificationUpdate": "Zaktualizuj powiadomienie",
"HeaderNotifications": "Powiadomienia", "HeaderNotifications": "Powiadomienia",
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect", "HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
"HeaderOpenListeningSessions": "Otwarte sesje słuchania",
"HeaderOpenRSSFeed": "Utwórz kanał RSS", "HeaderOpenRSSFeed": "Utwórz kanał RSS",
"HeaderOtherFiles": "Inne pliki", "HeaderOtherFiles": "Inne pliki",
"HeaderPasswordAuthentication": "Uwierzytelnianie hasłem", "HeaderPasswordAuthentication": "Uwierzytelnianie hasłem",
@ -188,6 +194,7 @@
"HeaderSettingsExperimental": "Funkcje eksperymentalne", "HeaderSettingsExperimental": "Funkcje eksperymentalne",
"HeaderSettingsGeneral": "Ogólne", "HeaderSettingsGeneral": "Ogólne",
"HeaderSettingsScanner": "Skanowanie", "HeaderSettingsScanner": "Skanowanie",
"HeaderSettingsWebClient": "Klient webowy",
"HeaderSleepTimer": "Wyłącznik czasowy", "HeaderSleepTimer": "Wyłącznik czasowy",
"HeaderStatsLargestItems": "Największe pozycje", "HeaderStatsLargestItems": "Największe pozycje",
"HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)", "HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)",
@ -438,7 +445,7 @@
"LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień", "LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień",
"LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.", "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", "LabelNumberOfBooks": "Liczba książek",
"LabelNumberOfEpisodes": "# odcinków", "LabelNumberOfEpisodes": "# Odcinków",
"LabelOpenRSSFeed": "Otwórz kanał RSS", "LabelOpenRSSFeed": "Otwórz kanał RSS",
"LabelOverwrite": "Nadpisz", "LabelOverwrite": "Nadpisz",
"LabelPassword": "Hasło", "LabelPassword": "Hasło",

1
client/strings/ro.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -206,6 +206,7 @@
"LabelAccountTypeAdmin": "Administratör", "LabelAccountTypeAdmin": "Administratör",
"LabelAccountTypeGuest": "Gäst", "LabelAccountTypeGuest": "Gäst",
"LabelAccountTypeUser": "Användare", "LabelAccountTypeUser": "Användare",
"LabelActivities": "Aktiviteter",
"LabelActivity": "Aktivitet", "LabelActivity": "Aktivitet",
"LabelAddToCollection": "Lägg till i en samling", "LabelAddToCollection": "Lägg till i en samling",
"LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen", "LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen",
@ -267,6 +268,7 @@
"LabelContinueSeries": "Fortsätt med serien", "LabelContinueSeries": "Fortsätt med serien",
"LabelCover": "Omslag", "LabelCover": "Omslag",
"LabelCoverImageURL": "URL till omslagsbild", "LabelCoverImageURL": "URL till omslagsbild",
"LabelCoverProvider": "Källa för omslag",
"LabelCreatedAt": "Skapad", "LabelCreatedAt": "Skapad",
"LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)", "LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)",
"LabelCurrent": "Nuvarande", "LabelCurrent": "Nuvarande",
@ -370,6 +372,7 @@
"LabelIntervalEvery6Hours": "Var 6:e timme", "LabelIntervalEvery6Hours": "Var 6:e timme",
"LabelIntervalEveryDay": "Varje dag", "LabelIntervalEveryDay": "Varje dag",
"LabelIntervalEveryHour": "Varje timme", "LabelIntervalEveryHour": "Varje timme",
"LabelIntervalEveryMinute": "Varje minut",
"LabelInvert": "Invertera", "LabelInvert": "Invertera",
"LabelItem": "Objekt", "LabelItem": "Objekt",
"LabelJumpBackwardAmount": "Inställning för \"hopp bakåt\"", "LabelJumpBackwardAmount": "Inställning för \"hopp bakåt\"",
@ -464,12 +467,13 @@
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
"LabelPreventIndexing": "Förhindra att ditt flöde indexeras av iTunes och Google-podcastsökmotorer", "LabelPreventIndexing": "Förhindra att ditt flöde indexeras av sökmotorer från iTunes och Google",
"LabelPrimaryEbook": "Primär e-bok", "LabelPrimaryEbook": "Primär e-bok",
"LabelProgress": "Framsteg", "LabelProgress": "Framsteg",
"LabelProvider": "Källa", "LabelProvider": "Källa",
"LabelPubDate": "Publiceringsdatum", "LabelPubDate": "Publiceringsdatum",
"LabelPublishYear": "Publiceringsår", "LabelPublishYear": "Publiceringsår",
"LabelPublishedDate": "Publicerad {0}",
"LabelPublishedDecade": "Årtionde för publicering", "LabelPublishedDecade": "Årtionde för publicering",
"LabelPublisher": "Utgivare", "LabelPublisher": "Utgivare",
"LabelPublishers": "Utgivare", "LabelPublishers": "Utgivare",
@ -794,11 +798,13 @@
"MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den", "MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den",
"MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />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.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.", "MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />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.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.",
"MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas att denna funktion ej aktiveras. Istället bör funktionen 'Watcher' vara aktiverad. Watcher kommer då automatiskt identifiera förändringar i biblioteket. För vissa filsystem (som t.ex. NFS) fungerar inte Watcher. Då kan schemalagda skanningar av biblioteken användas istället.", "MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas att denna funktion ej aktiveras. Istället bör funktionen 'Watcher' vara aktiverad. Watcher kommer då automatiskt identifiera förändringar i biblioteket. För vissa filsystem (som t.ex. NFS) fungerar inte Watcher. Då kan schemalagda skanningar av biblioteken användas istället.",
"MessageScheduleRunEveryWeekdayAtTime": "Startar varje {0} klockan {1}",
"MessageSearchResultsFor": "Sökresultat för", "MessageSearchResultsFor": "Sökresultat för",
"MessageSelected": "{0} valda", "MessageSelected": "{0} valda",
"MessageServerCouldNotBeReached": "Servern kunde inte nås", "MessageServerCouldNotBeReached": "Servern kunde inte nås",
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn", "MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
"MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?", "MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?",
"MessageTaskAudioFileNotWritable": "Det går inte att skriva till ljudfilen \"{0}\"",
"MessageTaskCanceledByUser": "Uppgiften avslutades av användaren", "MessageTaskCanceledByUser": "Uppgiften avslutades av användaren",
"MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"", "MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"",
"MessageTaskEmbeddingMetadata": "Infogar metadata", "MessageTaskEmbeddingMetadata": "Infogar metadata",
@ -812,7 +818,11 @@
"MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen", "MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen",
"MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata", "MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata",
"MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"", "MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"",
"MessageTaskNoFilesToScan": "Inga filer finns tillgängliga för skanning",
"MessageTaskOpmlImportDescription": "Skapar podcasts från {0} RSS-flöden",
"MessageTaskOpmlImportFeedDescription": "Importerar RSS-flödet \"{0}\"",
"MessageTaskOpmlImportFeedPodcastDescription": "Skapar podcast \"{0}\"", "MessageTaskOpmlImportFeedPodcastDescription": "Skapar podcast \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "En podcast finns redan med den adressen",
"MessageTaskOpmlImportFeedPodcastFailed": "Misslyckades med att skapa podcast", "MessageTaskOpmlImportFeedPodcastFailed": "Misslyckades med att skapa podcast",
"MessageTaskOpmlImportFinished": "Adderade {0} podcasts", "MessageTaskOpmlImportFinished": "Adderade {0} podcasts",
"MessageTaskOpmlParseFailed": "Misslyckades att tolka OPML-filen", "MessageTaskOpmlParseFailed": "Misslyckades att tolka OPML-filen",
@ -823,6 +833,7 @@
"MessageTaskScanItemsUpdated": "{0} uppdaterades", "MessageTaskScanItemsUpdated": "{0} uppdaterades",
"MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades", "MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades",
"MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats", "MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats",
"MessageTaskTargetDirectoryNotWritable": "Det är inte tillåtet att skriva i den angivna katalogen",
"MessageThinking": "Tänker...", "MessageThinking": "Tänker...",
"MessageUploaderItemFailed": "Misslyckades med att ladda upp", "MessageUploaderItemFailed": "Misslyckades med att ladda upp",
"MessageUploaderItemSuccess": "har blivit uppladdad!", "MessageUploaderItemSuccess": "har blivit uppladdad!",
@ -884,6 +895,7 @@
"ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopian", "ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopian",
"ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopian", "ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopian",
"ToastBackupUploadSuccess": "Säkerhetskopian uppladdad", "ToastBackupUploadSuccess": "Säkerhetskopian uppladdad",
"ToastBatchQuickMatchStarted": "Snabbmatchning av {0} böcker har påbörjats!",
"ToastBatchUpdateFailed": "Batchuppdateringen misslyckades", "ToastBatchUpdateFailed": "Batchuppdateringen misslyckades",
"ToastBatchUpdateSuccess": "Batchuppdateringen lyckades", "ToastBatchUpdateSuccess": "Batchuppdateringen lyckades",
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket", "ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",

View file

@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Адміністратор", "LabelAccountTypeAdmin": "Адміністратор",
"LabelAccountTypeGuest": "Гість", "LabelAccountTypeGuest": "Гість",
"LabelAccountTypeUser": "Користувач", "LabelAccountTypeUser": "Користувач",
"LabelActivities": "Діяльність",
"LabelActivity": "Активність", "LabelActivity": "Активність",
"LabelAddToCollection": "Додати у добірку", "LabelAddToCollection": "Додати у добірку",
"LabelAddToCollectionBatch": "Додати книги до добірки: {0}", "LabelAddToCollectionBatch": "Додати книги до добірки: {0}",
@ -283,6 +284,7 @@
"LabelContinueSeries": "Продовжити серії", "LabelContinueSeries": "Продовжити серії",
"LabelCover": "Обкладинка", "LabelCover": "Обкладинка",
"LabelCoverImageURL": "URL-адреса обкладинки", "LabelCoverImageURL": "URL-адреса обкладинки",
"LabelCoverProvider": "Постачальник покриття",
"LabelCreatedAt": "Дата створення", "LabelCreatedAt": "Дата створення",
"LabelCronExpression": "Команда cron", "LabelCronExpression": "Команда cron",
"LabelCurrent": "Поточне", "LabelCurrent": "Поточне",
@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Кожні 6 годин", "LabelIntervalEvery6Hours": "Кожні 6 годин",
"LabelIntervalEveryDay": "Щодня", "LabelIntervalEveryDay": "Щодня",
"LabelIntervalEveryHour": "Щогодини", "LabelIntervalEveryHour": "Щогодини",
"LabelIntervalEveryMinute": "Кожну хвилину",
"LabelInvert": "Інвертувати", "LabelInvert": "Інвертувати",
"LabelItem": "Елемент", "LabelItem": "Елемент",
"LabelJumpBackwardAmount": "Час переходу назад", "LabelJumpBackwardAmount": "Час переходу назад",
@ -845,6 +848,7 @@
"MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від", "MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від",
"MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.", "MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.",
"MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.", "MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.",
"MessageScheduleRunEveryWeekdayAtTime": "Запуск кожні {0} о {1}",
"MessageSearchResultsFor": "Результати пошуку для", "MessageSearchResultsFor": "Результати пошуку для",
"MessageSelected": "Вибрано: {0}", "MessageSelected": "Вибрано: {0}",
"MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера", "MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.19.4", "version": "2.19.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.19.4", "version": "2.19.5",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.19.4", "version": "2.19.5",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",

View file

@ -5,7 +5,7 @@ const Logger = require('./Logger')
const Task = require('./objects/Task') const Task = require('./objects/Task')
const TaskManager = require('./managers/TaskManager') const TaskManager = require('./managers/TaskManager')
const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs } = require('./utils/fileUtils') const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs, shouldIgnoreFile } = require('./utils/fileUtils')
/** /**
* @typedef PendingFileUpdate * @typedef PendingFileUpdate
@ -286,15 +286,10 @@ class FolderWatcher extends EventEmitter {
const relPath = path.replace(folderPath, '') const relPath = path.replace(folderPath, '')
if (Path.extname(relPath).toLowerCase() === '.part') { // Check for ignored extensions or directories, such as dotfiles and hidden directories
Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`) const shouldIgnore = shouldIgnoreFile(relPath)
return false if (shouldIgnore) {
} Logger.debug(`[Watcher] Ignoring ${shouldIgnore} - "${relPath}"`)
// Ignore files/folders starting with "."
const hasDotPath = relPath.split('/').find((p) => p.startsWith('.'))
if (hasDotPath) {
Logger.debug(`[Watcher] Ignoring dot path "${relPath}" | Piece "${hasDotPath}"`)
return false return false
} }

View file

@ -254,6 +254,11 @@ class LibraryController {
* @param {Response} res * @param {Response} res
*/ */
async update(req, res) { async update(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to update library`)
return res.sendStatus(403)
}
// Validation // Validation
const updatePayload = {} const updatePayload = {}
const keysToCheck = ['name', 'provider', 'mediaType', 'icon'] const keysToCheck = ['name', 'provider', 'mediaType', 'icon']
@ -519,6 +524,11 @@ class LibraryController {
* @param {Response} res * @param {Response} res
*/ */
async delete(req, res) { async delete(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library`)
return res.sendStatus(403)
}
// Remove library watcher // Remove library watcher
Watcher.removeLibrary(req.library) Watcher.removeLibrary(req.library)
@ -639,6 +649,11 @@ class LibraryController {
* @param {Response} res * @param {Response} res
*/ */
async removeLibraryItemsWithIssues(req, res) { async removeLibraryItemsWithIssues(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library items missing or invalid`)
return res.sendStatus(403)
}
const libraryItemsWithIssues = await Database.libraryItemModel.findAll({ const libraryItemsWithIssues = await Database.libraryItemModel.findAll({
where: { where: {
libraryId: req.library.id, libraryId: req.library.id,

View file

@ -41,6 +41,9 @@ class CustomProviderAdapter {
} }
const queryString = new URLSearchParams(queryObj).toString() const queryString = new URLSearchParams(queryObj).toString()
const url = `${provider.url}/search?${queryString}`
Logger.debug(`[CustomMetadataProvider] Search url: ${url}`)
// Setup headers // Setup headers
const axiosOptions = { const axiosOptions = {
timeout timeout
@ -52,7 +55,7 @@ class CustomProviderAdapter {
} }
const matches = await axios const matches = await axios
.get(`${provider.url}/search?${queryString}`, axiosOptions) .get(url, axiosOptions)
.then((res) => { .then((res) => {
if (!res?.data || !Array.isArray(res.data.matches)) return null if (!res?.data || !Array.isArray(res.data.matches)) return null
return res.data.matches return res.data.matches

View file

@ -131,6 +131,40 @@ async function readTextFile(path) {
} }
module.exports.readTextFile = readTextFile module.exports.readTextFile = readTextFile
/**
* Check if file or directory should be ignored. Returns a string of the reason to ignore, or null if not ignored
*
* @param {string} path
* @returns {string}
*/
module.exports.shouldIgnoreFile = (path) => {
// Check if directory or file name starts with "."
if (Path.basename(path).startsWith('.')) {
return 'dotfile'
}
if (path.split('/').find((p) => p.startsWith('.'))) {
return 'dotpath'
}
// If these strings exist anywhere in the filename or directory name, ignore. Vendor specific hidden directories
const includeAnywhereIgnore = ['@eaDir']
const filteredInclude = includeAnywhereIgnore.filter((str) => path.includes(str))
if (filteredInclude.length) {
return `${filteredInclude[0]} directory`
}
const extensionIgnores = ['.part', '.tmp', '.crdownload', '.download', '.bak', '.old', '.temp', '.tempfile', '.tempfile~']
// Check extension
if (extensionIgnores.includes(Path.extname(path).toLowerCase())) {
// Return the extension that is ignored
return `${Path.extname(path)} file`
}
// Should not ignore this file or directory
return null
}
/** /**
* @typedef FilePathItem * @typedef FilePathItem
* @property {string} name - file name e.g. "audiofile.m4b" * @property {string} name - file name e.g. "audiofile.m4b"
@ -147,7 +181,7 @@ module.exports.readTextFile = readTextFile
* @param {string} [relPathToReplace] * @param {string} [relPathToReplace]
* @returns {FilePathItem[]} * @returns {FilePathItem[]}
*/ */
async function recurseFiles(path, relPathToReplace = null) { module.exports.recurseFiles = async (path, relPathToReplace = null) => {
path = filePathToPOSIX(path) path = filePathToPOSIX(path)
if (!path.endsWith('/')) path = path + '/' if (!path.endsWith('/')) path = path + '/'
@ -197,14 +231,10 @@ async function recurseFiles(path, relPathToReplace = null) {
return false return false
} }
if (item.extension === '.part') { // Check for ignored extensions or directories
Logger.debug(`[fileUtils] Ignoring .part file "${relpath}"`) const shouldIgnore = this.shouldIgnoreFile(relpath)
return false if (shouldIgnore) {
} Logger.debug(`[fileUtils] Ignoring ${shouldIgnore} - "${relpath}"`)
// Ignore any file if a directory or the filename starts with "."
if (relpath.split('/').find((p) => p.startsWith('.'))) {
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
return false return false
} }
@ -235,7 +265,6 @@ async function recurseFiles(path, relPathToReplace = null) {
return list return list
} }
module.exports.recurseFiles = recurseFiles
/** /**
* *

View file

@ -344,22 +344,28 @@ module.exports = {
countCache.clear() countCache.clear()
}, },
async findAndCountAll(findOptions, limit, offset) { async findAndCountAll(findOptions, limit, offset, useCountCache) {
const findOptionsKey = stringifySequelizeQuery(findOptions) const model = Database.bookModel
Logger.debug(`[LibraryItemsBookFilters] findOptionsKey: ${findOptionsKey}`) if (useCountCache) {
const countCacheKey = stringifySequelizeQuery(findOptions)
Logger.debug(`[LibraryItemsBookFilters] countCacheKey: ${countCacheKey}`)
if (!countCache.has(countCacheKey)) {
const count = await model.count(findOptions)
countCache.set(countCacheKey, count)
}
findOptions.limit = limit || null
findOptions.offset = offset
const rows = await model.findAll(findOptions)
return { rows, count: countCache.get(countCacheKey) }
}
findOptions.limit = limit || null findOptions.limit = limit || null
findOptions.offset = offset findOptions.offset = offset
if (countCache.has(findOptionsKey)) { return await model.findAndCountAll(findOptions)
const rows = await Database.bookModel.findAll(findOptions)
return { rows, count: countCache.get(findOptionsKey) }
} else {
const result = await Database.bookModel.findAndCountAll(findOptions)
countCache.set(findOptionsKey, result.count)
return result
}
}, },
/** /**
@ -434,19 +440,17 @@ module.exports = {
const libraryItemIncludes = [] const libraryItemIncludes = []
const bookIncludes = [] const bookIncludes = []
if (includeRSSFeed) {
if (filterGroup === 'feed-open' || includeRSSFeed) {
const rssFeedRequired = filterGroup === 'feed-open'
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.feedModel, model: Database.feedModel,
required: filterGroup === 'feed-open', required: rssFeedRequired,
separate: true separate: !rssFeedRequired
}) })
} }
if (filterGroup === 'feed-open' && !includeRSSFeed) {
libraryItemIncludes.push({ if (filterGroup === 'share-open') {
model: Database.feedModel,
required: true
})
} else if (filterGroup === 'share-open') {
bookIncludes.push({ bookIncludes.push({
model: Database.mediaItemShareModel, model: Database.mediaItemShareModel,
required: true required: true
@ -608,7 +612,7 @@ module.exports = {
} }
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
const { rows: books, count } = await findAndCountAll(findOptions, limit, offset) const { rows: books, count } = await findAndCountAll(findOptions, limit, offset, !filterGroup)
const libraryItems = books.map((bookExpanded) => { const libraryItems = books.map((bookExpanded) => {
const libraryItem = bookExpanded.libraryItem const libraryItem = bookExpanded.libraryItem

View file

@ -105,22 +105,27 @@ module.exports = {
countCache.clear() countCache.clear()
}, },
async findAndCountAll(findOptions, model, limit, offset) { async findAndCountAll(findOptions, model, limit, offset, useCountCache) {
const cacheKey = stringifySequelizeQuery(findOptions) if (useCountCache) {
if (!countCache.has(cacheKey)) { const countCacheKey = stringifySequelizeQuery(findOptions)
const count = await model.count(findOptions) Logger.debug(`[LibraryItemsPodcastFilters] countCacheKey: ${countCacheKey}`)
countCache.set(cacheKey, count) if (!countCache.has(countCacheKey)) {
const count = await model.count(findOptions)
countCache.set(countCacheKey, count)
}
findOptions.limit = limit || null
findOptions.offset = offset
const rows = await model.findAll(findOptions)
return { rows, count: countCache.get(countCacheKey) }
} }
findOptions.limit = limit findOptions.limit = limit || null
findOptions.offset = offset findOptions.offset = offset
const rows = await model.findAll(findOptions) return await model.findAndCountAll(findOptions)
return {
rows,
count: countCache.get(cacheKey)
}
}, },
/** /**
@ -199,7 +204,7 @@ module.exports = {
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
const { rows: podcasts, count } = await findAndCountAll(findOptions, Database.podcastModel, limit, offset) const { rows: podcasts, count } = await findAndCountAll(findOptions, Database.podcastModel, limit, offset, !filterGroup)
const libraryItems = podcasts.map((podcastExpanded) => { const libraryItems = podcasts.map((podcastExpanded) => {
const libraryItem = podcastExpanded.libraryItem const libraryItem = podcastExpanded.libraryItem
@ -323,7 +328,7 @@ module.exports = {
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
const { rows: podcastEpisodes, count } = await findAndCountAll(findOptions, Database.podcastEpisodeModel, limit, offset) const { rows: podcastEpisodes, count } = await findAndCountAll(findOptions, Database.podcastEpisodeModel, limit, offset, !filterGroup)
const libraryItems = podcastEpisodes.map((ep) => { const libraryItems = podcastEpisodes.map((ep) => {
const libraryItem = ep.podcast.libraryItem const libraryItem = ep.podcast.libraryItem

View file

@ -0,0 +1,127 @@
const chai = require('chai')
const expect = chai.expect
const sinon = require('sinon')
const fileUtils = require('../../../server/utils/fileUtils')
const fs = require('fs')
const Logger = require('../../../server/Logger')
describe('fileUtils', () => {
it('shouldIgnoreFile', () => {
global.isWin = process.platform === 'win32'
const testCases = [
{ path: 'test.txt', expected: null },
{ path: 'folder/test.mp3', expected: null },
{ path: 'normal/path/file.m4b', expected: null },
{ path: 'test.txt.part', expected: '.part file' },
{ path: 'test.txt.tmp', expected: '.tmp file' },
{ path: 'test.txt.crdownload', expected: '.crdownload file' },
{ path: 'test.txt.download', expected: '.download file' },
{ path: 'test.txt.bak', expected: '.bak file' },
{ path: 'test.txt.old', expected: '.old file' },
{ path: 'test.txt.temp', expected: '.temp file' },
{ path: 'test.txt.tempfile', expected: '.tempfile file' },
{ path: 'test.txt.tempfile~', expected: '.tempfile~ file' },
{ path: '.gitignore', expected: 'dotfile' },
{ path: 'folder/.hidden', expected: 'dotfile' },
{ path: '.git/config', expected: 'dotpath' },
{ path: 'path/.hidden/file.txt', expected: 'dotpath' },
{ path: '@eaDir', expected: '@eaDir directory' },
{ path: 'folder/@eaDir', expected: '@eaDir directory' },
{ path: 'path/@eaDir/file.txt', expected: '@eaDir directory' },
{ path: '.hidden/test.tmp', expected: 'dotpath' },
{ path: '@eaDir/test.part', expected: '@eaDir directory' }
]
testCases.forEach(({ path, expected }) => {
const result = fileUtils.shouldIgnoreFile(path)
expect(result).to.equal(expected)
})
})
describe('recurseFiles', () => {
let readdirStub, realpathStub, statStub
beforeEach(() => {
global.isWin = process.platform === 'win32'
// Mock file structure with normalized paths
const mockDirContents = new Map([
['/test', ['file1.mp3', 'subfolder', 'ignoreme', 'temp.mp3.tmp']],
['/test/subfolder', ['file2.m4b']],
['/test/ignoreme', ['.ignore', 'ignored.mp3']]
])
const mockStats = new Map([
['/test/file1.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1' }],
['/test/subfolder', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '2' }],
['/test/subfolder/file2.m4b', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '3' }],
['/test/ignoreme', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '4' }],
['/test/ignoreme/.ignore', { isDirectory: () => false, size: 0, mtimeMs: Date.now(), ino: '5' }],
['/test/ignoreme/ignored.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '6' }],
['/test/temp.mp3.tmp', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '7' }]
])
// Stub fs.readdir
readdirStub = sinon.stub(fs, 'readdir')
readdirStub.callsFake((path, callback) => {
const contents = mockDirContents.get(path)
if (contents) {
callback(null, contents)
} else {
callback(new Error(`ENOENT: no such file or directory, scandir '${path}'`))
}
})
// Stub fs.realpath
realpathStub = sinon.stub(fs, 'realpath')
realpathStub.callsFake((path, callback) => {
// Return normalized path
callback(null, fileUtils.filePathToPOSIX(path).replace(/\/$/, ''))
})
// Stub fs.stat
statStub = sinon.stub(fs, 'stat')
statStub.callsFake((path, callback) => {
const normalizedPath = fileUtils.filePathToPOSIX(path).replace(/\/$/, '')
const stats = mockStats.get(normalizedPath)
if (stats) {
callback(null, stats)
} else {
callback(new Error(`ENOENT: no such file or directory, stat '${normalizedPath}'`))
}
})
// Stub Logger
sinon.stub(Logger, 'debug')
})
afterEach(() => {
sinon.restore()
})
it('should return filtered file list', async () => {
const files = await fileUtils.recurseFiles('/test')
expect(files).to.be.an('array')
expect(files).to.have.lengthOf(2)
expect(files[0]).to.deep.equal({
name: 'file1.mp3',
path: 'file1.mp3',
reldirpath: '',
fullpath: '/test/file1.mp3',
extension: '.mp3',
deep: 0
})
expect(files[1]).to.deep.equal({
name: 'file2.m4b',
path: 'subfolder/file2.m4b',
reldirpath: 'subfolder',
fullpath: '/test/subfolder/file2.m4b',
extension: '.m4b',
deep: 1
})
})
})
})