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

This commit is contained in:
Toni Barth 2025-07-12 22:20:14 +02:00
commit 900b217da2
98 changed files with 3671 additions and 1143 deletions

View file

@ -70,6 +70,11 @@ export default {
title: this.$strings.HeaderUsers,
path: '/config/users'
},
{
id: 'config-api-keys',
title: this.$strings.HeaderApiKeys,
path: '/config/api-keys'
},
{
id: 'config-sessions',
title: this.$strings.HeaderListeningSessions,

View file

@ -778,10 +778,6 @@ export default {
windowResize() {
this.executeRebuild()
},
socketInit() {
// Server settings are set on socket init
this.executeRebuild()
},
initListeners() {
window.addEventListener('resize', this.windowResize)
@ -794,7 +790,6 @@ export default {
})
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$on('socket_init', this.socketInit)
this.$eventBus.$on('user-settings', this.settingsUpdated)
if (this.$root.socket) {
@ -826,7 +821,6 @@ export default {
}
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$off('socket_init', this.socketInit)
this.$eventBus.$off('user-settings', this.settingsUpdated)
if (this.$root.socket) {

View file

@ -71,9 +71,6 @@ export default {
coverHeight() {
return this.cardHeight
},
userToken() {
return this.store.getters['user/getToken']
},
_author() {
return this.author || {}
},

View file

@ -198,7 +198,7 @@ export default {
return this.store.getters['user/getSizeMultiplier']
},
dateFormat() {
return this.store.state.serverSettings.dateFormat
return this.store.getters['getServerSetting']('dateFormat')
},
_libraryItem() {
return this.libraryItem || {}

View file

@ -71,7 +71,7 @@ export default {
return this.height * this.sizeMultiplier
},
dateFormat() {
return this.store.state.serverSettings.dateFormat
return this.store.getters['getServerSetting']('dateFormat')
},
labelFontSize() {
if (this.width < 160) return 0.75

View file

@ -94,6 +94,9 @@ export default {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
userCanAccessExplicitContent() {
return this.$store.getters['user/getUserCanAccessExplicitContent']
},
libraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
@ -239,6 +242,15 @@ export default {
sublist: false
}
]
if (this.userCanAccessExplicitContent) {
items.push({
text: this.$strings.LabelExplicit,
value: 'explicit',
sublist: false
})
}
if (this.userIsAdminOrUp) {
items.push({
text: this.$strings.LabelShareOpen,
@ -249,7 +261,7 @@ export default {
return items
},
podcastItems() {
return [
const items = [
{
text: this.$strings.LabelAll,
value: 'all'
@ -283,6 +295,16 @@ export default {
sublist: false
}
]
if (this.userCanAccessExplicitContent) {
items.push({
text: this.$strings.LabelExplicit,
value: 'explicit',
sublist: false
})
}
return items
},
selectItems() {
if (this.isSeries) return this.seriesItems

View file

@ -39,9 +39,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
_author() {
return this.author || {}
},

View file

@ -309,9 +309,9 @@ export default {
} else {
console.log('Account updated', data.user)
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
console.log('Current user token was updated')
this.$store.commit('user/setUserToken', data.user.token)
if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) {
console.log('Current user access token was updated')
this.$store.commit('user/setAccessToken', data.user.accessToken)
}
this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
@ -351,9 +351,6 @@ export default {
this.$toast.error(errMsg || 'Failed to create account')
})
},
toggleActive() {
this.newUser.isActive = !this.newUser.isActive
},
userTypeUpdated(type) {
this.newUser.permissions = {
download: type !== 'guest',

View file

@ -0,0 +1,60 @@
<template>
<modals-modal ref="modal" v-model="show" name="api-key-created" :width="800" :height="'unset'" persistent>
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 200px; max-height: 80vh">
<div class="w-full p-8">
<p class="text-lg text-white mb-4">{{ $getString('LabelApiKeyCreated', [apiKeyName]) }}</p>
<p class="text-lg text-white mb-4">{{ $strings.LabelApiKeyCreatedDescription }}</p>
<ui-text-input label="API Key" :value="apiKeyKey" readonly show-copy />
<div class="flex justify-end mt-4">
<ui-btn color="bg-primary" @click="show = false">{{ $strings.ButtonClose }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
apiKey: {
type: Object,
default: () => null
}
},
data() {
return {}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
title() {
return this.$strings.HeaderNewApiKey
},
apiKeyName() {
return this.apiKey?.name || ''
},
apiKeyKey() {
return this.apiKey?.apiKey || ''
}
},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,198 @@
<template>
<modals-modal ref="modal" v-model="show" name="api-key" :width="800" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
<div class="w-full p-8">
<div class="flex py-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model.trim="newApiKey.name" :readonly="!isNew" :label="$strings.LabelName" />
</div>
<div v-if="isNew" class="w-1/2 px-2">
<ui-text-input-with-label v-model.trim="newApiKey.expiresIn" :label="$strings.LabelExpiresInSeconds" type="number" :min="0" />
</div>
</div>
<div class="flex items-center pt-4 pb-2 gap-2">
<div class="flex items-center px-2">
<p class="px-3 font-semibold" id="user-enabled-toggle">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch :disabled="isExpired && !apiKey.isActive" labeledBy="user-enabled-toggle" v-model="newApiKey.isActive" />
</div>
<div v-if="isExpired" class="px-2">
<p class="text-sm text-error">{{ $strings.LabelExpired }}</p>
</div>
</div>
<div class="w-full border-t border-b border-black-200 py-4 px-3 mt-4">
<p class="text-lg mb-2 font-semibold">{{ $strings.LabelApiKeyUser }}</p>
<p class="text-sm mb-2 text-gray-400">{{ $strings.LabelApiKeyUserDescription }}</p>
<ui-select-input v-model="newApiKey.userId" :disabled="isExpired && !apiKey.isActive" :items="userItems" :placeholder="$strings.LabelSelectUser" :label="$strings.LabelApiKeyUser" label-hidden />
</div>
<div class="flex pt-4 px-2">
<div class="grow" />
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
apiKey: {
type: Object,
default: () => null
},
users: {
type: Array,
default: () => []
}
},
data() {
return {
processing: false,
newApiKey: {},
isNew: true
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
title() {
return this.isNew ? this.$strings.HeaderNewApiKey : this.$strings.HeaderUpdateApiKey
},
userItems() {
return this.users
.filter((u) => {
// Only show root user if the current user is root
return u.type !== 'root' || this.$store.getters['user/getIsRoot']
})
.map((u) => ({ text: u.username, value: u.id, subtext: u.type }))
},
isExpired() {
if (!this.apiKey || !this.apiKey.expiresAt) return false
return new Date(this.apiKey.expiresAt).getTime() < Date.now()
}
},
methods: {
submitForm() {
if (!this.newApiKey.name) {
this.$toast.error(this.$strings.ToastNameRequired)
return
}
if (!this.newApiKey.userId) {
this.$toast.error(this.$strings.ToastNewApiKeyUserError)
return
}
if (this.isNew) {
this.submitCreateApiKey()
} else {
this.submitUpdateApiKey()
}
},
submitUpdateApiKey() {
if (this.newApiKey.isActive === this.apiKey.isActive && this.newApiKey.userId === this.apiKey.userId) {
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
this.show = false
return
}
const apiKey = {
isActive: this.newApiKey.isActive,
userId: this.newApiKey.userId
}
this.processing = true
this.$axios
.$patch(`/api/api-keys/${this.apiKey.id}`, apiKey)
.then((data) => {
this.processing = false
if (data.error) {
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
} else {
this.show = false
this.$emit('updated', data.apiKey)
}
})
.catch((error) => {
this.processing = false
console.error('Failed to update apiKey', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
})
},
submitCreateApiKey() {
const apiKey = { ...this.newApiKey }
if (this.newApiKey.expiresIn) {
apiKey.expiresIn = parseInt(this.newApiKey.expiresIn)
} else {
delete apiKey.expiresIn
}
this.processing = true
this.$axios
.$post('/api/api-keys', apiKey)
.then((data) => {
this.processing = false
if (data.error) {
this.$toast.error(this.$strings.ToastFailedToCreate + ': ' + data.error)
} else {
this.show = false
this.$emit('created', data.apiKey)
}
})
.catch((error) => {
this.processing = false
console.error('Failed to create apiKey', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || this.$strings.ToastFailedToCreate)
})
},
init() {
this.isNew = !this.apiKey
if (this.apiKey) {
this.newApiKey = {
name: this.apiKey.name,
isActive: this.apiKey.isActive,
userId: this.apiKey.userId
}
} else {
this.newApiKey = {
name: null,
expiresIn: null,
isActive: true,
userId: null
}
}
}
},
mounted() {}
}
</script>

View file

@ -79,10 +79,10 @@ export default {
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {

View file

@ -159,10 +159,10 @@ export default {
return 'Unknown'
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
},
isOpenSession() {
return !!this._session.open

View file

@ -23,7 +23,7 @@ export default {
processing: Boolean,
persistent: {
type: Boolean,
default: true
default: false
},
width: {
type: [String, Number],
@ -99,7 +99,7 @@ export default {
this.preventClickoutside = false
return
}
if (this.processing && this.persistent) return
if (this.processing || this.persistent) return
if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) {
this.show = false
}

View file

@ -144,7 +144,7 @@ export default {
expirationDateString() {
if (!this.expireDurationSeconds) return this.$strings.LabelPermanent
const dateMs = Date.now() + this.expireDurationSeconds * 1000
return this.$formatDatetime(dateMs, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat)
return this.$formatDatetime(dateMs, this.$store.getters['getServerSetting']('dateFormat'), this.$store.getters['getServerSetting']('timeFormat'))
}
},
methods: {

View file

@ -40,7 +40,7 @@ export default {
}
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
releasesToShow() {
return this.versionData?.releasesToShow || []

View file

@ -29,9 +29,6 @@ export default {
media() {
return this.libraryItem.media || {}
},
userToken() {
return this.$store.getters['user/getToken']
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},

View file

@ -35,7 +35,14 @@
<widgets-podcast-type-indicator :type="episode.episodeType" />
</div>
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
<div class="flex items-center space-x-2">
<!-- published -->
<p class="text-xs text-gray-300 w-40">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
<!-- duration -->
<p v-if="episode.durationSeconds && !isNaN(episode.durationSeconds)" class="text-xs text-gray-300 min-w-28">{{ $strings.LabelDuration }}: {{ $elapsedPretty(episode.durationSeconds) }}</p>
<!-- size -->
<p v-if="episode.enclosure?.length && !isNaN(episode.enclosure.length) && Number(episode.enclosure.length) > 0" class="text-xs text-gray-300">{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}</p>
</div>
</div>
</div>
</div>

View file

@ -11,7 +11,7 @@
{{ $getString('MessageConfirmRemoveEpisode', [episodeTitle]) }}
</p>
<p v-else class="text-lg text-gray-200 mb-4">{{ $getString('MessageConfirmRemoveEpisodes', [episodes.length]) }}</p>
<p class="text-xs font-semibold text-warning/90">Note: This does not delete the audio file unless toggling "Hard delete file"</p>
<p class="text-xs font-semibold text-warning/90">{{ $strings.MessageConfirmRemoveEpisodeNote }}</p>
</div>
<div class="flex justify-between items-center pt-4">
<ui-checkbox v-model="hardDeleteFile" :label="$strings.LabelHardDeleteFile" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" />

View file

@ -16,7 +16,7 @@
</div>
</div>
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" dir="auto" class="default-style less-spacing" v-html="description" />
<div v-if="description" dir="auto" class="default-style less-spacing" @click="handleDescriptionClick" v-html="description" />
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
<div class="w-full h-px bg-white/5 my-4" />
@ -34,6 +34,12 @@
{{ audioFileSize }}
</p>
</div>
<div class="grow">
<p class="font-semibold text-xs mb-1">{{ $strings.LabelDuration }}</p>
<p class="mb-2 text-xs">
{{ audioFileDuration }}
</p>
</div>
</div>
</div>
</modals-modal>
@ -68,7 +74,7 @@ export default {
return this.episode.title || 'No Episode Title'
},
description() {
return this.episode.description || ''
return this.parseDescription(this.episode.description || '')
},
media() {
return this.libraryItem?.media || {}
@ -90,11 +96,49 @@ export default {
return this.$bytesPretty(size)
},
audioFileDuration() {
const duration = this.episode.duration || 0
return this.$elapsedPretty(duration)
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
}
},
methods: {},
methods: {
handleDescriptionClick(e) {
if (e.target.matches('span.time-marker')) {
const time = parseInt(e.target.dataset.time)
if (!isNaN(time)) {
this.$eventBus.$emit('play-item', {
episodeId: this.episodeId,
libraryItemId: this.libraryItem.id,
startTime: time
})
}
e.preventDefault()
}
},
parseDescription(description) {
const timeMarkerLinkRegex = /<a href="#([^"]*?\b\d{1,2}:\d{1,2}(?::\d{1,2})?)">(.*?)<\/a>/g
const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g
function convertToSeconds(time) {
const timeParts = time.split(':').map(Number)
return timeParts.reduce((acc, part, index) => acc * 60 + part, 0)
}
return description
.replace(timeMarkerLinkRegex, (match, href, displayTime) => {
const time = displayTime.match(timeMarkerRegex)[0]
const seekTimeInSeconds = convertToSeconds(time)
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${displayTime}</span>`
})
.replace(timeMarkerRegex, (match) => {
const seekTimeInSeconds = convertToSeconds(match)
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${match}</span>`
})
}
},
mounted() {}
}
</script>

View file

@ -129,9 +129,6 @@ export default {
return `${hoursRounded}h`
}
},
token() {
return this.$store.getters['user/getToken']
},
timeRemaining() {
if (this.useChapterTrack && this.currentChapter) {
var currChapTime = this.currentTime - this.currentChapter.start

View file

@ -104,9 +104,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() {
return this.libraryItem?.id
},
@ -234,10 +231,7 @@ export default {
async extract() {
this.loading = true
var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob',
headers: {
Authorization: `Bearer ${this.userToken}`
}
responseType: 'blob'
})
const archive = await Archive.open(buff)
const originalFilesObject = await archive.getFilesObject()

View file

@ -57,9 +57,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
/** @returns {string} */
libraryItemId() {
return this.libraryItem?.id
@ -97,9 +94,9 @@ export default {
},
ebookUrl() {
if (this.fileId) {
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}`
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook`
return `/api/items/${this.libraryItemId}/ebook`
},
themeRules() {
const isDark = this.ereaderSettings.theme === 'dark'
@ -309,14 +306,24 @@ export default {
/** @type {EpubReader} */
const reader = this
// Use axios to make request because we have token refresh logic in interceptor
const customRequest = async (url) => {
try {
return this.$axios.$get(url, {
responseType: 'arraybuffer'
})
} catch (error) {
console.error('EpubReader.initEpub customRequest failed:', error)
throw error
}
}
/** @type {ePub.Book} */
reader.book = new ePub(reader.ebookUrl, {
width: this.readerWidth,
height: this.readerHeight - 50,
openAs: 'epub',
requestHeaders: {
Authorization: `Bearer ${this.userToken}`
}
requestMethod: customRequest
})
/** @type {ePub.Rendition} */
@ -337,29 +344,33 @@ export default {
this.applyTheme()
})
reader.book.ready.then(() => {
// set up event listeners
reader.rendition.on('relocated', reader.relocated)
reader.rendition.on('keydown', reader.keyUp)
reader.book.ready
.then(() => {
// set up event listeners
reader.rendition.on('relocated', reader.relocated)
reader.rendition.on('keydown', reader.keyUp)
reader.rendition.on('touchstart', (event) => {
this.$emit('touchstart', event)
})
reader.rendition.on('touchend', (event) => {
this.$emit('touchend', event)
})
// load ebook cfi locations
const savedLocations = this.loadLocations()
if (savedLocations) {
reader.book.locations.load(savedLocations)
} else {
reader.book.locations.generate().then(() => {
this.checkSaveLocations(reader.book.locations.save())
reader.rendition.on('touchstart', (event) => {
this.$emit('touchstart', event)
})
}
this.getChapters()
})
reader.rendition.on('touchend', (event) => {
this.$emit('touchend', event)
})
// load ebook cfi locations
const savedLocations = this.loadLocations()
if (savedLocations) {
reader.book.locations.load(savedLocations)
} else {
reader.book.locations.generate().then(() => {
this.checkSaveLocations(reader.book.locations.save())
})
}
this.getChapters()
})
.catch((error) => {
console.error('EpubReader.initEpub failed:', error)
})
},
getChapters() {
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759

View file

@ -26,9 +26,6 @@ export default {
return {}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() {
return this.libraryItem?.id
},
@ -96,11 +93,8 @@ export default {
},
async initMobi() {
// Fetch mobi file as blob
var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob',
headers: {
Authorization: `Bearer ${this.userToken}`
}
const buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob'
})
var reader = new FileReader()
reader.onload = async (event) => {

View file

@ -55,7 +55,8 @@ export default {
loadedRatio: 0,
page: 1,
numPages: 0,
pdfDocInitParams: null
pdfDocInitParams: null,
isRefreshing: false
}
},
computed: {
@ -152,7 +153,34 @@ export default {
this.page++
this.updateProgress()
},
error(err) {
async refreshToken() {
if (this.isRefreshing) return
this.isRefreshing = true
const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => {
console.error('Failed to refresh token', error)
return null
})
if (!newAccessToken) {
// Redirect to login on failed refresh
this.$router.push('/login')
return
}
// Force Vue to re-render the PDF component by creating a new object
this.pdfDocInitParams = {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${newAccessToken}`
}
}
this.isRefreshing = false
},
async error(err) {
if (err && err.status === 401) {
console.log('Received 401 error, refreshing token')
await this.refreshToken()
return
}
console.error(err)
},
resize() {

View file

@ -266,9 +266,6 @@ export default {
isComic() {
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
},
userToken() {
return this.$store.getters['user/getToken']
},
keepProgress() {
return this.$store.state.ereaderKeepProgress
},

View file

@ -0,0 +1,177 @@
<template>
<div>
<div class="text-center">
<table v-if="apiKeys.length > 0" id="api-keys">
<tr>
<th>{{ $strings.LabelName }}</th>
<th class="w-44">{{ $strings.LabelApiKeyUser }}</th>
<th class="w-32">{{ $strings.LabelExpiresAt }}</th>
<th class="w-32">{{ $strings.LabelCreatedAt }}</th>
<th class="w-32"></th>
</tr>
<tr v-for="apiKey in apiKeys" :key="apiKey.id" :class="apiKey.isActive ? '' : 'bg-error/10!'">
<td>
<div class="flex items-center">
<p class="pl-2 truncate">{{ apiKey.name }}</p>
</div>
</td>
<td class="text-xs">
<nuxt-link v-if="apiKey.user" :to="`/config/users/${apiKey.user.id}`" class="text-xs hover:underline">
{{ apiKey.user.username }}
</nuxt-link>
<p v-else class="text-xs">Error</p>
</td>
<td class="text-xs">
<p v-if="apiKey.expiresAt" class="text-xs" :title="apiKey.expiresAt">{{ getExpiresAtText(apiKey) }}</p>
<p v-else class="text-xs">{{ $strings.LabelExpiresNever }}</p>
</td>
<td class="text-xs font-mono">
<ui-tooltip direction="top" :text="$formatJsDatetime(new Date(apiKey.createdAt), dateFormat, timeFormat)">
{{ $formatJsDate(new Date(apiKey.createdAt), dateFormat) }}
</ui-tooltip>
</td>
<td class="py-0">
<div class="w-full flex justify-left">
<div class="h-8 w-8 flex items-center justify-center text-white/50 hover:text-white/100 cursor-pointer" @click.stop="editApiKey(apiKey)">
<button type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-base">edit</button>
</div>
<div class="h-8 w-8 flex items-center justify-center text-white/50 hover:text-error cursor-pointer" @click.stop="deleteApiKeyClick(apiKey)">
<button type="button" :aria-label="$strings.ButtonDelete" class="material-symbols text-base">delete</button>
</div>
</div>
</td>
</tr>
</table>
<p v-else class="text-base text-gray-300 py-4">{{ $strings.LabelNoApiKeys }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
apiKeys: [],
isDeletingApiKey: false
}
},
computed: {
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
}
},
methods: {
getExpiresAtText(apiKey) {
if (new Date(apiKey.expiresAt).getTime() < Date.now()) {
return this.$strings.LabelExpired
}
return this.$formatJsDatetime(new Date(apiKey.expiresAt), this.dateFormat, this.timeFormat)
},
deleteApiKeyClick(apiKey) {
if (this.isDeletingApiKey) return
const payload = {
message: this.$getString('MessageConfirmDeleteApiKey', [apiKey.name]),
callback: (confirmed) => {
if (confirmed) {
this.deleteApiKey(apiKey)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteApiKey(apiKey) {
this.isDeletingApiKey = true
this.$axios
.$delete(`/api/api-keys/${apiKey.id}`)
.then((data) => {
if (data.error) {
this.$toast.error(data.error)
} else {
this.removeApiKey(apiKey.id)
this.$emit('numApiKeys', this.apiKeys.length)
}
})
.catch((error) => {
console.error('Failed to delete apiKey', error)
this.$toast.error(this.$strings.ToastFailedToDelete)
})
.finally(() => {
this.isDeletingApiKey = false
})
},
editApiKey(apiKey) {
this.$emit('edit', apiKey)
},
addApiKey(apiKey) {
this.apiKeys.push(apiKey)
},
removeApiKey(apiKeyId) {
this.apiKeys = this.apiKeys.filter((a) => a.id !== apiKeyId)
},
updateApiKey(apiKey) {
this.apiKeys = this.apiKeys.map((a) => (a.id === apiKey.id ? apiKey : a))
},
loadApiKeys() {
this.$axios
.$get('/api/api-keys')
.then((res) => {
this.apiKeys = res.apiKeys.sort((a, b) => {
return a.createdAt - b.createdAt
})
this.$emit('numApiKeys', this.apiKeys.length)
})
.catch((error) => {
console.error('Failed to load apiKeys', error)
})
}
},
mounted() {
this.loadApiKeys()
}
}
</script>
<style>
#api-keys {
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #474747;
width: 100%;
}
#api-keys td,
#api-keys th {
/* border: 1px solid #2e2e2e; */
padding: 8px 8px;
text-align: left;
}
#api-keys td.py-0 {
padding: 0px 8px;
}
#api-keys tr:nth-child(even) {
background-color: #373838;
}
#api-keys tr:nth-child(odd) {
background-color: #2f2f2f;
}
#api-keys tr:hover {
background-color: #444;
}
#api-keys th {
font-size: 0.8rem;
font-weight: 600;
padding-top: 5px;
padding-bottom: 5px;
background-color: #272727;
}
</style>

View file

@ -78,10 +78,10 @@ export default {
return this.$store.getters['user/getToken']
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {

View file

@ -49,9 +49,6 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
userToken() {
return this.$store.getters['user/getToken']
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},

View file

@ -53,9 +53,6 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
userToken() {
return this.$store.getters['user/getToken']
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},

View file

@ -76,10 +76,10 @@ export default {
return usermap
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {

View file

@ -112,7 +112,7 @@ export default {
return this.episode?.publishedAt
},
dateFormat() {
return this.store.state.serverSettings.dateFormat
return this.store.getters['getServerSetting']('dateFormat')
},
itemProgress() {
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId)

View file

@ -239,10 +239,10 @@ export default {
})
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {

View file

@ -85,9 +85,6 @@ export default {
this.$emit('input', val)
}
},
userToken() {
return this.$store.getters['user/getToken']
},
wrapperClass() {
var classes = []
if (this.disabled) classes.push('bg-black-300')

View file

@ -1,9 +1,9 @@
<template>
<div class="relative w-full">
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<p v-if="label && !labelHidden" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<button ref="buttonWrapper" type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded-sm shadow-xs pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center">
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small, 'text-gray-400': !selectedText }">{{ selectedText || placeholder }}</span>
<span v-if="selectedSubtext">:&nbsp;</span>
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
</span>
@ -36,10 +36,15 @@ export default {
type: String,
default: ''
},
labelHidden: Boolean,
items: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: ''
},
disabled: Boolean,
small: Boolean,
menuMaxHeight: {

View file

@ -6,7 +6,7 @@
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</label>
</slot>
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :min="min" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
</div>
</template>
@ -21,6 +21,7 @@ export default {
type: String,
default: 'text'
},
min: [String, Number],
readonly: Boolean,
disabled: Boolean,
inputClass: String,

View file

@ -85,7 +85,7 @@ export default {
nextRun() {
if (!this.cronExpression) return ''
const parsed = this.$getNextScheduledDate(this.cronExpression)
return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || ''
return this.$formatJsDatetime(parsed, this.$store.getters['getServerSetting']('dateFormat'), this.$store.getters['getServerSetting']('timeFormat')) || ''
},
description() {
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''

View file

@ -40,6 +40,7 @@ describe('LazySeriesCard', () => {
},
$store: {
getters: {
getServerSetting: () => 'MM/dd/yyyy',
'user/getUserCanUpdate': true,
'user/getUserMediaProgress': (id) => null,
'user/getSizeMultiplier': 1,

View file

@ -33,6 +33,7 @@ export default {
return {
socket: null,
isSocketConnected: false,
isSocketAuthenticated: false,
isFirstSocketConnection: true,
socketConnectionToastId: null,
currentLang: null,
@ -81,9 +82,28 @@ export default {
document.body.classList.add('app-bar')
}
},
tokenRefreshed(newAccessToken) {
if (this.isSocketConnected && !this.isSocketAuthenticated) {
console.log('[SOCKET] Re-authenticating socket after token refresh')
this.socket.emit('auth', newAccessToken)
}
},
updateSocketConnectionToast(content, type, timeout) {
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
const toastUpdateOptions = {
content: content,
options: {
timeout: timeout,
type: type,
closeButton: false,
position: 'bottom-center',
onClose: () => {
this.socketConnectionToastId = null
},
closeOnClick: timeout !== null
}
}
this.$toast.update(this.socketConnectionToastId, toastUpdateOptions, false)
} else {
this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null })
}
@ -109,7 +129,7 @@ export default {
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
},
reconnect() {
console.error('[SOCKET] reconnected')
console.log('[SOCKET] reconnected')
},
reconnectAttempt(val) {
console.log(`[SOCKET] reconnect attempt ${val}`)
@ -120,6 +140,10 @@ export default {
reconnectFailed() {
console.error('[SOCKET] reconnect failed')
},
authFailed(payload) {
console.error('[SOCKET] auth failed', payload.message)
this.isSocketAuthenticated = false
},
init(payload) {
console.log('Init Payload', payload)
@ -127,7 +151,7 @@ export default {
this.$store.commit('users/setUsersOnline', payload.usersOnline)
}
this.$eventBus.$emit('socket_init')
this.isSocketAuthenticated = true
},
streamOpen(stream) {
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
@ -354,6 +378,15 @@ export default {
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
},
initializeSocket() {
if (this.$root.socket) {
// Can happen in dev due to hot reload
console.warn('Socket already initialized')
this.socket = this.$root.socket
this.isSocketConnected = this.$root.socket?.connected
this.isFirstSocketConnection = false
this.socketConnectionToastId = null
return
}
this.socket = this.$nuxtSocket({
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
persist: 'main',
@ -364,6 +397,7 @@ export default {
path: `${this.$config.routerBasePath}/socket.io`
})
this.$root.socket = this.socket
this.isSocketAuthenticated = false
console.log('Socket initialized')
// Pre-defined socket events
@ -377,6 +411,7 @@ export default {
// Event received after authorizing socket
this.socket.on('init', this.init)
this.socket.on('auth_failed', this.authFailed)
// Stream Listeners
this.socket.on('stream_open', this.streamOpen)
@ -571,6 +606,7 @@ export default {
this.updateBodyClass()
this.resize()
this.$eventBus.$on('change-lang', this.changeLanguage)
this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown)
@ -594,6 +630,7 @@ export default {
},
beforeDestroy() {
this.$eventBus.$off('change-lang', this.changeLanguage)
this.$eventBus.$off('token_refreshed', this.tokenRefreshed)
window.removeEventListener('resize', this.resize)
window.removeEventListener('keydown', this.keyDown)
}

View file

@ -73,7 +73,8 @@ module.exports = {
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {
baseURL: routerBasePath
baseURL: routerBasePath,
progress: false
},
// nuxt/pwa https://pwa.nuxtjs.org

View file

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

View file

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

View file

@ -182,18 +182,19 @@ export default {
password: this.password,
newPassword: this.newPassword
})
.then((res) => {
if (res.success) {
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
this.resetForm()
} else {
this.$toast.error(res.error || this.$strings.ToastUnknownError)
}
this.changingPassword = false
.then(() => {
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
this.resetForm()
})
.catch((error) => {
console.error(error)
this.$toast.error(this.$strings.ToastUnknownError)
console.error('Failed to change password', error)
let errorMessage = this.$strings.ToastUnknownError
if (error.response?.data && typeof error.response.data === 'string') {
errorMessage = error.response.data
}
this.$toast.error(errorMessage)
})
.finally(() => {
this.changingPassword = false
})
},

View file

@ -53,6 +53,7 @@ export default {
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
else if (pageName === 'stats') return this.$strings.HeaderYourStats
else if (pageName === 'users') return this.$strings.HeaderUsers
else if (pageName === 'api-keys') return this.$strings.HeaderApiKeys
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
else if (pageName === 'email') return this.$strings.HeaderEmail

View file

@ -0,0 +1,84 @@
<template>
<div>
<app-settings-content :header-text="$strings.HeaderApiKeys">
<template #header-items>
<div v-if="numApiKeys" class="mx-2 px-1.5 rounded-lg bg-primary/50 text-gray-300/90 text-sm inline-flex items-center justify-center">
<span>{{ numApiKeys }}</span>
</div>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/api-keys" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
<div class="grow" />
<ui-btn color="bg-primary" :disabled="loadingUsers || users.length === 0" small @click="setShowApiKeyModal()">{{ $strings.ButtonAddApiKey }}</ui-btn>
</template>
<tables-api-keys-table ref="apiKeysTable" class="pt-2" @edit="setShowApiKeyModal" @numApiKeys="(count) => (numApiKeys = count)" />
</app-settings-content>
<modals-api-key-modal ref="apiKeyModal" v-model="showApiKeyModal" :api-key="selectedApiKey" :users="users" @created="apiKeyCreated" @updated="apiKeyUpdated" />
<modals-api-key-created-modal ref="apiKeyCreatedModal" v-model="showApiKeyCreatedModal" :api-key="selectedApiKey" />
</div>
</template>
<script>
export default {
asyncData({ store, redirect }) {
if (!store.getters['user/getIsAdminOrUp']) {
redirect('/')
}
},
data() {
return {
loadingUsers: false,
selectedApiKey: null,
showApiKeyModal: false,
showApiKeyCreatedModal: false,
numApiKeys: 0,
users: []
}
},
methods: {
apiKeyCreated(apiKey) {
this.numApiKeys++
this.selectedApiKey = apiKey
this.showApiKeyCreatedModal = true
if (this.$refs.apiKeysTable) {
this.$refs.apiKeysTable.addApiKey(apiKey)
}
},
apiKeyUpdated(apiKey) {
if (this.$refs.apiKeysTable) {
this.$refs.apiKeysTable.updateApiKey(apiKey)
}
},
setShowApiKeyModal(selectedApiKey) {
this.selectedApiKey = selectedApiKey
this.showApiKeyModal = true
},
loadUsers() {
this.loadingUsers = true
this.$axios
.$get('/api/users')
.then((res) => {
this.users = res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
})
.catch((error) => {
console.error('Failed', error)
})
.finally(() => {
this.loadingUsers = false
})
}
},
mounted() {
this.loadUsers()
},
beforeDestroy() {}
}
</script>

View file

@ -78,10 +78,10 @@ export default {
},
computed: {
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {

View file

@ -250,10 +250,10 @@ export default {
return user?.username || null
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
},
numSelected() {
return this.listeningSessions.filter((s) => s.selected).length

View file

@ -13,8 +13,8 @@
<widgets-online-indicator :value="!!userOnline" />
<h1 class="text-xl pl-2">{{ username }}</h1>
</div>
<div v-if="userToken" class="flex text-xs mt-4">
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly show-copy />
<div v-if="legacyToken" class="flex text-xs mt-4">
<ui-text-input-with-label label="Legacy API Token" :value="legacyToken" readonly show-copy />
</div>
<div class="w-full h-px bg-white/10 my-2" />
<div class="py-2">
@ -100,9 +100,12 @@ export default {
}
},
computed: {
userToken() {
legacyToken() {
return this.user.token
},
userToken() {
return this.user.accessToken
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
@ -129,10 +132,10 @@ export default {
return this.listeningSessions.sessions[0]
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {

View file

@ -98,10 +98,10 @@ export default {
return this.$store.getters['users/getIsUserOnline'](this.user.id)
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {

View file

@ -193,7 +193,7 @@ export default {
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/download?token=${this.userToken}`
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']

View file

@ -10,7 +10,7 @@
</tr>
<tr v-for="narrator in narrators" :key="narrator.id">
<td>
<p v-if="selectedNarrator?.id !== narrator.id" class="text-sm md:text-base text-gray-100">{{ narrator.name }}</p>
<nuxt-link v-if="selectedNarrator?.id !== narrator.id" :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${narrator.id}`" class="text-sm md:text-base text-gray-100 hover:underline">{{ narrator.name }}</nuxt-link>
<form v-else @submit.prevent="saveClick">
<ui-text-input v-model="newNarratorName" />
</form>

View file

@ -142,7 +142,7 @@ export default {
return episodeIds
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
return this.$store.getters['getServerSetting']('dateFormat')
}
},
methods: {

View file

@ -40,6 +40,15 @@
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<div v-if="showNewAuthSystemMessage" class="mb-4">
<widgets-alert type="warning">
<div>
<p>{{ $strings.MessageAuthenticationSecurityMessage }}</p>
<a v-if="showNewAuthSystemAdminMessage" href="https://github.com/advplyr/audiobookshelf/discussions/4460" target="_blank" class="underline">{{ $strings.LabelMoreInfo }}</a>
</div>
</widgets-alert>
</div>
<form v-show="login_local" @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
@ -85,7 +94,10 @@ export default {
MetadataPath: '',
login_local: true,
login_openid: false,
authFormData: null
authFormData: null,
// New JWT auth system re-login flags
showNewAuthSystemMessage: false,
showNewAuthSystemAdminMessage: false
}
},
watch: {
@ -179,11 +191,14 @@ export default {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
this.$store.commit('user/setUser', user)
this.$store.commit('user/setAccessToken', user.accessToken)
this.$store.dispatch('user/loadUserSettings')
},
async submitForm() {
this.error = null
this.showNewAuthSystemMessage = false
this.showNewAuthSystemAdminMessage = false
this.processing = true
const payload = {
@ -217,15 +232,24 @@ export default {
}
})
.then((res) => {
// Force re-login if user is using an old token with no expiration
if (res.user.isOldToken) {
this.username = res.user.username
this.showNewAuthSystemMessage = true
// Admin user sees link to github discussion
this.showNewAuthSystemAdminMessage = res.user.type === 'admin' || res.user.type === 'root'
return false
}
this.setUser(res)
this.processing = false
return true
})
.catch((error) => {
console.error('Authorize error', error)
this.processing = false
return false
})
.finally(() => {
this.processing = false
})
},
checkStatus() {
this.processing = true
@ -280,8 +304,9 @@ export default {
}
},
async mounted() {
if (this.$route.query?.setToken) {
localStorage.setItem('token', this.$route.query.setToken)
// Token passed as query parameter after successful oidc login
if (this.$route.query?.accessToken) {
localStorage.setItem('token', this.$route.query.accessToken)
}
if (localStorage.getItem('token')) {
if (await this.checkAuth()) return // if valid user no need to check status

View file

@ -1,4 +1,19 @@
export default function ({ $axios, store, $config }) {
export default function ({ $axios, store, $root, app }) {
// Track if we're currently refreshing to prevent multiple refresh attempts
let isRefreshing = false
let failedQueue = []
const processQueue = (error, token = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error)
} else {
resolve(token)
}
})
failedQueue = []
}
$axios.onRequest((config) => {
if (!config.url) {
console.error('Axios request invalid config', config)
@ -7,7 +22,7 @@ export default function ({ $axios, store, $config }) {
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
return
}
const bearerToken = store.state.user.user?.token || null
const bearerToken = store.getters['user/getToken']
if (bearerToken) {
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
}
@ -17,9 +32,79 @@ export default function ({ $axios, store, $config }) {
}
})
$axios.onError((error) => {
$axios.onError(async (error) => {
const originalRequest = error.config
const code = parseInt(error.response && error.response.status)
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
console.error('Axios error', code, message)
// Handle 401 Unauthorized (token expired)
if (code === 401 && !originalRequest._retry) {
// Skip refresh for auth endpoints to prevent infinite loops
if (originalRequest.url === '/auth/refresh' || originalRequest.url === '/login') {
// Refresh failed or login failed, redirect to login
store.commit('user/setUser', null)
store.commit('user/setAccessToken', null)
app.router.push('/login')
return Promise.reject(error)
}
if (isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
})
.then((token) => {
if (!originalRequest.headers) {
originalRequest.headers = {}
}
originalRequest.headers['Authorization'] = `Bearer ${token}`
return $axios(originalRequest)
})
.catch((err) => {
return Promise.reject(err)
})
}
originalRequest._retry = true
isRefreshing = true
try {
// Attempt to refresh the token
// Updates store if successful, otherwise clears store and throw error
const newAccessToken = await store.dispatch('user/refreshToken')
if (!newAccessToken) {
console.error('No new access token received')
return Promise.reject(error)
}
// Update the original request with new token
if (!originalRequest.headers) {
originalRequest.headers = {}
}
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`
// Process any queued requests
processQueue(null, newAccessToken)
// Retry the original request
return $axios(originalRequest)
} catch (refreshError) {
console.error('Token refresh failed:', refreshError)
// Process queued requests with error
processQueue(refreshError, null)
// Redirect to login
app.router.push('/login')
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
})
}

View file

@ -1,5 +1,6 @@
export const state = () => ({
user: null,
accessToken: null,
settings: {
orderBy: 'media.metadata.title',
orderDesc: false,
@ -25,19 +26,19 @@ export const getters = {
getIsRoot: (state) => state.user && state.user.type === 'root',
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
getToken: (state) => {
return state.user?.token || null
return state.accessToken || null
},
getUserMediaProgress:
(state) =>
(libraryItemId, episodeId = null) => {
if (!state.user.mediaProgress) return null
if (!state.user?.mediaProgress) return null
return state.user.mediaProgress.find((li) => {
if (episodeId && li.episodeId !== episodeId) return false
return li.libraryItemId == libraryItemId
})
},
getUserBookmarksForItem: (state) => (libraryItemId) => {
if (!state.user.bookmarks) return []
if (!state.user?.bookmarks) return []
return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId)
},
getUserSetting: (state) => (key) => {
@ -58,6 +59,9 @@ export const getters = {
getUserCanAccessAllLibraries: (state) => {
return !!state.user?.permissions?.accessAllLibraries
},
getUserCanAccessExplicitContent: (state) => {
return !!state.user?.permissions?.accessExplicitContent
},
getLibrariesAccessible: (state, getters) => {
if (!state.user) return []
if (getters.getUserCanAccessAllLibraries) return []
@ -142,21 +146,42 @@ export const actions = {
} catch (error) {
console.error('Failed to load userSettings from local storage', error)
}
},
refreshToken({ state, commit }) {
return this.$axios
.$post('/auth/refresh')
.then(async (response) => {
const newAccessToken = response.user.accessToken
commit('setUser', response.user)
commit('setAccessToken', newAccessToken)
// Emit event used to re-authenticate socket in default.vue since $root is not available here
if (this.$eventBus) {
this.$eventBus.$emit('token_refreshed', newAccessToken)
}
return newAccessToken
})
.catch((error) => {
console.error('Failed to refresh token', error)
commit('setUser', null)
commit('setAccessToken', null)
// Calling function handles redirect to login
throw error
})
}
}
export const mutations = {
setUser(state, user) {
state.user = user
if (user) {
if (user.token) localStorage.setItem('token', user.token)
} else {
localStorage.removeItem('token')
}
},
setUserToken(state, token) {
state.user.token = token
localStorage.setItem('token', token)
setAccessToken(state, token) {
if (!token) {
localStorage.removeItem('token')
state.accessToken = null
} else {
state.accessToken = token
localStorage.setItem('token', token)
}
},
updateMediaProgress(state, { id, data }) {
if (!state.user) return

View file

@ -346,11 +346,11 @@
"LabelExample": "Příklad",
"LabelExpandSeries": "Rozbalit série",
"LabelExpandSubSeries": "Rozbalit podsérie",
"LabelExplicit": "Explicitní",
"LabelExplicit": "Explicitně",
"LabelExplicitChecked": "Explicitní (zaškrtnuto)",
"LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)",
"LabelExportOPML": "Export OPML",
"LabelFeedURL": "URL zdroje",
"LabelFeedURL": "URL kanálu",
"LabelFetchingMetadata": "Získávání metadat",
"LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vzniku souboru",
@ -757,6 +757,7 @@
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
"MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?",
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodeNote": "Poznámka: Tím se zvukový soubor neodstraní, pokud nepřepnete volbu “Tvrdé odstranění souboru“",
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
"MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?",
"MessageConfirmRemoveMetadataFiles": "Jste si jisti, že chcete odstranit všechny metadata.{0} soubory ve složkách s položkami ve vaší knihovně?",
@ -918,6 +919,8 @@
"NotificationOnBackupCompletedDescription": "Spuštěno po dokončení zálohování",
"NotificationOnBackupFailedDescription": "Spuštěno pokud zálohování selže",
"NotificationOnEpisodeDownloadedDescription": "Spuštěno při automatickém stažení epizody podcastu",
"NotificationOnRSSFeedDisabledDescription": "Aktivováno když je automatické stahování pozastaveno z důvodu příliš mnoho neůspěšných pokusů",
"NotificationOnRSSFeedFailedDescription": "Aktivováno když selže RSS kanál pro stahování epizod",
"NotificationOnTestDescription": "Akce pro otestování upozorňovacího systému",
"PlaceholderNewCollection": "Nový název kolekce",
"PlaceholderNewFolderPath": "Nová cesta ke složce",
@ -962,7 +965,7 @@
"ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu",
"ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu",
"ToastBackupUploadSuccess": "Záloha nahrána",
"ToastBatchApplyDetailsToItemsSuccess": "Detaily aplikované na položky",
"ToastBatchApplyDetailsToItemsSuccess": "Detaily byly aplikované na položky",
"ToastBatchDeleteFailed": "Hromadné smazání selhalo",
"ToastBatchDeleteSuccess": "Hromadné smazání proběhlo úspěšně",
"ToastBatchQuickMatchFailed": "Rychlá schoda dávky se nezdařila!",
@ -976,7 +979,7 @@
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersInvalidShiftAmountLast": "Nesprávná délka posunu. Čas začátku poslední kapitoly by přesáhl dobu trvání této audioknihy.",
"ToastChaptersInvalidShiftAmountStart": "Nesprávná délka posunu. První kapitola by měla nulovou nebo zápornou délku a byla by přepsána druhou kapitolou. Zvětšete počáteční délku druhé kapitoly.",
"ToastChaptersInvalidShiftAmountStart": "Nesprávná délka posunu. První kapitola by měla nulovou nebo zápornou délku a byla by přepsána druhou kapitolou. Zvětšete čas začátku druhé kapitoly.",
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
"ToastChaptersRemoved": "Kapitoly odstraněny",
"ToastChaptersUpdated": "Kapitola aktualizována",

View file

@ -177,6 +177,7 @@
"HeaderPlaylist": "Afspilningsliste",
"HeaderPlaylistItems": "Afspilningsliste Elementer",
"HeaderPodcastsToAdd": "Podcasts til Tilføjelse",
"HeaderPresets": "Forudindstillinger",
"HeaderPreviewCover": "Forhåndsvis Omslag",
"HeaderRSSFeedGeneral": "RSS Detaljer",
"HeaderRSSFeedIsOpen": "RSS Feed er Åben",
@ -530,6 +531,7 @@
"LabelReleaseDate": "Udgivelsesdato",
"LabelRemoveAllMetadataAbs": "Fjern alle metadata.abs filer",
"LabelRemoveAllMetadataJson": "Fjern alle metadata.json filer",
"LabelRemoveAudibleBranding": "Fjern Audible intro og outro fra kapitler",
"LabelRemoveCover": "Fjern omslag",
"LabelRemoveMetadataFile": "Fjern alle metadata filer i biblioteksmapper",
"LabelRemoveMetadataFileHelp": "Fjern alle metadata.json og metadata.abs filer i dine {0} mapper.",
@ -604,6 +606,7 @@
"LabelSlug": "Snegl",
"LabelSortAscending": "Stigende",
"LabelSortDescending": "Faldende",
"LabelSortPubDate": "Sortér Pub Dato",
"LabelStart": "Start",
"LabelStartTime": "Starttid",
"LabelStarted": "Startet",
@ -704,6 +707,8 @@
"LabelYourProgress": "Din fremgang",
"MessageAddToPlayerQueue": "Tilføj til afspilningskø",
"MessageAppriseDescription": "For at bruge denne funktion skal du have en instans af <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kørende eller en API, der håndterer de samme anmodninger. <br /> Apprise API-webadressen skal være den fulde URL-sti for at sende underretningen, f.eks. hvis din API-instans er tilgængelig på <code>http://192.168.1.1:8337</code>, så skal du bruge <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Sikr dig at du bruger ASIN fra den korrekte Audible region, ikke Amazon.",
"MessageAuthenticationOIDCChangesRestart": "Genstart sin server efter du har gemt for at bekræfte OIDC ændringer.",
"MessageBackupsDescription": "Backups inkluderer brugere, brugerfremskridt, biblioteksvareoplysninger, serverindstillinger og billeder gemt i <code>/metadata/items</code> og <code>/metadata/authors</code>. Backups inkluderer <strong>ikke</strong> nogen filer gemt i dine biblioteksmapper.",
"MessageBackupsLocationEditNote": "Note: Opdatering af backup sti vil ikke fjerne eller modificere eksisterende backups",
"MessageBackupsLocationNoEditNote": "Note: Backup sti er sat igennem miljøvariabel og kan ikke ændres her.",
@ -722,6 +727,7 @@
"MessageChapterErrorStartGteDuration": "Ugyldig starttid skal være mindre end lydbogens varighed",
"MessageChapterErrorStartLtPrev": "Ugyldig starttid skal være større end eller lig med den foregående kapitels starttid",
"MessageChapterStartIsAfter": "Kapitelstarten er efter slutningen af din lydbog",
"MessageChaptersNotFound": "Kapitler ikke fundet",
"MessageCheckingCron": "Tjekker cron...",
"MessageConfirmCloseFeed": "Er du sikker på, at du vil lukke dette feed?",
"MessageConfirmDeleteBackup": "Er du sikker på, at du vil slette backup for {0}?",
@ -778,6 +784,7 @@
"MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.",
"MessageImportantNotice": "Vigtig besked!",
"MessageInsertChapterBelow": "Indsæt kapitel nedenfor",
"MessageInvalidAsin": "Ugyldig ASIN",
"MessageItemsSelected": "{0} elementer valgt",
"MessageItemsUpdated": "{0} elementer opdateret",
"MessageJoinUsOn": "Deltag i os på",
@ -849,6 +856,7 @@
"MessageScheduleRunEveryWeekdayAtTime": "Kør hvert {0} af {1}",
"MessageSearchResultsFor": "Søgeresultater for",
"MessageSelected": "{0} valgt",
"MessageSeriesSequenceCannotContainSpaces": "Serie sekvens kan ikke indeholde mellemrum",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
"MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn",
"MessageShareExpirationWillBe": "Udløb vil være <strong>{0}</strong>",
@ -910,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "Udløst når backup er færdig",
"NotificationOnBackupFailedDescription": "Udløst når backup fejler",
"NotificationOnEpisodeDownloadedDescription": "Udløst når et podcast afsnit er automatisk downloadet",
"NotificationOnRSSFeedDisabledDescription": "Aktiveret når automatiske episode-downloads er slået fra, på grund af for mange forsøg",
"NotificationOnRSSFeedFailedDescription": "Aktiveret når anmodning om RSS-feedet fejler for en automatisk episode-download",
"NotificationOnTestDescription": "Event for test af notifikationssystemet",
"PlaceholderNewCollection": "Nyt samlingnavn",
"PlaceholderNewFolderPath": "Ny mappes sti",
@ -954,6 +964,7 @@
"ToastBackupRestoreFailed": "Mislykkedes gendannelse af sikkerhedskopi",
"ToastBackupUploadFailed": "Mislykkedes upload af sikkerhedskopi",
"ToastBackupUploadSuccess": "Sikkerhedskopi uploadet",
"ToastBatchApplyDetailsToItemsSuccess": "Detaljer bekræftet på element",
"ToastBatchDeleteFailed": "Batch slet fejlede",
"ToastBatchDeleteSuccess": "Batch slet succes",
"ToastBatchQuickMatchFailed": "Batch Hurtig Match fejlede!",

View file

@ -32,7 +32,7 @@
"ButtonEditChapters": "Kapitel bearbeiten",
"ButtonEditPodcast": "Podcast bearbeiten",
"ButtonEnable": "Aktivieren",
"ButtonFireAndFail": "Abfeuern und versagen",
"ButtonFireAndFail": "Abschicken und fehlschlagen",
"ButtonFireOnTest": "Test-Event abfeuern",
"ButtonForceReScan": "Komplett-Scan (alle Medien)",
"ButtonFullPath": "Vollständiger Pfad",
@ -53,7 +53,7 @@
"ButtonNext": "Vor",
"ButtonNextChapter": "Nächstes Kapitel",
"ButtonNextItemInQueue": "Das nächste Element in der Warteschlange",
"ButtonOk": "Einverstanden",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Feed öffnen",
"ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pausieren",
@ -514,7 +514,7 @@
"LabelPublishers": "Herausgeber",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed öffnen",
"LabelRSSFeedOpen": "RSS Feed offen",
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
"LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
"LabelRSSFeedURL": "RSS-Feed-URL",
@ -708,7 +708,7 @@
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.",
"MessageAsinCheck": "Stellen Sie sicher, dass Sie die ASIN aus der richtigen Audible Region verwenden, nicht Amazon.",
"MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muß der Server neugestartet werden um die OIDC Änderungen zu übernehmen.",
"MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.",
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
"MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert",
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
@ -757,6 +757,7 @@
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemoveEpisodeNote": "Hinweis: Die Audiodatei wird nicht gelöscht, es sei denn \"Datei dauerhaft löschen\" ist aktiviert",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
"MessageConfirmRemoveMetadataFiles": "Bist du sicher, dass du alle metadata.{0} Dateien in deinen Bibliotheksordnern löschen willst?",
@ -852,7 +853,7 @@
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?",
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am",
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<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 Anwender wird empfohlen, diese Funktion deaktiviert und die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung wird Änderungen in den Bibliotheksordnern automatisch erkennen. Die Ordnerüberwachung funktioniert nicht mit allen Dateisystemen (wie NFS), hier kann stattdessen die automatischen Bibliothekssuchen verwendet werden.",
"MessageScheduleRunEveryWeekdayAtTime": "Immer {0} um {1} ausführen",
"MessageSearchResultsFor": "Suchergebnisse für",
"MessageSelected": "{0} ausgewählt",
@ -918,6 +919,8 @@
"NotificationOnBackupCompletedDescription": "Wird ausgeführt wenn ein Backup erstellt wurde",
"NotificationOnBackupFailedDescription": "Wird ausgeführt wenn ein Backup fehlgeschlagen ist",
"NotificationOnEpisodeDownloadedDescription": "Wird ausgeführt wenn eine Podcast Folge automatisch heruntergeladen wird",
"NotificationOnRSSFeedDisabledDescription": "Wird ausgeführt wenn automatische Downloads von Episoden wegen zu vielen fehlgeschlagenen Versuchen deaktiviert sind",
"NotificationOnRSSFeedFailedDescription": "Wird ausgelöst, wenn die RSS-Feed-Anforderung für einen automatischen Episoden-Download fehlschlägt",
"NotificationOnTestDescription": "Wird ausgeführt wenn das Benachrichtigungssystem getestet wird",
"PlaceholderNewCollection": "Neuer Sammlungsname",
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",

View file

@ -1,5 +1,6 @@
{
"ButtonAdd": "Add",
"ButtonAddApiKey": "Add API Key",
"ButtonAddChapters": "Add Chapters",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
@ -20,6 +21,7 @@
"ButtonChooseAFolder": "Choose a folder",
"ButtonChooseFiles": "Choose files",
"ButtonClearFilter": "Clear Filter",
"ButtonClose": "Close",
"ButtonCloseFeed": "Close Feed",
"ButtonCloseSession": "Close Open Session",
"ButtonCollections": "Collections",
@ -119,6 +121,7 @@
"HeaderAccount": "Account",
"HeaderAddCustomMetadataProvider": "Add Custom Metadata Provider",
"HeaderAdvanced": "Advanced",
"HeaderApiKeys": "API Keys",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
"HeaderAudioTracks": "Audio Tracks",
"HeaderAudiobookTools": "Audiobook File Management Tools",
@ -162,6 +165,7 @@
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metadata to embed",
"HeaderNewAccount": "New Account",
"HeaderNewApiKey": "New API Key",
"HeaderNewLibrary": "New Library",
"HeaderNotificationCreate": "Create Notification",
"HeaderNotificationUpdate": "Update Notification",
@ -206,6 +210,7 @@
"HeaderTableOfContents": "Table of Contents",
"HeaderTools": "Tools",
"HeaderUpdateAccount": "Update Account",
"HeaderUpdateApiKey": "Update API Key",
"HeaderUpdateAuthor": "Update Author",
"HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library",
@ -235,6 +240,10 @@
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library",
"LabelApiKeyCreated": "API Key \"{0}\" created successfully.",
"LabelApiKeyCreatedDescription": "Make sure to copy the API key now as you will not be able to see this again.",
"LabelApiKeyUser": "Act on behalf of user",
"LabelApiKeyUserDescription": "This API key will have the same permissions as the user it is acting on behalf of. This will appear the same in logs as if the user was making the request.",
"LabelApiToken": "API Token",
"LabelAppend": "Append",
"LabelAudioBitrate": "Audio Bitrate (e.g. 128k)",
@ -346,6 +355,10 @@
"LabelExample": "Example",
"LabelExpandSeries": "Expand Series",
"LabelExpandSubSeries": "Expand Sub Series",
"LabelExpired": "Expired",
"LabelExpiresAt": "Expires At",
"LabelExpiresInSeconds": "Expires in (seconds)",
"LabelExpiresNever": "Never",
"LabelExplicit": "Explicit",
"LabelExplicitChecked": "Explicit (checked)",
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
@ -455,6 +468,7 @@
"LabelNewestEpisodes": "Newest Episodes",
"LabelNextBackupDate": "Next backup date",
"LabelNextScheduledRun": "Next scheduled run",
"LabelNoApiKeys": "No API keys",
"LabelNoCustomMetadataProviders": "No custom metadata providers",
"LabelNoEpisodesSelected": "No episodes selected",
"LabelNotFinished": "Not Finished",
@ -544,6 +558,7 @@
"LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUser": "Select user",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequence",
@ -709,6 +724,7 @@
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Ensure you are using the ASIN from the correct Audible region, not Amazon.",
"MessageAuthenticationOIDCChangesRestart": "Restart your server after saving to apply OIDC changes.",
"MessageAuthenticationSecurityMessage": "Authentication has been improved for security. All users are required to re-login.",
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.",
"MessageBackupsLocationEditNote": "Note: Updating the backup location will not move or modify existing backups",
"MessageBackupsLocationNoEditNote": "Note: The backup location is set through an environment variable and cannot be changed here.",
@ -730,6 +746,7 @@
"MessageChaptersNotFound": "Chapters not found",
"MessageCheckingCron": "Checking cron...",
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
"MessageConfirmDeleteApiKey": "Are you sure you want to delete API key \"{0}\"?",
"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?",
@ -757,6 +774,7 @@
"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}\"?",
"MessageConfirmRemoveEpisodeNote": "Note: This does not delete the audio file unless toggling \"Hard delete file\"",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveMetadataFiles": "Are you sure you want to remove all metadata.{0} files in your library item folders?",
@ -1000,6 +1018,8 @@
"ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared",
"ToastEpisodeUpdateSuccess": "{0} episodes updated",
"ToastErrorCannotShare": "Cannot share natively on this device",
"ToastFailedToCreate": "Failed to create",
"ToastFailedToDelete": "Failed to delete",
"ToastFailedToLoadData": "Failed to load data",
"ToastFailedToMatch": "Failed to match",
"ToastFailedToShare": "Failed to share",
@ -1031,6 +1051,7 @@
"ToastMustHaveAtLeastOnePath": "Must have at least one path",
"ToastNameEmailRequired": "Name and email are required",
"ToastNameRequired": "Name is required",
"ToastNewApiKeyUserError": "Must select a user",
"ToastNewEpisodesFound": "{0} new episodes found",
"ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
"ToastNewUserCreatedSuccess": "New account created",

View file

@ -9,6 +9,9 @@
"ButtonApply": "લાગુ કરો",
"ButtonApplyChapters": "પ્રકરણો લાગુ કરો",
"ButtonAuthors": "લેખકો",
"ButtonBack": "પાછા",
"ButtonBatchEditPopulateFromExisting": "હાલની માહિતીમાંથી ભરો",
"ButtonBatchEditPopulateMapDetails": "નકશાની વિગત ભરો",
"ButtonBrowseForFolder": "ફોલ્ડર માટે જુઓ",
"ButtonCancel": "રદ કરો",
"ButtonCancelEncode": "એન્કોડ રદ કરો",
@ -27,11 +30,14 @@
"ButtonEdit": "સંપાદિત કરો",
"ButtonEditChapters": "પ્રકરણો સંપાદિત કરો",
"ButtonEditPodcast": "પોડકાસ્ટ સંપાદિત કરો",
"ButtonEnable": "સક્રિય કરો",
"ButtonForceReScan": "બળપૂર્વક ફરીથી સ્કેન કરો",
"ButtonFullPath": "સંપૂર્ણ પથ",
"ButtonHide": "છુપાવો",
"ButtonHome": "ઘર",
"ButtonIssues": "સમસ્યાઓ",
"ButtonJumpBackward": "પાછળ જાવો",
"ButtonJumpForward": "આગળ જાવો",
"ButtonLatest": "નવીનતમ",
"ButtonLibrary": "પુસ્તકાલય",
"ButtonLogout": "લૉગ આઉટ",
@ -41,19 +47,32 @@
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
"ButtonNevermind": "કંઈ વાંધો નહીં",
"ButtonNext": "આગળ જાઓ",
"ButtonNextChapter": "આગળનું અધ્યાય",
"ButtonNextItemInQueue": "કતારમાં આવતું આગળનું અધ્યાય",
"ButtonOk": "ઓકે",
"ButtonOpenFeed": "ફીડ ખોલો",
"ButtonOpenManager": "મેનેજર ખોલો",
"ButtonPause": "વિરામ",
"ButtonPlay": "ચલાવો",
"ButtonPlayAll": "બધું ચલાવો",
"ButtonPlaying": "ચલાવી રહ્યું છે",
"ButtonPlaylists": "પ્લેલિસ્ટ",
"ButtonPrevious": "પાછળનું",
"ButtonPreviousChapter": "પાછળનું અધ્યાય",
"ButtonProbeAudioFile": "ઑડિયો ફાઇલ તપાસો",
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
"ButtonQueueAddItem": "કતારમાં ઉમેરો",
"ButtonQueueRemoveItem": "કતારથી કાઢી નાખો",
"ButtonQuickEmbed": "ઝડપથી સમાવેશ કરો",
"ButtonQuickEmbedMetadata": "ઝડપથી મેટાડેટા સમાવવો",
"ButtonQuickMatch": "ઝડપી મેળ ખવડાવો",
"ButtonReScan": "ફરીથી સ્કેન કરો",
"ButtonRead": "વાંચો",
"ButtonReadLess": "ઓછું વાંચો",
"ButtonReadMore": "વધારે વાંચો",
"ButtonRefresh": "તાજું કરો",
"ButtonRemove": "કાઢી નાખો",
"ButtonRemoveAll": "બધું કાઢી નાખો",
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
@ -68,16 +87,21 @@
"ButtonSaveTracklist": "ટ્રેક યાદી સાચવો",
"ButtonScan": "સ્કેન કરો",
"ButtonScanLibrary": "પુસ્તકાલય સ્કેન કરો",
"ButtonScrollLeft": "ડાબે",
"ButtonScrollRight": "જમણે",
"ButtonSearch": "શોધો",
"ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો",
"ButtonSeries": "સિરીઝ",
"ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો",
"ButtonShare": "શેર કરો",
"ButtonShiftTimes": "સમય શિફ્ટ કરો",
"ButtonShow": "બતાવો",
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
"ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
"ButtonStats": "આંકડા",
"ButtonSubmit": "સબમિટ કરો",
"ButtonTest": "પરખ કરો",
"ButtonUnlinkOpenId": "OpenID દૂર કરો",
"ButtonUpload": "અપલોડ કરો",
"ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
"ButtonUploadCover": "કવર અપલોડ કરો",
@ -86,11 +110,16 @@
"ButtonUserEdit": "વપરાશકર્તા {0} સંપાદિત કરો",
"ButtonViewAll": "બધું જુઓ",
"ButtonYes": "હા",
"ErrorUploadFetchMetadataAPI": "મેટાડેટા મેળવવામાં તકલીફ આવી",
"ErrorUploadFetchMetadataNoResults": "મેટાડેટા મેળવી શક્યા નહીં કૃપા કરીને શીર્ષક અને/અથવા લેખકનું નામ અપડેટ કરવાનો પ્રયત્ન કરો",
"ErrorUploadLacksTitle": "શીર્ષક હોવું આવશ્યક છે",
"HeaderAccount": "એકાઉન્ટ",
"HeaderAddCustomMetadataProvider": "કસ્ટમ મેટાડેટા પ્રોવાઇડર ઉમેરો",
"HeaderAdvanced": "અડ્વાન્સડ",
"HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ",
"HeaderAudioTracks": "ઓડિયો ટ્રેક્સ",
"HeaderAudiobookTools": "ઓડિયોબુક ફાઇલ વ્યવસ્થાપન ટૂલ્સ",
"HeaderAuthentication": "પ્રમાણીકરણ",
"HeaderBackups": "બેકઅપ્સ",
"HeaderChangePassword": "પાસવર્ડ બદલો",
"HeaderChapters": "પ્રકરણો",
@ -99,6 +128,7 @@
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
"HeaderCover": "આવરણ",
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
"HeaderCustomMetadataProviders": "કસ્ટમ મેટાડેટા પ્રોવાઇડર્સ",
"HeaderDetails": "વિગતો",
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
"HeaderEbookFiles": "ઇબુક ફાઇલો",
@ -129,6 +159,7 @@
"HeaderMetadataToEmbed": "એમ્બેડ કરવા માટે મેટાડેટા",
"HeaderNewAccount": "નવું એકાઉન્ટ",
"HeaderNewLibrary": "નવી પુસ્તકાલય",
"HeaderNotificationCreate": "સૂચના બનાવો",
"HeaderNotifications": "સૂચનાઓ",
"HeaderOpenRSSFeed": "RSS ફીડ ખોલો",
"HeaderOtherFiles": "અન્ય ફાઇલો",

View file

@ -918,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "Pokreće se po završetku sigurnosnog kopiranja",
"NotificationOnBackupFailedDescription": "Pokreće se kada sigurnosno kopiranje ne uspije",
"NotificationOnEpisodeDownloadedDescription": "Pokreće se kada se nastavak podcasta automatski preuzme",
"NotificationOnRSSFeedDisabledDescription": "Pokreće se kada su automatska preuzimanja nastavaka onemogućena zbog previše neuspjelih pokušaja",
"NotificationOnRSSFeedFailedDescription": "Pokreće se u slučaju pogreške pri pokušaju automatskog preuzimanja nastavka s RSS izvora",
"NotificationOnTestDescription": "Događaj za testiranje sustava obavijesti",
"PlaceholderNewCollection": "Ime nove zbirke",
"PlaceholderNewFolderPath": "Nova putanja mape",

View file

@ -278,7 +278,7 @@
"LabelCollapseSeries": "Sorozat összecsukása",
"LabelCollapseSubSeries": "Alszéria összecsukása",
"LabelCollection": "Gyűjtemény",
"LabelCollections": "Gyűjtemény",
"LabelCollections": "Gyűjtemények",
"LabelComplete": "Kész",
"LabelConfirmPassword": "Jelszó megerősítése",
"LabelContinueListening": "Hallgatás folytatása",
@ -757,6 +757,7 @@
"MessageConfirmRemoveAuthor": "Biztosan eltávolítja a(z) \"{0}\" szerzőt?",
"MessageConfirmRemoveCollection": "Biztosan eltávolítja a(z) \"{0}\" gyűjteményt?",
"MessageConfirmRemoveEpisode": "Biztosan eltávolítja a(z) \"{0}\" epizódot?",
"MessageConfirmRemoveEpisodeNote": "Megjegyzés: Ez nem törli a hangfájlt, kivéve, ha a \"Hangfájl végleges törlése\" be van kapcsolva",
"MessageConfirmRemoveEpisodes": "Biztosan eltávolítja a(z) {0} epizódot?",
"MessageConfirmRemoveListeningSessions": "Biztosan eltávolítja a(z) {0} hallgatási munkamenetet?",
"MessageConfirmRemoveMetadataFiles": "Biztos, hogy az összes metaadatot el akarja távolítani {0} fájl van könyvtár mappáiban?",

View file

@ -514,7 +514,7 @@
"LabelPublishers": "Editori",
"LabelRSSFeedCustomOwnerEmail": "E-mail del proprietario personalizzato",
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
"LabelRSSFeedOpen": "Flusso RSS aperto",
"LabelRSSFeedOpen": "Feed RSS aperto",
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
"LabelRSSFeedSlug": "Parole chiave del flusso RSS",
"LabelRSSFeedURL": "URL del flusso RSS",
@ -918,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "Attivato al completamento di un backup",
"NotificationOnBackupFailedDescription": "Attivato quando un backup fallisce",
"NotificationOnEpisodeDownloadedDescription": "Attivato quando un episodio di podcast viene scaricato automaticamente",
"NotificationOnRSSFeedDisabledDescription": "Attivato quando i download automatici degli episodi vengono disabilitati a causa di troppi tentativi falliti",
"NotificationOnRSSFeedFailedDescription": "Attivato quando la richiesta del feed RSS per il download automatico di un episodio fallisce",
"NotificationOnTestDescription": "test il sistema di notifica",
"PlaceholderNewCollection": "Nome Nuova Raccolta",
"PlaceholderNewFolderPath": "Nuovo Percorso Cartella",

View file

@ -24,7 +24,7 @@
"ButtonCloseSession": "Sluit Sessie",
"ButtonCollections": "Collecties",
"ButtonConfigureScanner": "Configureer scanner",
"ButtonCreate": "Creëer",
"ButtonCreate": "Aanmaken",
"ButtonCreateBackup": "Maak back-up",
"ButtonDelete": "Verwijder",
"ButtonDownloadQueue": "Wachtrij",
@ -43,9 +43,9 @@
"ButtonJumpForward": "Spring vooruit",
"ButtonLatest": "Meest recent",
"ButtonLibrary": "Bibliotheek",
"ButtonLogout": "Log uit",
"ButtonLogout": "Uitloggen",
"ButtonLookup": "Zoeken",
"ButtonManageTracks": "Beheer tracks",
"ButtonManageTracks": "Tracks beheren",
"ButtonMapChapterTitles": "Hoofdstuktitels mappen",
"ButtonMatchAllAuthors": "Alle auteurs matchen",
"ButtonMatchBooks": "Alle boeken matchen",
@ -72,7 +72,7 @@
"ButtonQuickEmbedMetadata": "Snel Metadata Insluiten",
"ButtonQuickMatch": "Snelle match",
"ButtonReScan": "Nieuwe scan",
"ButtonRead": "Lees",
"ButtonRead": "Lezen",
"ButtonReadLess": "Lees minder",
"ButtonReadMore": "Lees meer",
"ButtonRefresh": "Verversen",
@ -107,7 +107,7 @@
"ButtonUnlinkOpenId": "OpenID Ontkoppelen",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload back-up",
"ButtonUploadCover": "Upload cover",
"ButtonUploadCover": "Omslag uploaden",
"ButtonUploadOPMLFile": "Upload OPML-bestand",
"ButtonUserDelete": "Verwijder gebruiker {0}",
"ButtonUserEdit": "Wijzig gebruiker {0}",
@ -177,7 +177,8 @@
"HeaderPlaylist": "Afspeellijst",
"HeaderPlaylistItems": "Onderdelen in afspeellijst",
"HeaderPodcastsToAdd": "Toe te voegen podcasts",
"HeaderPreviewCover": "Preview cover",
"HeaderPresets": "Voorinstellingen",
"HeaderPreviewCover": "Voorbeeld omslag",
"HeaderRSSFeedGeneral": "RSS-details",
"HeaderRSSFeedIsOpen": "RSS-feed is open",
"HeaderRSSFeeds": "RSS-feeds",
@ -284,7 +285,7 @@
"LabelContinueReading": "Verder lezen",
"LabelContinueSeries": "Doorgaan met Serie",
"LabelCover": "Omslag",
"LabelCoverImageURL": "Coverafbeelding URL",
"LabelCoverImageURL": "Omslagafbeelding-URL",
"LabelCoverProvider": "Omslag bron",
"LabelCreatedAt": "Gecreëerd op",
"LabelCronExpression": "Cron-uitdrukking",
@ -321,7 +322,7 @@
"LabelEmailSettingsSecure": "Veilig",
"LabelEmailSettingsSecureHelp": "Als 'waar', dan gebruikt de verbinding TLS om met de server te verbinden. Als 'onwaar', dan wordt TLS gebruikt als de server de STARTTLS-extensie ondersteunt. In de meeste gevallen kies je voor 'waar' verbindt met poort 465. Voo poort 587 of 25, laat op 'onwaar'. (van nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test-adres",
"LabelEmbeddedCover": "Ingesloten cover",
"LabelEmbeddedCover": "Omslag in bestand",
"LabelEnable": "Inschakelen",
"LabelEncodingBackupLocation": "Er wordt een back-up van uw originele audiobestanden opgeslagen in:",
"LabelEncodingChaptersNotEmbedded": "Hoofdstukken zijn niet ingesloten in audioboeken met meerdere sporen.",
@ -330,7 +331,7 @@
"LabelEncodingInfoEmbedded": "Metagegevens worden ingesloten in de audiotracks in uw audioboekmap.",
"LabelEncodingStartedNavigation": "Eenmaal de taak is gestart kan u weg navigeren van deze pagina.",
"LabelEncodingTimeWarning": "Encoding kan tot 30 minuten duren.",
"LabelEncodingWarningAdvancedSettings": "Waarschuwing: update deze instellingen niet tenzij u bekend bent met de coderingsopties van ffmpeg.",
"LabelEncodingWarningAdvancedSettings": "Waarschuwing: pas deze instellingen niet aan tenzij u bekend bent met de coderingsopties van ffmpeg.",
"LabelEncodingWatcherDisabled": "Als u de watcher hebt uitgeschakeld, moet u het audioboek daarna opnieuw scannen.",
"LabelEnd": "Einde",
"LabelEndOfChapter": "Einde van het Hoofdstuk",
@ -372,7 +373,7 @@
"LabelFull": "Vol",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard-delete bestand",
"LabelHardDeleteFile": "Bestand permanent verwijderen",
"LabelHasEbook": "Heeft Ebook",
"LabelHasSupplementaryEbook": "Heeft aanvullend Ebook",
"LabelHideSubtitles": "Ondertitels Verstoppen",
@ -406,7 +407,7 @@
"LabelLastBookUpdated": "Laatst bijgewerkte boek",
"LabelLastSeen": "Laatst gezien",
"LabelLastTime": "Laatste keer",
"LabelLastUpdate": "Laatste update",
"LabelLastUpdate": "Laatste wijziging",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Enkele pagina",
"LabelLayoutSplitPage": "Gesplitste pagina",
@ -513,7 +514,7 @@
"LabelPublishers": "Uitgevers",
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
"LabelRSSFeedOpen": "RSS-feed open",
"LabelRSSFeedOpen": "RSS Feed Open",
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
"LabelRSSFeedSlug": "RSS-feed slug",
"LabelRSSFeedURL": "RSS-feed URL",
@ -531,7 +532,7 @@
"LabelRemoveAllMetadataAbs": "Verwijder alle metadata.abs bestanden",
"LabelRemoveAllMetadataJson": "Verwijder alle metadata.json bestanden",
"LabelRemoveAudibleBranding": "Verwijder Audible intro en outro uit hoofdstukken",
"LabelRemoveCover": "Verwijder cover",
"LabelRemoveCover": "Omslag verwijderen",
"LabelRemoveMetadataFile": "Verwijder metadata bestanden in bibliotheek item folders",
"LabelRemoveMetadataFileHelp": "Verwijder alle metadata.json en metadata.abs bestanden in uw {0} folders.",
"LabelRowsPerPage": "Rijen per pagina",
@ -559,7 +560,7 @@
"LabelSettingsAudiobooksOnlyHelp": "Deze instelling inschakelen zorgt ervoor dat ebook-bestanden genegeerd worden tenzij ze in een audiobook-map staan, in welk geval ze worden ingesteld als supplementaire ebooks",
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
"LabelSettingsChromecastSupport": "Chromecast ondersteuning",
"LabelSettingsDateFormat": "Datum format",
"LabelSettingsDateFormat": "Datumnotatie",
"LabelSettingsEnableWatcher": "Bibliotheken automatisch scannen op wijzigingen",
"LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch scannen op wijzigingen",
"LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server",
@ -567,8 +568,8 @@
"LabelSettingsEpubsAllowScriptedContentHelp": "Sta toe dat epub-bestanden scripts uitvoeren. Het wordt aanbevolen om deze instelling uitgeschakeld te houden, tenzij u de bron van de epub-bestanden vertrouwt.",
"LabelSettingsExperimentalFeatures": "Experimentele functies",
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
"LabelSettingsFindCovers": "Zoek covers",
"LabelSettingsFindCoversHelp": "Als je audioboek geen ingesloten cover of cover in de map heeft, zal de scanner proberen een cover te vinden.<br>Opmerking: Dit zal de scan-duur verlengen",
"LabelSettingsFindCovers": "Omslagen zoeken",
"LabelSettingsFindCoversHelp": "Als je audioboek geen omslag in het bestand of in de map heeft, zal de scanner automatisch proberen een omslag te vinden.<br>Opmerking: Dit kan de scantijd verlengen",
"LabelSettingsHideSingleBookSeries": "Verberg series met een enkel boek",
"LabelSettingsHideSingleBookSeriesHelp": "Series die slechts een enkel boek bevatten worden verborgen op de seriespagina en de homepagina-planken.",
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
@ -578,18 +579,18 @@
"LabelSettingsLibraryMarkAsFinishedWhen": "Markeer media item wanneer voltooid",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Sla eedere boeken in Serie Verderzetten over",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "De Continue Series home page shelf toont het eerste boek dat nog niet is begonnen in series waarvan er minstens één is voltooid en er geen boeken in uitvoering zijn. Als u deze instelling inschakelt, wordt de serie voortgezet vanaf het boek dat het verst is voltooid in plaats van het eerste boek dat nog niet is begonnen.",
"LabelSettingsParseSubtitles": "Parseer subtitel",
"LabelSettingsParseSubtitles": "Subtitel afleiden uit foldernaam",
"LabelSettingsParseSubtitlesHelp": "Haal subtitels uit mapnaam van audioboek.<br>Subtitel moet gescheiden zijn met \" - \"<br>b.v. \"Boektitel - Een Subtitel Hier\" heeft als subtitel \"Een Subtitel Hier\"",
"LabelSettingsPreferMatchedMetadata": "Prefereer gematchte metadata",
"LabelSettingsPreferMatchedMetadata": "Geef voorkeur aan gematchte metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Gematchte data zal onderdeeldetails overschrijven bij gebruik van Quick Match. Standaard vult Quick Match uitsluitend ontbrekende details aan.",
"LabelSettingsSkipMatchingBooksWithASIN": "Sla matchen van boeken over die al over een ASIN beschikken",
"LabelSettingsSkipMatchingBooksWithISBN": "Sla matchen van boeken over die al over een ISBN beschikken",
"LabelSettingsSortingIgnorePrefixes": "Negeer voorvoegsels bij sorteren",
"LabelSettingsSortingIgnorePrefixesHelp": "b.v. voor voorvoegsel \"The\" wordt titel \"The Title\" dan gesorteerd als \"Title, The\"",
"LabelSettingsSquareBookCovers": "Gebruik vierkante boekcovers",
"LabelSettingsSquareBookCoversHelp": "Prefereer gebruik van vierkante covers boven standaard 1.6:1 boekcovers",
"LabelSettingsStoreCoversWithItem": "Bewaar covers bij onderdeel",
"LabelSettingsStoreCoversWithItemHelp": "Standaard worden covers bewaard in /metadata/items, door deze instelling in te schakelen zullen covers in de map van je bibliotheekonderdeel bewaard worden. Slechts een bestand genaamd \"cover\" zal worden bewaard",
"LabelSettingsSquareBookCovers": "Gebruik vierkante boekomslagen",
"LabelSettingsSquareBookCoversHelp": "Gebruik vierkante boekomslagen in plaats van standaard 1,6:1",
"LabelSettingsStoreCoversWithItem": "Bewaar omslagen bij onderdeel",
"LabelSettingsStoreCoversWithItemHelp": "Omslagen worden standaard in /metadata/items opgeslagen. Bij inschakelen worden ze in de map van het bibliotheekitem zelf opgeslagen. Slechts een bestand genaamd \"cover\" zal worden bewaard",
"LabelSettingsStoreMetadataWithItem": "Bewaar metadata bij onderdeel",
"LabelSettingsStoreMetadataWithItemHelp": "Standaard worden metadata-bestanden bewaard in /metadata/items, door deze instelling in te schakelen zullen metadata bestanden in de map van je bibliotheekonderdeel bewaard worden",
"LabelSettingsTimeFormat": "Tijdformat",
@ -605,6 +606,7 @@
"LabelSlug": "Slak",
"LabelSortAscending": "Oplopend",
"LabelSortDescending": "Aflopend",
"LabelSortPubDate": "Sorteer Pub Datum",
"LabelStart": "Start",
"LabelStartTime": "Starttijd",
"LabelStarted": "Gestart",
@ -650,12 +652,12 @@
"LabelTimeToShift": "Tijd op te schuiven in seconden",
"LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Metadata insluiten",
"LabelToolsEmbedMetadataDescription": "Metadata insluiten in audiobestanden, inclusief coverafbeelding en hoofdstukken.",
"LabelToolsEmbedMetadataDescription": "Metadata insluiten in audiobestanden, inclusief omslagafbeelding en hoofdstukken.",
"LabelToolsM4bEncoder": "M4B Encoder",
"LabelToolsMakeM4b": "Maak M4B-audioboekbestand",
"LabelToolsMakeM4bDescription": "Genereer een .M4B-audioboekbestand met ingesloten metadata, coverafbeelding en hoofdstukken.",
"LabelToolsMakeM4bDescription": "Genereer een .M4B-audioboekbestand met ingesloten metadata, omslagafbeelding en hoofdstukken.",
"LabelToolsSplitM4b": "Splitst M4B in MP3's",
"LabelToolsSplitM4bDescription": "Maak MP3's van een M4B, gesplitst per hoofdstuk met ingesloten metadata, coverafbeelding en hoofdstukken.",
"LabelToolsSplitM4bDescription": "Maak MP3's van een M4B, gesplitst per hoofdstuk met ingesloten metadata, omslagafbeelding en hoofdstukken.",
"LabelTotalDuration": "Totale duur",
"LabelTotalTimeListened": "Totale tijd geluisterd",
"LabelTrackFromFilename": "Track vanuit bestandsnaam",
@ -670,8 +672,8 @@
"LabelUndo": "Ongedaan maken",
"LabelUnknown": "Onbekend",
"LabelUnknownPublishDate": "Onbekende uitgeefdatum",
"LabelUpdateCover": "Cover bijwerken",
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
"LabelUpdateCover": "Omslag bijwerken",
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande omslagen toe voor de geselecteerde boeken wanneer een match is gevonden",
"LabelUpdateDetails": "Details bijwerken",
"LabelUpdateDetailsHelp": "Sta overschrijven van bestaande details toe voor de geselecteerde boeken wanneer een match is gevonden",
"LabelUpdatedAt": "Bijgewerkt op",
@ -705,13 +707,15 @@
"LabelYourProgress": "Je voortgang",
"MessageAddToPlayerQueue": "Toevoegen aan wachtrij",
"MessageAppriseDescription": "Om deze functie te gebruiken heb je een draaiende instantie van <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nodig of een api die dezelfde requests afhandelt. <br />De Apprise API Url moet het volledige URL-pad zijn om de notificatie te verzenden, b.v., als je API-instantie draait op <code>http://192.168.1.1:8337</code> dan zou je <code>http://192.168.1.1:8337/notify</code> gebruiken.",
"MessageAsinCheck": "Zorg ervoor dat u de ASIN van de juiste Audible-regio gebruikt, niet die van Amazon.",
"MessageAuthenticationOIDCChangesRestart": "Start uw server opnieuw op nadat u het opslaan hebt uitgevoerd, om de OIDC-wijzigingen toe te passen.",
"MessageBackupsDescription": "Back-ups omvatten gebruikers, gebruikers' voortgang, bibliotheekonderdeeldetails, serverinstellingen en afbeeldingen bewaard in <code>/metadata/items</code> & <code>/metadata/authors</code>. Back-ups <strong>bevatten niet</strong> de bestanden bewaard in je bibliotheekmappen.",
"MessageBackupsLocationEditNote": "Let op: het bijwerken van de back-uplocatie zal bestaande back-ups niet verplaatsen of wijzigen",
"MessageBackupsLocationNoEditNote": "Let op: De back-uplocatie wordt ingesteld via een omgevingsvariabele en kan hier niet worden gewijzigd.",
"MessageBackupsLocationPathEmpty": "Backup locatie pad kan niet leeg zijn",
"MessageBatchEditPopulateMapDetailsAllHelp": "Vul actieve velden in met data van alle items. Velden met meerdere waarden zullen worden samengevoegd",
"MessageBatchEditPopulateMapDetailsItemHelp": "Vul actieve folder detail velden met de data van dit item",
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
"MessageBatchQuickMatchDescription": "Quick Match probeert ontbrekende omslagen en metadata toe te voegen aan de geselecteerde items. Schakel de opties hieronder in om Quick Match bestaande omslagen en/of metadata te laten overschrijven.",
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
"MessageBookshelfNoCollectionsHelp": "Collecties zijn publiekelijk. Alle gebruikers met toegang tot de bibliotheek kunnen ze zien.",
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
@ -753,6 +757,7 @@
"MessageConfirmRemoveAuthor": "Weet je zeker dat je auteur \"{0}\" wil verwijderen?",
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisodeNote": "Let op: Het audiobestand wordt niet verwijderd, tenzij je Bestand permanent verwijderen inschakelt",
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
"MessageConfirmRemoveListeningSessions": "Weet je zeker dat je {0} luistersessies wilt verwijderen?",
"MessageConfirmRemoveMetadataFiles": "Bent u zeker dat u alle metadata wil verwijderen. {0} bestanden in uw bibliotheel item folders?",
@ -781,8 +786,8 @@
"MessageImportantNotice": "Belangrijke opmerking!",
"MessageInsertChapterBelow": "Hoofdstuk hieronder invoegen",
"MessageInvalidAsin": "Ongeldige ASIN",
"MessageItemsSelected": "{0} onderdelen geselecteerd",
"MessageItemsUpdated": "{0} onderdelen bijgewerkt",
"MessageItemsSelected": "{0} items geselecteerd",
"MessageItemsUpdated": "{0} items bijgewerkt",
"MessageJoinUsOn": "Doe mee op",
"MessageLoading": "Aan het laden...",
"MessageLoadingFolders": "Mappen aan het laden...",
@ -794,14 +799,14 @@
"MessageMarkAllEpisodesNotFinished": "Markeer alle afleveringen als niet voltooid",
"MessageMarkAsFinished": "Markeer als Voltooid",
"MessageMarkAsNotFinished": "Markeer als Niet Voltooid",
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.",
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te koppelen aan een boek uit de geselecteerde bron en ontbrekende gegevens en een omslag toe te voegen. Overschrijft geen bestaande gegevens.",
"MessageNoAudioTracks": "Geen audiotracks",
"MessageNoAuthors": "Geen auteurs",
"MessageNoBackups": "Geen back-ups",
"MessageNoBookmarks": "Geen boekwijzers",
"MessageNoChapters": "Geen hoofdstukken",
"MessageNoCollections": "Geen collecties",
"MessageNoCoversFound": "Geen covers gevonden",
"MessageNoCoversFound": "Geen omslagen gevonden",
"MessageNoDescription": "Geen beschrijving",
"MessageNoDevices": "Geen Apparaten",
"MessageNoDownloadsInProgress": "Geen downloads bezig op dit moment",
@ -839,7 +844,7 @@
"MessageQuickEmbedInProgress": "Snelle inbedding in uitvoering",
"MessageQuickEmbedQueue": "In de wachtrij voor snelle insluiting ({0} in wachtrij)",
"MessageQuickMatchAllEpisodes": "Alle Afleveringen Snel Matchen",
"MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.",
"MessageQuickMatchDescription": "Vult ontbrekende gegevens & omslag met eerste matchresultaat van '{0}'. Overschrijft gegevens alleen als de serverinstelling Geef voorkeur aan gematchte metadata is ingeschakeld.",
"MessageRemoveChapter": "Verwijder hoofdstuk",
"MessageRemoveEpisodes": "Verwijder {0} aflevering(en)",
"MessageRemoveFromPlayerQueue": "Verwijder uit afspeelwachtrij",
@ -847,10 +852,12 @@
"MessageReportBugsAndContribute": "Rapporteer bugs, vraag functionaliteiten aan en draag bij op",
"MessageResetChaptersConfirm": "Weet je zeker dat je de hoofdstukken wil resetten en de wijzigingen die je gemaakt hebt ongedaan wil maken?",
"MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op",
"MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.<br /><br />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.<br /><br />Alle clients die van je server gebruik maken zullen automatisch worden ververst.",
"MessageRestoreBackupWarning": "Een back-up herstellen zal de volledige database in /config en de omslagen in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om omslagen en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle apparaten die je server gebruiken, worden automatisch ververst.",
"MessageScheduleLibraryScanNote": "Voor de meeste gebruikers is het raadzaam om deze functie uitgeschakeld te laten en de folder watcher-instelling ingeschakeld te houden. De folder watcher detecteert automatisch wijzigingen in uw bibliotheekmappen. De folder watcher werkt niet voor elk bestandssysteem (zoals NFS), dus geplande bibliotheekscans kunnen in plaats daarvan worden gebruikt.",
"MessageScheduleRunEveryWeekdayAtTime": "Elke {0} uitvoeren op {1}",
"MessageSearchResultsFor": "Zoekresultaten voor",
"MessageSelected": "{0} geselecteerd",
"MessageSeriesSequenceCannotContainSpaces": "Serievolgorde mag geen spaties bevatten",
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
"MessageShareExpirationWillBe": "Vervaldatum is <strong>{0}</strong>",
@ -912,6 +919,8 @@
"NotificationOnBackupCompletedDescription": "Wordt geactiveerd wanneer een back-up is voltooid",
"NotificationOnBackupFailedDescription": "Wordt geactiveerd wanneer een back-up mislukt",
"NotificationOnEpisodeDownloadedDescription": "Wordt geactiveerd wanneer een podcastaflevering automatisch wordt gedownload",
"NotificationOnRSSFeedDisabledDescription": "Wordt geactiveerd wanneer automatische afleveringsdownloads zijn uitgeschakeld vanwege te veel mislukte pogingen",
"NotificationOnRSSFeedFailedDescription": "Getriggerd wanneer de RSS feed aanvraag faalt voor een automatische aflevering download",
"NotificationOnTestDescription": "Event voor het testen van het notificatiesysteem",
"PlaceholderNewCollection": "Nieuwe naam collectie",
"PlaceholderNewFolderPath": "Nieuwe locatie map",
@ -956,6 +965,7 @@
"ToastBackupRestoreFailed": "Herstellen back-up mislukt",
"ToastBackupUploadFailed": "Uploaden back-up mislukt",
"ToastBackupUploadSuccess": "Back-up geüpload",
"ToastBatchApplyDetailsToItemsSuccess": "Details toegepast op items",
"ToastBatchDeleteFailed": "Batch verwijderen mislukt",
"ToastBatchDeleteSuccess": "Batch verwijderen gelukt",
"ToastBatchQuickMatchFailed": "Batch Snel Vergelijken mislukt!",
@ -968,13 +978,15 @@
"ToastCachePurgeFailed": "Cache wissen is mislukt",
"ToastCachePurgeSuccess": "Cache succesvol verwijderd",
"ToastChaptersHaveErrors": "Hoofdstukken bevatten fouten",
"ToastChaptersInvalidShiftAmountLast": "Ongeldige shift-tijd. De starttijd van het laatste hoofdstuk zou langer zijn dan de duur van dit audioboek.",
"ToastChaptersInvalidShiftAmountStart": "Ongeldige shift-lengte. Het eerste hoofdstuk zou nul of een negatieve lengte hebben en zou worden overschreven door het tweede hoofdstuk. Verleng de startduur van het tweede hoofdstuk.",
"ToastChaptersMustHaveTitles": "Hoofdstukken moeten titels hebben",
"ToastChaptersRemoved": "Hoofdstukken verwijderd",
"ToastChaptersUpdated": "Hoofdstukken bijgewerkt",
"ToastCollectionItemsAddFailed": "Item(s) toegevoegd aan collectie mislukt",
"ToastCollectionRemoveSuccess": "Collectie verwijderd",
"ToastCollectionUpdateSuccess": "Collectie bijgewerkt",
"ToastCoverUpdateFailed": "Cover update mislukt",
"ToastCoverUpdateFailed": "Omslag bijwerken mislukt",
"ToastDateTimeInvalidOrIncomplete": "Datum en tijd ongeldig of onvolledig",
"ToastDeleteFileFailed": "Bestand verwijderen mislukt",
"ToastDeleteFileSuccess": "Bestand verwijderd",
@ -996,7 +1008,7 @@
"ToastInvalidImageUrl": "Ongeldige afbeeldings-URL",
"ToastInvalidMaxEpisodesToDownload": "Ongeldig maximum aantal afleveringen om te downloaden",
"ToastInvalidUrl": "Ongeldige URL",
"ToastItemCoverUpdateSuccess": "Cover onderdeel bijgewerkt",
"ToastItemCoverUpdateSuccess": "Omslag bijgewerkt",
"ToastItemDeletedFailed": "Item verwijderen mislukt",
"ToastItemDeletedSuccess": "Verwijderd item",
"ToastItemDetailsUpdateSuccess": "Details onderdeel bijgewerkt",
@ -1068,6 +1080,7 @@
"ToastSelectAtLeastOneUser": "Selecteer ten minste een gebruiker",
"ToastSendEbookToDeviceFailed": "Ebook naar apparaat sturen mislukt",
"ToastSendEbookToDeviceSuccess": "Ebook verstuurd naar apparaat \"{0}\"",
"ToastSeriesSubmitFailedSameName": "Kan niet twee series met dezelfde naam toevoegen",
"ToastSeriesUpdateFailed": "Bijwerken serie mislukt",
"ToastSeriesUpdateSuccess": "Bijwerken serie gelukt",
"ToastServerSettingsUpdateSuccess": "Server instellingen bijgewerkt",
@ -1086,6 +1099,8 @@
"ToastUnknownError": "Onbekende fout",
"ToastUnlinkOpenIdFailed": "Gebruiker ontkoppelen van OpenID mislukt",
"ToastUnlinkOpenIdSuccess": "Gebruiker ontkoppeld van OpenID",
"ToastUploaderFilepathExistsError": "Bestandspad \"{0}\" bestaat al op de server",
"ToastUploaderItemExistsInSubdirectoryError": "Item \"{0}\" gebruikt een submap van het uploadpad.",
"ToastUserDeleteFailed": "Verwijderen gebruiker mislukt",
"ToastUserDeleteSuccess": "Gebruiker verwijderd",
"ToastUserPasswordChangeSuccess": "Wachtwoord succesvol gewijzigd",

View file

@ -346,7 +346,7 @@
"LabelExample": "Príklad",
"LabelExpandSeries": "Rozbaliť série",
"LabelExpandSubSeries": "Rozbaliť podsérie",
"LabelExplicit": "Explicitné",
"LabelExplicit": "Explicitný obsah",
"LabelExplicitChecked": "Explicitné (zaškrtnuté)",
"LabelExplicitUnchecked": "Ne-explicitné (nezaškrtnuté)",
"LabelExportOPML": "Exportovať OPML",
@ -757,6 +757,7 @@
"MessageConfirmRemoveAuthor": "Ste si istý, že chcete odstrániť autora \"{0}\"?",
"MessageConfirmRemoveCollection": "Ste si istý, že chcete odstrániť zbierku \"{0}\"?",
"MessageConfirmRemoveEpisode": "Ste si istý, že chcete odstrániť epizódu \"{0}\"?",
"MessageConfirmRemoveEpisodeNote": "Poznámka: Tento krok neodstráni zvukový súbor, pokiaľ nezaškrtnete voľbu \"Nezvratné zmazanie súborov\"",
"MessageConfirmRemoveEpisodes": "Ste si istý, že chcete odstrániť {0} epizód?",
"MessageConfirmRemoveListeningSessions": "Ste si istý, že chcete odstrániť týchto {0} relácií?",
"MessageConfirmRemoveMetadataFiles": "Ste si istý, že chcete odstrániť všetky súbory metadata.{0} z priečinkov položiek vašej knižnice?",
@ -918,6 +919,8 @@
"NotificationOnBackupCompletedDescription": "Spustené po dokončení zálohovania",
"NotificationOnBackupFailedDescription": "Spustené pri zlyhaní zálohovania",
"NotificationOnEpisodeDownloadedDescription": "Spustené po automatickom stiahnutí epizódy podcastu",
"NotificationOnRSSFeedDisabledDescription": "Spustí sa, keď je automatické sťahovanie epizód pozastavené z dôvodu veľkého počtu zlyhaní",
"NotificationOnRSSFeedFailedDescription": "Spustí sa v prípade, keď zlyhá požiadavka RSS zdroja na automatické stiahnutie epizódy",
"NotificationOnTestDescription": "Udalosť určená na testovanie systému notifikácií",
"PlaceholderNewCollection": "Názov novej zbierky",
"PlaceholderNewFolderPath": "Umiestnenie nového priečinka",

View file

@ -757,6 +757,7 @@
"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}\"?",
"MessageConfirmRemoveEpisodeNote": "Opomba: S tem se zvočna datoteka ne izbriše, razen če vklopite možnost \"Trdo brisanje datoteke\"",
"MessageConfirmRemoveEpisodes": "Ali ste prepričani, da želite odstraniti {0} epizod?",
"MessageConfirmRemoveListeningSessions": "Ali ste prepričani, da želite odstraniti {0} sej poslušanja?",
"MessageConfirmRemoveMetadataFiles": "Ali ste prepričani, da želite odstraniti vse metapodatke.{0} v mapah elementov knjižnice?",

View file

@ -3,7 +3,7 @@
"ButtonAddChapters": "Додати глави",
"ButtonAddDevice": "Додати пристрій",
"ButtonAddLibrary": "Додати бібліотеку",
"ButtonAddPodcasts": "Додати подкаст",
"ButtonAddPodcasts": "Додати подкасти",
"ButtonAddUser": "Додати користувача",
"ButtonAddYourFirstLibrary": "Додайте вашу першу бібліотеку",
"ButtonApply": "Застосувати",
@ -16,7 +16,7 @@
"ButtonCancel": "Скасувати",
"ButtonCancelEncode": "Скасувати кодування",
"ButtonChangeRootPassword": "Змінити кореневий пароль",
"ButtonCheckAndDownloadNewEpisodes": "Перевірити та завантажити нові епізоди",
"ButtonCheckAndDownloadNewEpisodes": "Перевірити та скачати нові епізоди",
"ButtonChooseAFolder": "Обрати теку",
"ButtonChooseFiles": "Обрати файли",
"ButtonClearFilter": "Очистити фільтр",
@ -32,8 +32,8 @@
"ButtonEditChapters": "Редагувати глави",
"ButtonEditPodcast": "Редагувати подкаст",
"ButtonEnable": "Увімкнути",
"ButtonFireAndFail": "Вогонь і невдача",
"ButtonFireOnTest": "Випробування на вогнестійкість",
"ButtonFireAndFail": "Виконати і завершити з помилкою",
"ButtonFireOnTest": "Виконати подію onTest",
"ButtonForceReScan": "Примусово сканувати",
"ButtonFullPath": "Повний шлях",
"ButtonHide": "Приховати",
@ -44,7 +44,7 @@
"ButtonLatest": "Останні",
"ButtonLibrary": "Бібліотека",
"ButtonLogout": "Вийти",
"ButtonLookup": "Пошук",
"ButtonLookup": "Пошуки",
"ButtonManageTracks": "Керувати доріжками",
"ButtonMapChapterTitles": "Призначити назви глав",
"ButtonMatchAllAuthors": "Віднайти усіх авторів",
@ -57,7 +57,7 @@
"ButtonOpenFeed": "Відкрити стрічку",
"ButtonOpenManager": "Відкрити менеджер",
"ButtonPause": "Пауза",
"ButtonPlay": "Слухати",
"ButtonPlay": "Відтворити",
"ButtonPlayAll": "Відтворити все",
"ButtonPlaying": "Відтворюється",
"ButtonPlaylists": "Списки відтворення",
@ -86,7 +86,7 @@
"ButtonResetToDefault": "Скинути до стандартних",
"ButtonRestore": "Відновити",
"ButtonSave": "Зберегти",
"ButtonSaveAndClose": "Зберегти та закрити",
"ButtonSaveAndClose": "Зберегти і закрити",
"ButtonSaveTracklist": "Зберегти порядок",
"ButtonScan": "Сканувати",
"ButtonScanLibrary": "Сканувати бібліотеку",
@ -103,7 +103,7 @@
"ButtonStartMetadataEmbed": "Почати вбудування метаданих",
"ButtonStats": "Статистика",
"ButtonSubmit": "Надіслати",
"ButtonTest": "Перевірити",
"ButtonTest": "Тест",
"ButtonUnlinkOpenId": "Вимкнути OpenID",
"ButtonUpload": "Завантажити",
"ButtonUploadBackup": "Завантажити резервну копію",
@ -115,7 +115,7 @@
"ButtonYes": "Так",
"ErrorUploadFetchMetadataAPI": "Помилка при отриманні метаданих",
"ErrorUploadFetchMetadataNoResults": "Не вдалося отримати метадані — спробуйте оновити заголовок та/або автора",
"ErrorUploadLacksTitle": "Назва обов'язкова",
"ErrorUploadLacksTitle": "Потрібна назва",
"HeaderAccount": "Профіль",
"HeaderAddCustomMetadataProvider": "Додати користувацький постачальник метаданих",
"HeaderAdvanced": "Розширені",
@ -130,11 +130,11 @@
"HeaderCollection": "Добірка",
"HeaderCollectionItems": "Елементи добірки",
"HeaderCover": "Обкладинка",
"HeaderCurrentDownloads": "Поточні завантаження",
"HeaderCurrentDownloads": "Поточні скачування",
"HeaderCustomMessageOnLogin": "Повідомлення при вході",
"HeaderCustomMetadataProviders": "Постачальники метаданих",
"HeaderDetails": "Подробиці",
"HeaderDownloadQueue": "Черга завантажень",
"HeaderDownloadQueue": "Черга скачувань",
"HeaderEbookFiles": "Файли електронних книг",
"HeaderEmail": "Електронна пошта",
"HeaderEmailSettings": "Налаштування електронної пошти",
@ -152,13 +152,13 @@
"HeaderLibraryFiles": "Файли бібліотеки",
"HeaderLibraryStats": "Статистика бібліотеки",
"HeaderListeningSessions": "Сеанси прослуховування",
"HeaderListeningStats": "Статистика відтворення",
"HeaderListeningStats": "Статистика прослуховування",
"HeaderLogin": "Вхід",
"HeaderLogs": "Журнал",
"HeaderManageGenres": "Керувати жанрами",
"HeaderManageTags": "Керувати мітками",
"HeaderMapDetails": "Призначити подробиці",
"HeaderMatch": "Пошук",
"HeaderMatch": "Допасуй",
"HeaderMetadataOrderOfPrecedence": "Порядок метаданих",
"HeaderMetadataToEmbed": "Вбудувати метадані",
"HeaderNewAccount": "Новий профіль",
@ -176,7 +176,7 @@
"HeaderPlayerSettings": "Налаштування програвача",
"HeaderPlaylist": "Список відтворення",
"HeaderPlaylistItems": "Елементи списку відтворення",
"HeaderPodcastsToAdd": "Додати подкасти",
"HeaderPodcastsToAdd": "Подкасти для додання",
"HeaderPresets": "Пресети",
"HeaderPreviewCover": "Попередній перегляд",
"HeaderRSSFeedGeneral": "Подробиці RSS",
@ -186,7 +186,7 @@
"HeaderRemoveEpisodes": "Видалити епізодів: {0}",
"HeaderSavedMediaProgress": "Збережений прогрес медіа",
"HeaderSchedule": "Розклад",
"HeaderScheduleEpisodeDownloads": "Запланувати автоматичне завантаження епізодів",
"HeaderScheduleEpisodeDownloads": "Запланувати автоматичне скачування епізодів",
"HeaderScheduleLibraryScans": "Розклад автосканування бібліотеки",
"HeaderSession": "Сеанс",
"HeaderSetBackupSchedule": "Встановити розклад резервного копіювання",
@ -223,21 +223,21 @@
"LabelActivities": "Діяльність",
"LabelActivity": "Активність",
"LabelAddToCollection": "Додати у добірку",
"LabelAddToCollectionBatch": "Додати книги до добірки: {0}",
"LabelAddToCollectionBatch": "Додати {0} книг до добірки",
"LabelAddToPlaylist": "Додати до списку відтворення",
"LabelAddToPlaylistBatch": "Додано елементів у список відтворення: {0}",
"LabelAddToPlaylistBatch": "Додати {0} елементів до списку відтворення",
"LabelAddedAt": "Дата додавання",
"LabelAddedDate": "Додано {0}",
"LabelAdminUsersOnly": "Тільки для адміністраторів",
"LabelAll": "Усе",
"LabelAllEpisodesDownloaded": "Усі серії завантажено",
"LabelAllEpisodesDownloaded": "Усі епізоди скачано",
"LabelAllUsers": "Усі користувачі",
"LabelAllUsersExcludingGuests": "Усі, крім гостей",
"LabelAllUsersIncludingGuests": "Усі, включно з гостями",
"LabelAlreadyInYourLibrary": "Вже у вашій бібліотеці",
"LabelApiToken": "Токен API",
"LabelAppend": "Додати",
"LabelAudioBitrate": "Бітрейт аудіо (напр. 128k)",
"LabelAudioBitrate": "Бітрейт аудіо (наприклад, 128k)",
"LabelAudioChannels": "Канали аудіо (1 або 2)",
"LabelAudioCodec": "Аудіокодек",
"LabelAuthor": "Автор",
@ -256,18 +256,18 @@
"LabelBackupLocation": "Розташування резервних копій",
"LabelBackupsEnableAutomaticBackups": "Автоматичне резервне копіювання",
"LabelBackupsEnableAutomaticBackupsHelp": "Резервні копії збережено у /metadata/backups",
"LabelBackupsMaxBackupSize": "Максимальний розмір резервної копії (у ГБ) (0 — необмежене)",
"LabelBackupsMaxBackupSize": "Максимальний розмір резервної копії (у ГБ) (0 — без обмежень)",
"LabelBackupsMaxBackupSizeHelp": "У якості захисту від неправильного налаштування, резервну копію не буде збережено, якщо її розмір перевищуватиме вказаний.",
"LabelBackupsNumberToKeep": "Кількість резервних копій",
"LabelBackupsNumberToKeepHelp": "Лиш 1 резервну копію буде видалено за раз, тож якщо їх багато, то вам варто видалити їх вручну.",
"LabelBackupsNumberToKeepHelp": "Видаляється лише 1 резервна копія за раз, тому якщо у вас більше копій, видаліть їх вручну.",
"LabelBitrate": "Бітрейт",
"LabelBonus": "Бонус",
"LabelBooks": "Книги",
"LabelBooks": "Книг",
"LabelButtonText": "Текст кнопки",
"LabelByAuthor": "від {0}",
"LabelChangePassword": "Змінити пароль",
"LabelChannels": "Канали",
"LabelChapterCount": "{0} Глав",
"LabelChapterCount": "{0} глав",
"LabelChapterTitle": "Назва глави",
"LabelChapters": "Глави",
"LabelChaptersFound": "глав знайдено",
@ -304,9 +304,9 @@
"LabelDiscFromFilename": "Диск за назвою файлу",
"LabelDiscFromMetadata": "Диск за метаданими",
"LabelDiscover": "Огляд",
"LabelDownload": "Завантажити",
"LabelDownloadNEpisodes": "Завантажити епізодів: {0}",
"LabelDownloadable": "Можна завантажити",
"LabelDownload": "Скачати",
"LabelDownloadNEpisodes": "Скачати {0} епізодів",
"LabelDownloadable": "Можна скачати",
"LabelDuration": "Тривалість",
"LabelDurationComparisonExactMatch": "(повний збіг)",
"LabelDurationComparisonLonger": "(на {0} довше)",
@ -346,16 +346,16 @@
"LabelExample": "Приклад",
"LabelExpandSeries": "Розгорнути серії",
"LabelExpandSubSeries": "Розгорнути підсерії",
"LabelExplicit": "Відверта",
"LabelExplicit": "Відвертий",
"LabelExplicitChecked": "Відверта (з прапорцем)",
"LabelExplicitUnchecked": "Не відверта (без прапорця)",
"LabelExportOPML": "Експорт OPML",
"LabelFeedURL": "Адреса стрічки",
"LabelFetchingMetadata": "Отримання метаданих",
"LabelFile": "Файл",
"LabelFileBirthtime": "Дата створення",
"LabelFileBirthtime": "Дата створення файлу",
"LabelFileBornDate": "Народився {0}",
"LabelFileModified": "Дата змінення",
"LabelFileModified": "Дата зміни файлу",
"LabelFileModifiedDate": "Змінено {0}",
"LabelFilename": "Ім'я файлу",
"LabelFilterByUser": "Фільтрувати за користувачем",
@ -395,7 +395,7 @@
"LabelIntervalEvery6Hours": "Кожні 6 годин",
"LabelIntervalEveryDay": "Щодня",
"LabelIntervalEveryHour": "Щогодини",
"LabelIntervalEveryMinute": "Кожну хвилину",
"LabelIntervalEveryMinute": "Щохвилини",
"LabelInvert": "Інвертувати",
"LabelItem": "Елемент",
"LabelJumpBackwardAmount": "Час переходу назад",
@ -427,10 +427,10 @@
"LabelLowestPriority": "Найнижчий пріоритет",
"LabelMatchExistingUsersBy": "Шукати наявних користувачів за",
"LabelMatchExistingUsersByDescription": "Використовується для підключення наявних користувачів. Після підключення користувач отримає унікальний id від вашого сервісу SSO",
"LabelMaxEpisodesToDownload": "Максимальна кількість епізодів для завантаження. Використовуйте 0 для необмеженої кількості.",
"LabelMaxEpisodesToDownloadPerCheck": "Максимальна кількість нових епізодів для завантаження за перевірку",
"LabelMaxEpisodesToDownload": "Максимальна кількість епізодів для скачування. Використовуйте 0 для необмеженої кількості.",
"LabelMaxEpisodesToDownloadPerCheck": "Максимальна кількість нових епізодів для скачування за перевірку",
"LabelMaxEpisodesToKeep": "Максимальна кількість епізодів для зберігання",
"LabelMaxEpisodesToKeepHelp": "Значення 0 не встановлює обмеження. Після автоматичного завантаження нового епізоду, буде видалено найстаріший епізод, якщо у вас більше ніж X епізодів. Видаляється лише 1 епізод за одне нове завантаження.",
"LabelMaxEpisodesToKeepHelp": "Значення 0 — без обмежень. Після автоматичного завантаження нового епізоду буде видалено найстаріший, якщо їх більше X. Видаляється лише 1 епізод за одне нове завантаження.",
"LabelMediaPlayer": "Програвач медіа",
"LabelMediaType": "Тип медіа",
"LabelMetaTag": "Метатег",
@ -485,7 +485,7 @@
"LabelPermissionsAccessExplicitContent": "Доступ до відвертого вмісту",
"LabelPermissionsCreateEreader": "Можна створити читалку",
"LabelPermissionsDelete": "Може видаляти",
"LabelPermissionsDownload": "Може завантажувати",
"LabelPermissionsDownload": "Може скачувати",
"LabelPermissionsUpdate": "Може оновлювати",
"LabelPermissionsUpload": "Може завантажувати",
"LabelPersonalYearReview": "Ваші підсумки року ({0})",
@ -542,8 +542,8 @@
"LabelSeason": "Сезон",
"LabelSeasonNumber": "Сезон #{0}",
"LabelSelectAll": "Вибрати все",
"LabelSelectAllEpisodes": "Вибрати всі серії",
"LabelSelectEpisodesShowing": "Обрати показані епізоди: {0}",
"LabelSelectAllEpisodes": "Вибрати всі епізоди",
"LabelSelectEpisodesShowing": "Вибрати {0} показаних епізодів",
"LabelSelectUsers": "Вибрати користувачів",
"LabelSendEbookToDevice": "Надіслати електронну книгу на...",
"LabelSequence": "Послідовність",
@ -595,7 +595,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "За замовчуванням файли метаданих зберігаються у /metadata/items. Цей параметр увімкне збереження метаданих у теці елемента бібліотеки",
"LabelSettingsTimeFormat": "Формат часу",
"LabelShare": "Поділитися",
"LabelShareDownloadableHelp": "Дозволяє користувачам із посиланням для спільного доступу завантажувати zip-файл елемента бібліотеки.",
"LabelShareDownloadableHelp": "Дозволяє користувачам із посиланням для спільного доступу скачування zip-файлу елемента бібліотеки.",
"LabelShareOpen": "Поділитися відкрито",
"LabelShareURL": "Поділитися URL",
"LabelShowAll": "Показати все",
@ -714,19 +714,19 @@
"MessageBackupsLocationNoEditNote": "Примітка: розташування резервної копії встановлюється за допомогою змінної середовища та не може бути змінене тут.",
"MessageBackupsLocationPathEmpty": "Шлях розташування резервної копії не може бути порожнім",
"MessageBatchEditPopulateMapDetailsAllHelp": "Заповнити увімкнені поля даними з усіх елементів. Поля з кількома значеннями буде об’єднано",
"MessageBatchEditPopulateMapDetailsItemHelp": "Заповніть увімкнені поля деталей карти даними з цього елемента",
"MessageBatchEditPopulateMapDetailsItemHelp": "Заповнити увімкнені поля деталізації даними з цього елемента",
"MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.",
"MessageBookshelfNoCollections": "Ви не створили жодної добірки",
"MessageBookshelfNoCollections": "Ви ще не створили жодної добірки",
"MessageBookshelfNoCollectionsHelp": "Колекції публічні. Їх можуть бачити всі користувачі, які мають доступ до бібліотеки.",
"MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів",
"MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Немає результатів за запитом",
"MessageBookshelfNoSeries": "Серії відсутні",
"MessageChapterEndIsAfter": "Кінець глави знаходиться після закінчення книги",
"MessageChapterErrorFirstNotZero": "Перша глава мусить починатися з 0",
"MessageChapterErrorStartGteDuration": "Час початку мусить бути меншим за тривалість аудіокниги",
"MessageChapterErrorStartLtPrev": "Неприпустимий час початку, має бути більшим за час початку попередньої глави",
"MessageChapterStartIsAfter": "Початок глави знаходиться після закінчення книги",
"MessageBookshelfNoSeries": "У вас немає серій",
"MessageChapterEndIsAfter": "Кінець глави після завершення аудіокниги",
"MessageChapterErrorFirstNotZero": "Перша глава повинна починатися з 0",
"MessageChapterErrorStartGteDuration": "Час початку має бути меншим за тривалість аудіокниги",
"MessageChapterErrorStartLtPrev": "Час початку має бути більшим або рівним часу початку попередньої глави",
"MessageChapterStartIsAfter": "Початок глави після завершення аудіокниги",
"MessageChaptersNotFound": "Розділи не знайдені",
"MessageCheckingCron": "Перевірка планувальника...",
"MessageConfirmCloseFeed": "Ви дійсно бажаєте закрити цей канал?",
@ -734,71 +734,72 @@
"MessageConfirmDeleteDevice": "Ви впевнені, що хочете видалити пристрій для читання \"{0}\"?",
"MessageConfirmDeleteFile": "Файл буде видалено з вашої файлової системи. Ви впевнені?",
"MessageConfirmDeleteLibrary": "Ви дійсно бажаєте назавжди видалити бібліотеку \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Елемент бібліотеки буде видалено з бази даних та вашої файлової системи. Ви впевнені?",
"MessageConfirmDeleteLibraryItems": "З бази даних та вашої файлової системи будуть видалені елементи бібліотеки: {0}. Ви впевнені?",
"MessageConfirmDeleteLibraryItem": "Елемент бібліотеки буде видалено з бази даних і файлової системи. Ви впевнені?",
"MessageConfirmDeleteLibraryItems": "Буде видалено {0} елементів бібліотеки з бази даних і файлової системи. Ви впевнені?",
"MessageConfirmDeleteMetadataProvider": "Ви впевнені, що хочете видалити користувацького постачальника метаданих \"{0}\"?",
"MessageConfirmDeleteNotification": "Ви впевнені, що хочете видалити це сповіщення?",
"MessageConfirmDeleteSession": "Ви дійсно бажаєте видалити цей сеанс?",
"MessageConfirmEmbedMetadataInAudioFiles": "Ви впевнені, що хочете вставити метадані в {0} аудіофайлів?",
"MessageConfirmEmbedMetadataInAudioFiles": "Ви впевнені, що хочете вбудувати метадані у {0} аудіофайлів?",
"MessageConfirmForceReScan": "Ви дійсно бажаєте примусово пересканувати?",
"MessageConfirmMarkAllEpisodesFinished": "Ви дійсно бажаєте позначити усі епізоди завершеними?",
"MessageConfirmMarkAllEpisodesNotFinished": "Ви дійсно бажаєте позначити усі епізоди незавершеними?",
"MessageConfirmMarkAllEpisodesFinished": "Ви впевнені, що хочете позначити всі епізоди завершеними?",
"MessageConfirmMarkAllEpisodesNotFinished": "Ви впевнені, що хочете позначити всі епізоди незавершеними?",
"MessageConfirmMarkItemFinished": "Ви впевнені, що хочете позначити \"{0}\" як завершене?",
"MessageConfirmMarkItemNotFinished": "Ви впевнені, що хочете позначити \"{0}\" як незавершене?",
"MessageConfirmMarkSeriesFinished": "Ви дійсно бажаєте позначити усі книги серії завершеними?",
"MessageConfirmMarkSeriesNotFinished": "Ви дійсно бажаєте позначити всі книги серії незавершеними?",
"MessageConfirmNotificationTestTrigger": "Активувати це сповіщення з тестовими даними?",
"MessageConfirmPurgeCache": "Очищення кешу видалить усю теку <code>/metadata/cache</code>. <br /><br />Ви дійсно бажаєте видалити теку кешу?",
"MessageConfirmPurgeItemsCache": "Очищення кешу елементів видалить усю теку <code>/metadata/cache/items</code>. <br />Ви певні?",
"MessageConfirmQuickEmbed": "Увага! Швидке вбудування не створює резервних копій ваших аудіо. Переконайтеся, що маєте копію ваших файлів.<br><br>Продовжити?",
"MessageConfirmQuickMatchEpisodes": "При виявленні співпадінь інформація про епізоди швидкого пошуку буде перезаписана. Будуть оновлені тільки несуперечливі епізоди. Ви впевнені?",
"MessageConfirmReScanLibraryItems": "Ви дійсно бажаєте пересканувати елементи: {0}?",
"MessageConfirmPurgeCache": "Очищення кешу видалить всю теку <code>/metadata/cache</code>. <br /><br />Ви впевнені, що хочете видалити теку кешу?",
"MessageConfirmPurgeItemsCache": "Очищення кешу елементів видалить всю теку <code>/metadata/cache/items</code>.<br />Ви впевнені?",
"MessageConfirmQuickEmbed": "Увага! Швидке вбудовування не створює резервних копій ваших аудіофайлів. Переконайтеся, що маєте резервну копію. <br><br>Продовжити?",
"MessageConfirmQuickMatchEpisodes": "Швидке співставлення епізодів перезапише подробиці, якщо знайдено відповідність. Оновлюються лише невідповідні епізоди. Ви впевнені?",
"MessageConfirmReScanLibraryItems": "Ви впевнені, що хочете пересканувати {0} елементів?",
"MessageConfirmRemoveAllChapters": "Ви дійсно бажаєте видалити усі глави?",
"MessageConfirmRemoveAuthor": "Ви дійсно бажаєте видалити автора \"{0}\"?",
"MessageConfirmRemoveCollection": "Ви дійсно бажаєте видалити добірку \"{0}\"?",
"MessageConfirmRemoveEpisode": "Ви дійсно бажаєте видалити епізод \"{0}\"?",
"MessageConfirmRemoveEpisodeNote": "Примітка: Це не видаляє аудіофайл, якщо не перемикає \"файл жорсткого видалення\"",
"MessageConfirmRemoveEpisodes": "Ви дійсно бажаєте видалити епізодів: {0}?",
"MessageConfirmRemoveListeningSessions": "Ви дійсно бажаєте видалити сеанси прослуховування: {0}?",
"MessageConfirmRemoveMetadataFiles": "Ви впевнені, що хочете видалити всі файли metadata.{0} у папках елементів вашої бібліотеки?",
"MessageConfirmRemoveNarrator": "Ви дійсно бажаєте видалити читця \"{0}\"?",
"MessageConfirmRemovePlaylist": "Ви дійсно бажаєте видалити список відтворення \"{0}\"?",
"MessageConfirmRenameGenre": "Ви дійсно бажаєте замінити жанр \"{0}\" на \"{1}\" для усіх елементів?",
"MessageConfirmRenameGenreMergeNote": "Примітка: такий жанр вже існує, тож їх буде об'єднано.",
"MessageConfirmRenameGenreWarning": "Увага! Вже існує схожий жанр у іншому регістрі \"{0}\".",
"MessageConfirmRenameTag": "Ви дійсно бажаєте замінити мітку \"{0}\" на \"{1}\" для усіх елементів?",
"MessageConfirmRenameTagMergeNote": "Примітка: така мітка вже існує, тож їх буде об'єднано.",
"MessageConfirmRenameTagWarning": "Увага! Вже існує схожа мітка у іншому регістрі \"{0}\".",
"MessageConfirmRemovePlaylist": "Ви дійсно бажаєте видалити ваш список відтворення \"{0}\"?",
"MessageConfirmRenameGenre": "Ви впевнені, що хочете перейменувати жанр \"{0}\" на \"{1}\" для всіх елементів?",
"MessageConfirmRenameGenreMergeNote": "Примітка: Такий жанр вже існує, тому вони будуть об'єднані.",
"MessageConfirmRenameGenreWarning": "Увага! Схожий жанр з іншом регістром вже існує \"{0}\".",
"MessageConfirmRenameTag": "Ви впевнені, що хочете перейменувати мітку \"{0}\" на \"{1}\" для всіх елементів?",
"MessageConfirmRenameTagMergeNote": "Примітка: Така мітка вже існує, тому вони будуть об'єднані.",
"MessageConfirmRenameTagWarning": "Увага! Схожа мітка з іншою регістром вже існує \"{0}\".",
"MessageConfirmResetProgress": "Ви впевнені, що хочете скинути свій прогрес?",
"MessageConfirmSendEbookToDevice": "Ви дійсно хочете відправити на пристрій \"{2}\" електроні книги: {0}, \"{1}\"?",
"MessageConfirmUnlinkOpenId": "Ви впевнені, що хочете відв'язати цього користувача від OpenID?",
"MessageDaysListenedInTheLastYear": "{0} днів, прослуханих за останній рік",
"MessageDownloadingEpisode": "Завантаження епізоду",
"MessageDownloadingEpisode": "Скачування епізоду",
"MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку",
"MessageEmbedFailed": "Не вдалося вбудувати!",
"MessageEmbedFinished": "Вбудовано!",
"MessageEmbedQueue": "В черзі на вбудовування метаданих ({0} в черзі)",
"MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}",
"MessageEmbedFinished": "Вбудовування завершено!",
"MessageEmbedQueue": "У черзі на вбудовування метаданих ({0} у черзі)",
"MessageEpisodesQueuedForDownload": "{0} епізод(ів) у черзі на завантаження",
"MessageEreaderDevices": "Аби гарантувати отримання електронних книг, вам може знадобитися додати вказану вище адресу електронної пошти як правильного відправника на кожному з пристроїв зі списку нижче.",
"MessageFeedURLWillBe": "URL-адреса каналу буде {0}",
"MessageFetching": "Отримання...",
"MessageForceReScanDescription": "Просканує усі файли заново, неначе вперше. ID3-мітки, файли OPF та текстові файли будуть проскановані як нові.",
"MessageForceReScanDescription": "Просканує всі файли заново, як при першому скануванні. ID3-мітки, OPF-файли та текстові файли будуть проскановані як нові.",
"MessageImportantNotice": "Важливе повідомлення!",
"MessageInsertChapterBelow": "Введіть главу нижче",
"MessageInvalidAsin": "Невірний ASIN",
"MessageItemsSelected": "Вибрано елементів: {0}",
"MessageItemsUpdated": "Оновлено елементів: {0}",
"MessageItemsSelected": "Вибрано {0} елементів",
"MessageItemsUpdated": "Оновлено {0} елементів",
"MessageJoinUsOn": "Приєднуйтесь до",
"MessageLoading": "Завантаження...",
"MessageLoadingFolders": "Завантаження тек...",
"MessageLoadingFolders": "Завантаження папок...",
"MessageLogsDescription": "Журнали зберігаються у <code>/metadata/logs</code> як JSON-файли. Журнали збоїв зберігаються у <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "Помилка M4B!",
"MessageM4BFinished": "M4B створено!",
"MessageMapChapterTitles": "Встановіть назви глав вашої аудіокниги без визначення налаштувань тривалості",
"MessageMapChapterTitles": "Встановіть назви глав вашої аудіокниги без зміни часових міток",
"MessageMarkAllEpisodesFinished": "Позначити всі епізоди завершеними",
"MessageMarkAllEpisodesNotFinished": "Позначити всі епізоди незавершеними",
"MessageMarkAsFinished": "Позначити завершеним",
"MessageMarkAsNotFinished": "Позначити незавершеним",
"MessageMatchBooksDescription": "Спробує віднайти книгу у вказаному джерелі пошуку та встановити подробиці та обкладинку, яких бракує. Не перезаписує подробиці.",
"MessageMarkAsFinished": "Позначити як завершене",
"MessageMarkAsNotFinished": "Позначити як незавершене",
"MessageMatchBooksDescription": "Спробує знайти книги у бібліотеці у вибраному джерелі пошуку та заповнити порожні подробиці й обкладинку. Не перезаписує подробиці.",
"MessageNoAudioTracks": "Аудіодоріжки відсутні",
"MessageNoAuthors": "Автори відсутні",
"MessageNoBackups": "Резервні копії відсутні",
@ -808,8 +809,8 @@
"MessageNoCoversFound": "Обкладинок не знайдено",
"MessageNoDescription": "Без опису",
"MessageNoDevices": "Немає пристроїв",
"MessageNoDownloadsInProgress": "Немає активних завантажень",
"MessageNoDownloadsQueued": "Немає завантажень у черзі",
"MessageNoDownloadsInProgress": "Немає активних скачувань",
"MessageNoDownloadsQueued": "Немає скачувань у черзі",
"MessageNoEpisodeMatchesFound": "Відповідних епізодів не знайдено",
"MessageNoEpisodes": "Епізоди відсутні",
"MessageNoFoldersAvailable": "Немає доступних тек",
@ -821,18 +822,18 @@
"MessageNoLogs": "Немає журнали",
"MessageNoMediaProgress": "Прогрес відсутній",
"MessageNoNotifications": "Сповіщення відсутні",
"MessageNoPodcastFeed": "Невірний подкаст: Немає каналу",
"MessageNoPodcastFeed": "Некоректний подкаст: немає каналу",
"MessageNoPodcastsFound": "Подкастів не знайдено",
"MessageNoResults": "Немає результатів",
"MessageNoSearchResultsFor": "Немає результатів пошуку для \"{0}\"",
"MessageNoSeries": "Без серії",
"MessageNoTags": "Без міток",
"MessageNoSeries": "Немає серій",
"MessageNoTags": "Немає міток",
"MessageNoTasksRunning": "Немає активних завдань",
"MessageNoUpdatesWereNecessary": "Оновлень не потрібно",
"MessageNoUpdatesWereNecessary": "Оновлення не потрібні",
"MessageNoUserPlaylists": "У вас немає списків відтворення",
"MessageNoUserPlaylistsHelp": "Списки відтворення приватні. Лише користувач, який їх створює, може бачити їх.",
"MessageNoUserPlaylistsHelp": "Списки відтворення приватні. Лише користувач, який їх створив, може їх бачити.",
"MessageNotYetImplemented": "Ще не реалізовано",
"MessageOpmlPreviewNote": "Примітка: це попередній перегляд OPML-файлу. Актуальна назва подкасту буде завантажена з RSS-каналу.",
"MessageOpmlPreviewNote": "Примітка: це попередній перегляд OPML-файлу. Актуальна назва подкасту буде взята з RSS-каналу.",
"MessageOr": "або",
"MessagePauseChapter": "Призупинити відтворення глави",
"MessagePlayChapter": "Слухати початок глави",
@ -841,7 +842,7 @@
"MessagePodcastHasNoRSSFeedForMatching": "Подкаст не має RSS-каналу для пошуку",
"MessagePodcastSearchField": "Введіть пошуковий запит або URL RSS-стрічки",
"MessageQuickEmbedInProgress": "Швидке вбудовування в процесі",
"MessageQuickEmbedQueue": "В черзі на швидке вбудовування ({0} в черзі)",
"MessageQuickEmbedQueue": "У черзі на швидке вбудовування ({0} в черзі)",
"MessageQuickMatchAllEpisodes": "Швидке співставлення всіх епізодів",
"MessageQuickMatchDescription": "Заповнити відсутні подробиці та обкладинку першим результатом пошуку '{0}'. Не перезаписує подробиці, якщо не увімкнено параметр \"Надавати перевагу віднайденим метаданим\".",
"MessageRemoveChapter": "Видалити главу",
@ -849,9 +850,9 @@
"MessageRemoveFromPlayerQueue": "Вилучити з черги відтворення",
"MessageRemoveUserWarning": "Ви дійсно бажаєте назавжди видалити користувача \"{0}\"?",
"MessageReportBugsAndContribute": "Повідомляйте про помилки, пропонуйте функції та долучайтеся на",
"MessageResetChaptersConfirm": "Ви дійсно бажаєте скинути глави та скасувати внесені зміни?",
"MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від",
"MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.",
"MessageResetChaptersConfirm": "Ви впевнені, що хочете скинути глави та скасувати внесені зміни?",
"MessageRestoreBackupConfirm": "Ви впевнені, що хочете відновити резервну копію, створену",
"MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних у /config і зображення обкладинок у /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють файли у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються.<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.",
"MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.",
"MessageScheduleRunEveryWeekdayAtTime": "Запуск кожні {0} о {1}",
"MessageSearchResultsFor": "Результати пошуку для",
@ -860,12 +861,12 @@
"MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера",
"MessageSetChaptersFromTracksDescription": "Створити глави з аудіодоріжок, встановивши назви файлів за заголовки",
"MessageShareExpirationWillBe": "Термін сплине за <strong>{0}</strong>",
"MessageShareExpiresIn": "Сплине за {0}",
"MessageShareURLWillBe": "Поширюваний URL - <strong>{0}</strong>",
"MessageShareExpiresIn": "Спливає через {0}",
"MessageShareURLWillBe": "URL для спільного доступу — <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "Почати відтворення \"{0}\" з {1}?",
"MessageTaskAudioFileNotWritable": "Аудіофайл \"{0}\" недоступний для запису",
"MessageTaskCanceledByUser": "Задача скасована користувачем",
"MessageTaskDownloadingEpisodeDescription": "Завантаження епізоду \"{0}\"",
"MessageTaskCanceledByUser": "Завдання скасовано користувачем",
"MessageTaskDownloadingEpisodeDescription": "Скачування епізоду \"{0}\"",
"MessageTaskEmbeddingMetadata": "Вбудовування метаданих",
"MessageTaskEmbeddingMetadataDescription": "Вбудовування метаданих у аудіокнигу \"{0}\"",
"MessageTaskEncodingM4b": "Кодування M4B",
@ -880,19 +881,19 @@
"MessageTaskMatchingBooksInLibrary": "Відповідність книг у бібліотеці \"{0}\"",
"MessageTaskNoFilesToScan": "Немає файлів для сканування",
"MessageTaskOpmlImport": "Імпорт OPML",
"MessageTaskOpmlImportDescription": "Створення подкастів з {0} RSS-стрічок",
"MessageTaskOpmlImportFeed": "Канал імпорту OPML",
"MessageTaskOpmlImportDescription": "Створення подкастів з {0} RSS-каналів",
"MessageTaskOpmlImportFeed": "Імпорт RSS-каналу OPML",
"MessageTaskOpmlImportFeedDescription": "Імпорт RSS-каналу \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Не вдалося отримати подкаст-стрічку",
"MessageTaskOpmlImportFeedFailed": "Не вдалося отримати подкаст-канал",
"MessageTaskOpmlImportFeedPodcastDescription": "Створення подкасту \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Подкаст вже існує за цим шляхом",
"MessageTaskOpmlImportFeedPodcastFailed": "Не вдалося створити подкаст",
"MessageTaskOpmlImportFinished": "Додано {0} подкастів",
"MessageTaskOpmlParseFailed": "Не вдалося розібрати файл OPML",
"MessageTaskOpmlParseFastFail": "Невірний файл OPML: не знайдено тег <opml> або тег <outline>",
"MessageTaskOpmlParseNoneFound": "У файлі OPML не знайдено жодного канала",
"MessageTaskOpmlParseFailed": "Не вдалося розібрати OPML-файл",
"MessageTaskOpmlParseFastFail": "Некоректний OPML-файл: не знайдено тег <opml> або <outline>",
"MessageTaskOpmlParseNoneFound": "У OPML-файлі не знайдено жодного каналу",
"MessageTaskScanItemsAdded": "{0} додано",
"MessageTaskScanItemsMissing": "{0} відсутній",
"MessageTaskScanItemsMissing": "{0} відсутні",
"MessageTaskScanItemsUpdated": "{0} оновлено",
"MessageTaskScanNoChangesNeeded": "Змін не потрібно",
"MessageTaskScanningFileChanges": "Сканування змін файлів у \"{0}\"",
@ -902,24 +903,24 @@
"MessageUploaderItemFailed": "Не вдалося завантажити",
"MessageUploaderItemSuccess": "Успішно завантажено!",
"MessageUploading": "Завантаження...",
"MessageValidCronExpression": "Допустима команда cron",
"MessageWatcherIsDisabledGlobally": "Спостерігача вимкнено в налаштуваннях сервера",
"MessageValidCronExpression": "Коректний cron-вираз",
"MessageWatcherIsDisabledGlobally": "Спостерігача вимкнено у глобальних налаштуваннях сервера",
"MessageXLibraryIsEmpty": "Бібліотека {0} порожня!",
"MessageYourAudiobookDurationIsLonger": "Тривалість вашої аудіокниги довша за віднайдену",
"MessageYourAudiobookDurationIsShorter": "Тривалість вашої аудіокниги коротша за віднайдену",
"NoteChangeRootPassword": "Тільки користувач root — єдиний, хто може мати порожній пароль",
"NoteChapterEditorTimes": "Примітка: Перша глава мусить починатися з 0:00, а час початку останньої глави не може бути більшим за зазначену тривалість аудіокниги.",
"NoteFolderPicker": "Примітка: вже обрані теки не буде показано",
"NoteRSSFeedPodcastAppsHttps": "Попередження: Більшість додатків подкастів вимагатимуть використання протоколу HTTPS від RSS-каналу",
"NoteRSSFeedPodcastAppsPubDate": "Попередження: 1 або більше ваших епізодів не мають дати публікації. Деякі додатки подкастів вимагають це.",
"NoteUploaderFoldersWithMediaFiles": "Теки з медіафайлами буде оброблено як окремі елементи бібліотеки.",
"NoteUploaderOnlyAudioFiles": "Якщо завантажувати лише аудіофайли, то кожен файл буде оброблено як окрему книгу.",
"NoteUploaderUnsupportedFiles": "Непідтримувані файли пропущено. Під час вибору або перетягування теки, файли, що знаходяться поза текою, пропускаються.",
"NotificationOnBackupCompletedDescription": "Запускається після завершення резервного копіювання",
"NotificationOnBackupFailedDescription": "Срабатывает при збої резервного копіювання",
"NotificationOnEpisodeDownloadedDescription": "Запускається при автоматичному завантаженні епізоду подкасту",
"NotificationOnRSSFeedDisabledDescription": "Активується, коли автоматичне завантаження епізодів вимкнено через занадто багато невдалих спроб",
"NotificationOnRSSFeedFailedDescription": "Активується, коли запит RSS-каналу не вдається виконати для автоматичного завантаження епізоду",
"MessageYourAudiobookDurationIsLonger": "Тривалість вашої аудіокниги більша за знайдену",
"MessageYourAudiobookDurationIsShorter": "Тривалість вашої аудіокниги менша за знайдену",
"NoteChangeRootPassword": "Тільки користувач root може мати порожній пароль",
"NoteChapterEditorTimes": "Примітка: Перша глава повинна починатися з 0:00, а час початку останньої глави не може перевищувати тривалість цієї аудіокниги.",
"NoteFolderPicker": "Примітка: вже додані папки не відображаються",
"NoteRSSFeedPodcastAppsHttps": "Попередження: більшість додатків подкастів вимагають використання HTTPS для RSS-каналу",
"NoteRSSFeedPodcastAppsPubDate": "Попередження: один або більше ваших епізодів не мають дати публікації. Деякі додатки подкастів цього вимагають.",
"NoteUploaderFoldersWithMediaFiles": "Теки з медіафайлами обробляються як окремі елементи бібліотеки.",
"NoteUploaderOnlyAudioFiles": "Якщо завантажувати лише аудіофайли, кожен файл буде окремою аудіокнигою.",
"NoteUploaderUnsupportedFiles": "Непідтримувані файли ігноруються. При виборі або перетягуванні теки, файли поза теками елементів ігноруються.",
"NotificationOnBackupCompletedDescription": "Виконується після завершення резервного копіювання",
"NotificationOnBackupFailedDescription": "Виконується при помилці резервного копіювання",
"NotificationOnEpisodeDownloadedDescription": "Виконується при автоматичному завантаженні епізоду подкасту",
"NotificationOnRSSFeedDisabledDescription": "Виконується, коли автоматичне завантаження епізодів вимкнено через забагато невдалих спроб",
"NotificationOnRSSFeedFailedDescription": "Виконується, коли запит RSS-каналу не вдається для автоматичного завантаження епізоду",
"NotificationOnTestDescription": "Подія для тестування системи сповіщень",
"PlaceholderNewCollection": "Нова назва добірки",
"PlaceholderNewFolderPath": "Новий шлях до теки",
@ -997,7 +998,7 @@
"ToastEncodeCancelFailed": "Не вдалося скасувати кодування",
"ToastEncodeCancelSucces": "Кодування скасовано",
"ToastEpisodeDownloadQueueClearFailed": "Не вдалося очистити чергу",
"ToastEpisodeDownloadQueueClearSuccess": "Чергу на завантаження епізодів очищено",
"ToastEpisodeDownloadQueueClearSuccess": "Чергу на скачування епізодів очищено",
"ToastEpisodeUpdateSuccess": "{0} епізодів оновлено",
"ToastErrorCannotShare": "Не можна типово поширити на цей пристрій",
"ToastFailedToLoadData": "Не вдалося завантажити дані",
@ -1005,7 +1006,7 @@
"ToastFailedToShare": "Не вдалося поділитися",
"ToastFailedToUpdate": "Не вдалося оновити",
"ToastInvalidImageUrl": "Невірний URL зображення",
"ToastInvalidMaxEpisodesToDownload": "Невірна кількість епізодів для завантаження",
"ToastInvalidMaxEpisodesToDownload": "Невірна кількість епізодів для скачування",
"ToastInvalidUrl": "Невірний URL",
"ToastItemCoverUpdateSuccess": "Обкладинку елемента оновлено",
"ToastItemDeletedFailed": "Не вдалося видалити елемент",

View file

@ -11,7 +11,7 @@
"ButtonAuthors": "作者",
"ButtonBack": "返回",
"ButtonBatchEditPopulateFromExisting": "用现有内容填充",
"ButtonBatchEditPopulateMapDetails": "填充地图详细信息",
"ButtonBatchEditPopulateMapDetails": "填入此项详情",
"ButtonBrowseForFolder": "浏览文件夹",
"ButtonCancel": "取消",
"ButtonCancelEncode": "取消编码",
@ -73,7 +73,7 @@
"ButtonQuickMatch": "快速匹配",
"ButtonReScan": "重新扫描",
"ButtonRead": "读取",
"ButtonReadLess": "阅读较少",
"ButtonReadLess": "收起",
"ButtonReadMore": "阅读更多",
"ButtonRefresh": "刷新",
"ButtonRemove": "移除",
@ -212,7 +212,7 @@
"HeaderUsers": "用户",
"HeaderYearReview": "{0} 年回顾",
"HeaderYourStats": "你的统计数据",
"LabelAbridged": "概要",
"LabelAbridged": "删节版",
"LabelAbridgedChecked": "删节版 (已勾选)",
"LabelAbridgedUnchecked": "未删节版 (未勾选)",
"LabelAccessibleBy": "可访问",
@ -320,7 +320,7 @@
"LabelEmailSettingsRejectUnauthorized": "拒绝未经授权的证书",
"LabelEmailSettingsRejectUnauthorizedHelp": "禁用SSL证书验证可能会使你的连接面临安全风险, 例如中间人攻击. 只有当你了解其中的含义并信任所连接的邮件服务器时, 才能禁用此选项.",
"LabelEmailSettingsSecure": "安全",
"LabelEmailSettingsSecureHelp": "如果选是, 则连接将在连接到服务器时使用TLS. 如果选否, 则若服务器支持STARTTLS扩展, 则使用TLS. 在大多数情况下, 如果连接到端口465, 请将该值设置为是. 对于端口587或25, 请保持为否. (来自nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsSecureHelp": "开启此选项时将始终通过TLS连接服务器。关闭此选项时仅在服务器支持STARTTLS扩展时使用TLS。在大多数情况下如果连接到端口465请将此项设为开启。如果连接到端口587或25请将此设置保持为关闭。来自nodemailer.com/smtp/#authentication",
"LabelEmailSettingsTestAddress": "测试地址",
"LabelEmbeddedCover": "嵌入封面",
"LabelEnable": "启用",
@ -346,15 +346,15 @@
"LabelExample": "示例",
"LabelExpandSeries": "展开系列",
"LabelExpandSubSeries": "展开子系列",
"LabelExplicit": "信息准确",
"LabelExplicitChecked": "明确(已选中",
"LabelExplicitUnchecked": "不明确 (未选中)",
"LabelExplicit": "含成人内容",
"LabelExplicitChecked": "成人内容(已核实",
"LabelExplicitUnchecked": "无成人内容 (未核实)",
"LabelExportOPML": "导出 OPML",
"LabelFeedURL": "源 URL",
"LabelFetchingMetadata": "正在获取元数据",
"LabelFile": "文件",
"LabelFileBirthtime": "文件创建时间",
"LabelFileBornDate": "于 {0}",
"LabelFileBornDate": "添加于 {0}",
"LabelFileModified": "文件修改时间",
"LabelFileModifiedDate": "已修改 {0}",
"LabelFilename": "文件名",
@ -482,7 +482,7 @@
"LabelPermanent": "永久的",
"LabelPermissionsAccessAllLibraries": "可以访问所有媒体库",
"LabelPermissionsAccessAllTags": "可以访问所有标签",
"LabelPermissionsAccessExplicitContent": "可以访问显式内容",
"LabelPermissionsAccessExplicitContent": "可以访问成人内容",
"LabelPermissionsCreateEreader": "可以创建电子阅读器",
"LabelPermissionsDelete": "可以删除",
"LabelPermissionsDownload": "可以下载",
@ -613,12 +613,12 @@
"LabelStartedAt": "从这开始",
"LabelStatsAudioTracks": "音轨",
"LabelStatsAuthors": "作者",
"LabelStatsBestDay": "最好的一天",
"LabelStatsBestDay": "单日最高",
"LabelStatsDailyAverage": "每日平均值",
"LabelStatsDays": "",
"LabelStatsDays": "连续收听",
"LabelStatsDaysListened": "收听天数",
"LabelStatsHours": "小时",
"LabelStatsInARow": "在一行",
"LabelStatsInARow": "",
"LabelStatsItemsFinished": "已完成的项目",
"LabelStatsItemsInLibrary": "媒体库中的项目",
"LabelStatsMinutes": "分钟",
@ -714,7 +714,7 @@
"MessageBackupsLocationNoEditNote": "注意: 备份位置是通过环境变量设置的, 不能在此处更改.",
"MessageBackupsLocationPathEmpty": "备份位置路径不能为空",
"MessageBatchEditPopulateMapDetailsAllHelp": "使用所有项目的数据填充已启用的字段. 具有多个值的字段将被合并",
"MessageBatchEditPopulateMapDetailsItemHelp": "使用此项目的数据填充已启用的地图详细信息字段",
"MessageBatchEditPopulateMapDetailsItemHelp": "提取此项目的信息,填入上方所有勾选的编辑框中",
"MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.",
"MessageBookshelfNoCollections": "你尚未进行任何收藏",
"MessageBookshelfNoCollectionsHelp": "收藏是公开的. 所有有权访问图书馆的用户都可以看到它们.",
@ -757,6 +757,7 @@
"MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?",
"MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?",
"MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?",
"MessageConfirmRemoveEpisodeNote": "注意:此操作不会删除音频文件,除非勾选“完全删除文件”选项",
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
"MessageConfirmRemoveListeningSessions": "你确定要移除 {0} 收听会话吗?",
"MessageConfirmRemoveMetadataFiles": "你确实要删除库项目文件夹中的所有 metadata.{0} 文件吗?",
@ -917,7 +918,9 @@
"NoteUploaderUnsupportedFiles": "不支持的文件将被忽略. 选择或删除文件夹时, 将忽略不在项目文件夹中的其他文件.",
"NotificationOnBackupCompletedDescription": "备份完成时触发",
"NotificationOnBackupFailedDescription": "备份失败时触发",
"NotificationOnEpisodeDownloadedDescription": "当播客节目自动下载时触发",
"NotificationOnEpisodeDownloadedDescription": "当播客节目自动下载完成时触发",
"NotificationOnRSSFeedDisabledDescription": "由于尝试失败次数过多而导致剧集自动下载被禁用时触发",
"NotificationOnRSSFeedFailedDescription": "当用于自动下载剧集的 RSS 源请求失败时触发",
"NotificationOnTestDescription": "测试通知系统的事件",
"PlaceholderNewCollection": "输入收藏夹名称",
"PlaceholderNewFolderPath": "输入文件夹路径",