Merge branch 'refs/heads/master' into mf/loginPage

This commit is contained in:
mfcar 2024-03-15 08:14:45 +00:00
commit a76da14fb0
No known key found for this signature in database
58 changed files with 2121 additions and 144 deletions

View File

@ -5,5 +5,6 @@ module.exports.config = {
ConfigPath: Path.resolve('config'),
MetadataPath: Path.resolve('metadata'),
FFmpegPath: '/usr/bin/ffmpeg',
FFProbePath: '/usr/bin/ffprobe'
FFProbePath: '/usr/bin/ffprobe',
SkipBinariesCheck: false
}

View File

@ -98,6 +98,9 @@
<template v-else-if="page === 'authors'">
<div class="flex-grow" />
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
<!-- author sort select -->
<controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
</template>
</div>
</div>
@ -183,6 +186,30 @@ export default {
}
]
},
authorSortItems() {
return [
{
text: this.$strings.LabelAuthorFirstLast,
value: 'name'
},
{
text: this.$strings.LabelAuthorLastFirst,
value: 'lastFirst'
},
{
text: this.$strings.LabelNumberOfBooks,
value: 'numBooks'
},
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: this.$strings.LabelUpdatedAt,
value: 'updatedAt'
}
]
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
@ -455,6 +482,9 @@ export default {
updateCollapseBookSeries() {
this.saveSettings()
},
updateAuthorSort() {
this.saveSettings()
},
saveSettings() {
this.$store.dispatch('user/updateUserSettings', this.settings)
},

View File

@ -368,9 +368,17 @@ export default {
id: 'ebook',
name: this.$strings.LabelHasEbook
},
{
id: 'no-ebook',
name: this.$strings.LabelMissingEbook
},
{
id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook
},
{
id: 'no-supplementary',
name: this.$strings.LabelMissingSupplementaryEbook
}
]
},

View File

@ -49,8 +49,8 @@
</div>
<div v-if="media.coverPath">
<p class="text-center text-gray-200">Current</p>
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a>
</div>
</div>

View File

@ -127,6 +127,7 @@ export default {
skipMatchingMediaWithIsbn: false,
autoScanCronExpression: null,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
}
}

View File

@ -49,6 +49,17 @@
</ui-tooltip>
</div>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isPodcastLibrary" class="py-3">
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="formUpdated" />
</div>
@ -73,6 +84,7 @@ export default {
skipMatchingMediaWithIsbn: false,
audiobooksOnly: false,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us'
}
},
@ -107,6 +119,7 @@ export default {
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly,
hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion
}
}
@ -121,6 +134,7 @@ export default {
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
}
},

View File

@ -57,7 +57,6 @@ export default {
},
watch: {
duration: {
immediate: true,
handler() {
this.setChapterTicks()
}
@ -205,10 +204,14 @@ export default {
},
windowResize() {
this.setTrackWidth()
this.setChapterTicks()
this.updatePlayedTrackWidth()
this.updateBufferTrack()
}
},
mounted() {
this.setTrackWidth()
this.setChapterTicks()
window.addEventListener('resize', this.windowResize)
},
beforeDestroy() {

View File

@ -11,13 +11,13 @@
</div>
{{ item }}
</div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div>
</form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item }}</span>
</div>
@ -54,7 +54,7 @@ export default {
menuDisabled: {
type: Boolean,
default: false
},
}
},
data() {
return {
@ -62,7 +62,9 @@ export default {
currentSearch: null,
typingTimeout: null,
isFocused: false,
menu: null
menu: null,
filteredItems: null,
selectedMenuItemIndex: null
}
},
watch: {
@ -91,24 +93,63 @@ export default {
return classes.join(' ')
},
itemsToShow() {
if (!this.currentSearch || !this.textInput) {
if (!this.currentSearch || !this.textInput || !this.filteredItems) {
return this.items
}
return this.items.filter((i) => {
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
return this.filteredItems
}
},
methods: {
editItem(item) {
this.$emit('edit', item)
},
keydownInput() {
search() {
if (!this.textInput) {
this.filteredItems = null
return
}
this.currentSearch = this.textInput
const results = this.items.filter((i) => {
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
this.filteredItems = results || []
},
keydownInput(event) {
let items = this.itemsToShow
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.currentSearch = this.textInput
this.search()
}, 100)
this.setInputWidth()
},
@ -120,6 +161,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@ -208,7 +267,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
if (this.$refs.input) this.$refs.input.focus()
if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
var newSelected = null
if (this.selected.includes(itemValue)) {
@ -219,6 +281,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
this.recalcMenuPos()
@ -245,6 +308,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@ -261,6 +325,7 @@ export default {
} else {
this.insertNewItem(this.textInput)
}
if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()

View File

@ -14,13 +14,13 @@
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
<span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
</div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div>
</form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item.name }}</span>
</div>
@ -63,7 +63,8 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
items: []
items: [],
selectedMenuItemIndex: null
}
},
watch: {
@ -122,7 +123,35 @@ export default {
this.items = results || []
},
keydownInput() {
keydownInput(event) {
let items = this.itemsToShow
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.search()
@ -137,6 +166,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@ -228,7 +275,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
if (this.$refs.input) this.$refs.input.focus()
if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
let newSelected = null
if (this.getIsSelected(item.id)) {
@ -244,6 +294,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
@ -271,6 +322,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@ -291,6 +343,7 @@ export default {
name: this.textInput
})
}
if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()

View File

@ -1,6 +1,6 @@
<template>
<div ref="wrapper" class="relative">
<input :id="inputId" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div>
@ -33,6 +33,7 @@ export default {
textCenter: Boolean,
clearable: Boolean,
inputId: String,
inputName: String,
step: [String, Number],
min: [String, Number]
},
@ -117,4 +118,4 @@ input:read-only {
input::-webkit-calendar-picker-indicator {
filter: invert(1);
}
</style>
</style>

View File

@ -20,44 +20,44 @@
<div class="overflow-hidden">
<transition name="slide">
<div v-if="openMapOptions" class="flex flex-wrap">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.authors" />
<!-- Authors filter only contains authors in this library, uses filter data -->
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" filter-key="authors" class="mb-4 ml-4" />
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" filter-key="authors" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.series" />
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-5 ml-4" />
</div>
<div class="flex items-center px-4 w-1/2">
<div class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.genres" />
<ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" :label="$strings.LabelGenres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-4 ml-4" />
<ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" :label="$strings.LabelGenres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-5 ml-4" />
</div>
<div class="flex items-center px-4 w-1/2">
<div class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.tags" />
<ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" :label="$strings.LabelTags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" />
<ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" :label="$strings.LabelTags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.narrators" />
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publisher" />
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-5 ml-4" />
</div>
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.language" />
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-5 ml-4" />
</div>
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.explicit" />
<div class="ml-4">
<ui-checkbox
@ -71,6 +71,20 @@
/>
</div>
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.abridged" />
<div class="ml-4">
<ui-checkbox
v-model="batchDetails.abridged"
:label="$strings.LabelAbridged"
:disabled="!selectedBatchUsage.abridged"
:checkbox-bg="!selectedBatchUsage.abridged ? 'bg' : 'primary'"
:check-color="!selectedBatchUsage.abridged ? 'gray-600' : 'green-500'"
border-color="gray-600"
:label-class="!selectedBatchUsage.abridged ? 'pl-2 text-base text-gray-400 font-semibold' : 'pl-2 text-base font-semibold'"
/>
</div>
</div>
<div class="w-full flex items-center justify-end p-4">
<ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">{{ $strings.ButtonApply }}</ui-btn>
@ -139,7 +153,8 @@ export default {
narrators: [],
publisher: null,
language: null,
explicit: false
explicit: false,
abridged: false
},
selectedBatchUsage: {
subtitle: false,
@ -151,7 +166,8 @@ export default {
narrators: false,
publisher: false,
language: false,
explicit: false
explicit: false,
abridged: false
},
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
openMapOptions: false

View File

@ -3,7 +3,7 @@
<app-book-shelf-toolbar page="authors" is-home :authors="authors" />
<div id="bookshelf" class="w-full h-full p-8 overflow-y-auto">
<div class="flex flex-wrap justify-center">
<template v-for="author in authors">
<template v-for="author in authorsSorted">
<cards-author-card :key="author.id" :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" />
</template>
</div>
@ -44,6 +44,22 @@ export default {
},
selectedAuthor() {
return this.$store.state.globals.selectedAuthor
},
authorSortBy() {
return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name'
},
authorSortDesc() {
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
},
authorsSorted() {
const sortProp = this.authorSortBy
const bDesc = this.authorSortDesc ? -1 : 1
return this.authors.sort((a, b) => {
if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
return a[sortProp] > b[sortProp] ? bDesc : -bDesc
}
return a[sortProp].localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
})
}
},
methods: {

View File

@ -8,11 +8,11 @@
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
<template v-for="(episode, index) in episodesMapped">
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
<div class="flex-grow pl-4 max-w-2xl">
<!-- mobile -->
<div class="flex md:hidden mb-2">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
<div class="flex-grow px-2">
<div class="flex items-center">
<div class="flex" @click.stop>

View File

@ -35,10 +35,10 @@
<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" />
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
<ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" />
<ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" />
<div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
</div>

View File

@ -22,7 +22,9 @@ const languageCodeMap = {
'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' },
'ru': { label: 'Русский', dateFnsLocale: 'ru' },
'sv': { label: 'Svenska', dateFnsLocale: 'sv' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' },
}
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
return {

View File

@ -11,7 +11,9 @@ export const state = () => ({
useChapterTrack: false,
seriesSortBy: 'name',
seriesSortDesc: false,
seriesFilterBy: 'all'
seriesFilterBy: 'all',
authorSortBy: 'name',
authorSortDesc: false
}
})

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Metaznačky",
"LabelMinute": "Minuta",
"LabelMissing": "Chybějící",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Chybějící díly",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Více",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.",
"LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami",
"LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analzyovat podtitul",
"LabelSettingsParseSubtitlesHelp": "Rozparsovat podtitul z názvů složek audioknih.<br>Podtiul musí být oddělen znakem \" - \"<br>tj. \"Název knihy - Zde Podtitul\" má podtitul \"Zde podtitul\"",
"LabelSettingsPreferMatchedMetadata": "Preferovat spárovaná metadata",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta-tags",
"LabelMinute": "Minut",
"LabelMissing": "Mangler",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Manglende dele",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mere",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.",
"LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside",
"LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Fortolk undertekster",
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Fehlend",
"LabelMissingEbook": "E-Book fehlt",
"LabelMissingParts": "Fehlende Teile",
"LabelMissingSupplementaryEbook": "Ergänzendes E-Book fehlt",
"LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App",
"LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.",
"LabelMore": "Mehr",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.",
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Metaetiquetas",
"LabelMinute": "Minuto",
"LabelMissing": "Ausente",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Partes Ausentes",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos",
"LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.",
"LabelMore": "Más",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.",
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Extraer Subtítulos",
"LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"",
"LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta märgendid",
"LabelMinute": "Minut",
"LabelMissing": "Puudub",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Puuduvad osad",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d",
"LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks <code>audiobookshelf://oauth</code>, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (<code>*</code>) ainukese kirjena kasutamine võimaldab mis tahes URI-d.",
"LabelMore": "Rohkem",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.",
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Lugege subtiitreid",
"LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Balises de métadonnée",
"LabelMinute": "Minute",
"LabelMissing": "Manquant",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Parties manquantes",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
"LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche dURI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. Lutilisation dun astérisque (<code>*</code>) comme seule entrée autorise nimporte quel URI.",
"LabelMore": "Plus",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent quun seul livre seront masquées sur la page de la série et sur les étagères de la page daccueil.",
"LabelSettingsHomePageBookshelfView": "La page daccueil utilise la vue étagère",
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analyser les sous-titres",
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.<br>Les sous-titres doivent être séparés par « - »<br>cest-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »",
"LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance",
@ -774,4 +778,4 @@
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
"ToastUserDeleteFailed": "Échec de la suppression de lutilisateur",
"ToastUserDeleteSuccess": "Utilisateur supprimé"
}
}

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuta",
"LabelMissing": "Nedostaje",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Nedostajali dijelovi",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Više",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu",
"LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parsaj podnapise",
"LabelSettingsParseSubtitlesHelp": "Izvadi podnapise iz imena od audiobook foldera.<br>Podnapis mora biti odvojen sa \" - \"<br>npr. \"Ime knjige - Podnapis ovdje\" ima podnapis \"Podnapis ovdje\"",
"LabelSettingsPreferMatchedMetadata": "Preferiraj matchane metapodatke",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta címkék",
"LabelMinute": "Perc",
"LabelMissing": "Hiányzó",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Hiányzó részek",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k",
"LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett <code>audiobookshelf://oauth</code>, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (<code>*</code>), akkor bármely URI engedélyezett.",
"LabelMore": "Több",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "A csak egy könyvet tartalmazó sorozatok el lesznek rejtve a sorozatok oldalról és a kezdőlap polcairól.",
"LabelSettingsHomePageBookshelfView": "Kezdőlap használja a könyvespolc nézetet",
"LabelSettingsLibraryBookshelfView": "Könyvtár használja a könyvespolc nézetet",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Feliratok elemzése",
"LabelSettingsParseSubtitlesHelp": "Feliratok kinyerése a hangoskönyv mappaneveiből.<br>A feliratnak el kell különülnie egy \" - \" jellel<br>például: \"Könyv címe - Egy felirat itt\" esetén a felirat \"Egy felirat itt\"",
"LabelSettingsPreferMatchedMetadata": "Preferált egyeztetett metaadatok",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuto",
"LabelMissing": "Altro",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Parti rimanenti",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti",
"LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è <code>audiobookshelf://oauth</code>, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (<code>*</code>) poiché l'unica voce consente qualsiasi URI.",
"LabelMore": "Molto",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.",
"LabelSettingsHomePageBookshelfView": "Home page con sfondo legno",
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analizza sottotitoli",
"LabelSettingsParseSubtitlesHelp": "Estrai i sottotitoli dai nomi delle cartelle degli audiolibri. <br> I sottotitoli devono essere separati da \" - \"<br> Per esempio \"Il signore degli anelli - Le due Torri \" avrà il sottotitolo \"Le due Torri\"",
"LabelSettingsPreferMatchedMetadata": "Preferisci i metadata trovati",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta žymos",
"LabelMinute": "Minutė",
"LabelMissing": "Trūksta",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Trūkstamos dalys",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Daugiau",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.",
"LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą",
"LabelSettingsLibraryBookshelfView": "Naudoti bibliotekos knygų lentynų vaizdą",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analizuoti subtitrus",
"LabelSettingsParseSubtitlesHelp": "Išskleisti subtitrus iš audioknygos aplanko pavadinimų.<br>Subtitrai turi būti atskirti brūkšniu \"-\"<br>pavyzdžiui, \"Knygos pavadinimas - Čia yra subtitrai\" turi subtitrą \"Čia yra subtitrai\"",
"LabelSettingsPreferMatchedMetadata": "Pirmenybė atitaikytiems metaduomenis",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta-tags",
"LabelMinute": "Minuut",
"LabelMissing": "Ontbrekend",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Ontbrekende delen",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Meer",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series die slechts een enkel boek bevatten worden verborgen op de seriespagina en de homepagina-planken.",
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
"LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parseer subtitel",
"LabelSettingsParseSubtitlesHelp": "Haal subtitels uit mapnaam van audioboek.<br>Subtitel moet gescheiden zijn met \" - \"<br>b.v. \"Boektitel - Een Subtitel Hier\" heeft als subtitel \"Een Subtitel Hier\"",
"LabelSettingsPreferMatchedMetadata": "Prefereer gematchte metadata",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minutt",
"LabelMissing": "Mangler",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Manglende deler",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mer",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier som har kun en bok vil bli gjemt på serie- og hjemmeside hyllen.",
"LabelSettingsHomePageBookshelfView": "Hjemmeside bruk bokhyllevisning",
"LabelSettingsLibraryBookshelfView": "Bibliotek bruk bokhyllevisning",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analyser undertekster",
"LabelSettingsParseSubtitlesHelp": "Trekk ut undertekster fra lydbok mappenavn.<br>undertekster må være separert med \" - \"<br>f.eks. \"Boktittel - Undertekst her\" har Undertekst \"Undertekst her\"",
"LabelSettingsPreferMatchedMetadata": "Foretrekk funnet metadata",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuta",
"LabelMissing": "Brakujący",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Brakujące cześci",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Więcej",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej",
"LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Przetwarzaj podtytuły",
"LabelSettingsParseSubtitlesHelp": "Opcja pozwala na pobranie podtytułu z nazwy folderu z audiobookiem. <br>Podtytuł musi być rozdzielony za pomocą separatora \" - \"<br>Przykład: \"Book Title - A Subtitle Here\" podtytuł \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Preferowanie dopasowanych metadanych",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Etiquetas Meta",
"LabelMinute": "Minuto",
"LabelMissing": "Ausente",
"LabelMissingEbook": "Ebook não existe",
"LabelMissingParts": "Partes Ausentes",
"LabelMissingSupplementaryEbook": "Ebook complementar não existe",
"LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas",
"LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de terceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.",
"LabelMore": "Mais",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.",
"LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal",
"LabelSettingsLibraryBookshelfView": "Usar visão estante na página da biblioteca",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analisar subtítulos",
"LabelSettingsParseSubtitlesHelp": "Extrair subtítulos do nome da pasta do audiobook.<br>Subtítulo deve estar separado por \" - \"<br>ex: \"Título do Livro - Um Subtítulo Aqui\" tem o subtítulo \"Um Subtítulo Aqui\"",
"LabelSettingsPreferMatchedMetadata": "Preferir metadados consultados",
@ -774,4 +778,4 @@
"ToastSocketFailedToConnect": "Falha na conexão do socket",
"ToastUserDeleteFailed": "Falha ao apagar usuário",
"ToastUserDeleteSuccess": "Usuário apagado"
}
}

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Мета теги",
"LabelMinute": "Минуты",
"LabelMissing": "Потеряно",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Потерянные части",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
"LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.",
"LabelMore": "Еще",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Серии, в которых всего одна книга, будут скрыты со страницы серий и полок домашней страницы.",
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице",
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Разбор подзаголовков",
"LabelSettingsParseSubtitlesHelp": "Извлечение подзаголовков из имен папок аудиокниг.<br>Подзаголовок должны быть отделен \" - \"<br>например \"Название Книги - Тут Подзаголовок\" подзаголовок будет \"Тут Подзаголовок\"",
"LabelSettingsPreferMatchedMetadata": "Предпочитать метаданные поиска",

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "Metamärken",
"LabelMinute": "Minut",
"LabelMissing": "Saknad",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Saknade delar",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mer",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier som har en enda bok kommer att döljas från seriesidan och hyllsidan på startsidan.",
"LabelSettingsHomePageBookshelfView": "Startsida använd bokhyllvy",
"LabelSettingsLibraryBookshelfView": "Bibliotek använd bokhyllvy",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analysera undertexter",
"LabelSettingsParseSubtitlesHelp": "Extrahera undertexter från mappnamn för ljudböcker.<br>Undertext måste vara åtskilda av \" - \"<br>t.ex. \"Boktitel - En undertitel här\" har undertiteln \"En undertitel här\"",
"LabelSettingsPreferMatchedMetadata": "Föredra matchad metadata",

781
client/strings/vi-vn.json Normal file
View File

@ -0,0 +1,781 @@
{
"ButtonAdd": "Thêm",
"ButtonAddChapters": "Thêm Chương",
"ButtonAddDevice": "Thêm Thiết Bị",
"ButtonAddLibrary": "Thêm Thư Viện",
"ButtonAddPodcasts": "Thêm Podcasts",
"ButtonAddUser": "Thêm Người Dùng",
"ButtonAddYourFirstLibrary": "Thêm thư viện đầu tiên của bạn",
"ButtonApply": "Áp Dụng",
"ButtonApplyChapters": "Áp Dụng Chương",
"ButtonAuthors": "Tác Giả",
"ButtonBrowseForFolder": "Duyệt Thư Mục",
"ButtonCancel": "Hủy",
"ButtonCancelEncode": "Hủy Mã Hóa",
"ButtonChangeRootPassword": "Thay Đổi Mật Khẩu Root",
"ButtonCheckAndDownloadNewEpisodes": "Kiểm Tra và Tải Xuống Các Tập Phim Mới",
"ButtonChooseAFolder": "Chọn một thư mục",
"ButtonChooseFiles": "Chọn tập tin",
"ButtonClearFilter": "Xóa Bộ Lọc",
"ButtonCloseFeed": "Đóng Feed",
"ButtonCollections": "Bộ Sưu Tập",
"ButtonConfigureScanner": "Cấu Hình Bộ Quét",
"ButtonCreate": "Tạo",
"ButtonCreateBackup": "Tạo Bản Sao Lưu",
"ButtonDelete": "Xóa",
"ButtonDownloadQueue": "Hàng Chờ",
"ButtonEdit": "Chỉnh Sửa",
"ButtonEditChapters": "Chỉnh Sửa Chương",
"ButtonEditPodcast": "Chỉnh Sửa Podcast",
"ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Đường Dẫn Đầy Đủ",
"ButtonHide": "Ẩn",
"ButtonHome": "Trang Chủ",
"ButtonIssues": "Vấn Đề",
"ButtonJumpBackward": "Bước Lùi",
"ButtonJumpForward": "Bước Tiến",
"ButtonLatest": "Mới Nhất",
"ButtonLibrary": "Thư Viện",
"ButtonLogout": "Đăng Xuất",
"ButtonLookup": "Tra Cứu",
"ButtonManageTracks": "Quản Lý Tracks",
"ButtonMapChapterTitles": "Ánh Xạ Tiêu Đề Chương",
"ButtonMatchAllAuthors": "Khớp Tất Cả Tác Giả",
"ButtonMatchBooks": "Khớp Sách",
"ButtonNevermind": "Không Sao",
"ButtonNext": "Tiếp Theo",
"ButtonNextChapter": "Chương Tiếp Theo",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Mở Feed",
"ButtonOpenManager": "Mở Quản Lý",
"ButtonPause": "Tạm Dừng",
"ButtonPlay": "Phát",
"ButtonPlaying": "Đang Phát",
"ButtonPlaylists": "Danh Sách Phát",
"ButtonPrevious": "Trước",
"ButtonPreviousChapter": "Chương Trước",
"ButtonPurgeAllCache": "Xóa Sạch Tất Cả Bộ Nhớ Cache",
"ButtonPurgeItemsCache": "Xóa Sạch Bộ Nhớ Cache Các Mục",
"ButtonPurgeMediaProgress": "Xóa Sạch Tiến Trình Phương Tiện",
"ButtonQueueAddItem": "Thêm vào hàng đợi",
"ButtonQueueRemoveItem": "Xóa khỏi hàng đợi",
"ButtonQuickMatch": "Khớp Nhanh",
"ButtonRead": "Đọc",
"ButtonRefresh": "Làm Mới",
"ButtonRemove": "Xóa",
"ButtonRemoveAll": "Xóa Tất Cả",
"ButtonRemoveAllLibraryItems": "Xóa Tất Cả Các Mục Thư Viện",
"ButtonRemoveFromContinueListening": "Xóa khỏi Tiếp Tục Nghe",
"ButtonRemoveFromContinueReading": "Xóa khỏi Tiếp Tục Đọc",
"ButtonRemoveSeriesFromContinueSeries": "Xóa Series khỏi Tiếp Tục Series",
"ButtonReScan": "Quét Lại",
"ButtonReset": "Đặt Lại",
"ButtonResetToDefault": "Đặt Lại về Mặc Định",
"ButtonRestore": "Khôi Phục",
"ButtonSave": "Lưu",
"ButtonSaveAndClose": "Lưu & Đóng",
"ButtonSaveTracklist": "Lưu Danh Sách Track",
"ButtonScan": "Quét",
"ButtonScanLibrary": "Quét Thư Viện",
"ButtonSearch": "Tìm Kiếm",
"ButtonSelectFolderPath": "Chọn Đường Dẫn Thư Mục",
"ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Đặt chương từ các track",
"ButtonShare": "Chia Sẻ",
"ButtonShiftTimes": "Dời Thời Gian",
"ButtonShow": "Hiện",
"ButtonStartM4BEncode": "Bắt đầu Mã Hóa M4B",
"ButtonStartMetadataEmbed": "Bắt đầu Nhúng Dữ Liệu",
"ButtonSubmit": "Gửi",
"ButtonTest": "Kiểm Tra",
"ButtonUpload": "Tải Lên",
"ButtonUploadBackup": "Tải Lên Bản Sao Lưu",
"ButtonUploadCover": "Tải Lên Bìa",
"ButtonUploadOPMLFile": "Tải Lên Tệp OPML",
"ButtonUserDelete": "Xóa người dùng {0}",
"ButtonUserEdit": "Chỉnh Sửa người dùng {0}",
"ButtonViewAll": "Xem Tất Cả",
"ButtonYes": "Có",
"ErrorUploadFetchMetadataAPI": "Lỗi khi lấy dữ liệu metadata",
"ErrorUploadFetchMetadataNoResults": "Không thể lấy dữ liệu metadata - hãy thử cập nhật tiêu đề và/hoặc tác giả",
"ErrorUploadLacksTitle": "Phải có một tiêu đề",
"HeaderAccount": "Tài Khoản",
"HeaderAdvanced": "Nâng Cao",
"HeaderAppriseNotificationSettings": "Cài Đặt Thông Báo Apprise",
"HeaderAudiobookTools": "Công Cụ Quản Lý Tệp Truyện Nói",
"HeaderAudioTracks": "Các Track Âm Thanh",
"HeaderAuthentication": "Xác Thực",
"HeaderBackups": "Bản Sao Lưu",
"HeaderChangePassword": "Thay Đổi Mật Khẩu",
"HeaderChapters": "Chương",
"HeaderChooseAFolder": "Chọn Một Thư Mục",
"HeaderCollection": "Bộ Sưu Tập",
"HeaderCollectionItems": "Các Mục Bộ Sưu Tập",
"HeaderCover": "Bìa",
"HeaderCurrentDownloads": "Tải Xuống Hiện Tại",
"HeaderCustomMetadataProviders": "Các Nhà Cung Cấp Metadata Tùy Chỉnh",
"HeaderDetails": "Chi Tiết",
"HeaderDownloadQueue": "Hàng Đợi Tải Xuống",
"HeaderEbookFiles": "Tệp Ebook",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Cài Đặt Email",
"HeaderEpisodes": "Tập Phim",
"HeaderEreaderDevices": "Thiết Bị Đọc Sách",
"HeaderEreaderSettings": "Cài Đặt Thiết Bị Đọc Sách",
"HeaderFiles": "Tệp",
"HeaderFindChapters": "Tìm Kiếm Chương",
"HeaderIgnoredFiles": "Tệp Bị Bỏ Qua",
"HeaderItemFiles": "Tệp Mục",
"HeaderItemMetadataUtils": "Công Cụ Metadata Mục",
"HeaderLastListeningSession": "Phiên Nghe Gần Nhất",
"HeaderLatestEpisodes": "Tập Mới Nhất",
"HeaderLibraries": "Thư Viện",
"HeaderLibraryFiles": "Tệp Thư Viện",
"HeaderLibraryStats": "Thống Kê Thư Viện",
"HeaderListeningSessions": "Phiên Nghe",
"HeaderListeningStats": "Thống Kê Nghe",
"HeaderLogin": "Đăng Nhập",
"HeaderLogs": "Nhật Ký",
"HeaderManageGenres": "Quản Lý Thể Loại",
"HeaderManageTags": "Quản Lý Thẻ",
"HeaderMapDetails": "Bản Đồ Chi Tiết",
"HeaderMatch": "Kết Hợp",
"HeaderMetadataOrderOfPrecedence": "Thứ Tự Ưu Tiên Metadata",
"HeaderMetadataToEmbed": "Metadata để nhúng",
"HeaderNewAccount": "Tài Khoản Mới",
"HeaderNewLibrary": "Thư Viện Mới",
"HeaderNotifications": "Thông Báo",
"HeaderOpenIDConnectAuthentication": "Xác Thực Mở ID Connect",
"HeaderOpenRSSFeed": "Mở RSS Feed",
"HeaderOtherFiles": "Các Tệp Khác",
"HeaderPasswordAuthentication": "Xác Thực Mật Khẩu",
"HeaderPermissions": "Quyền Hạn",
"HeaderPlayerQueue": "Hàng Đợi Người Chơi",
"HeaderPlaylist": "Danh Sách Phát",
"HeaderPlaylistItems": "Các Mục Danh Sách Phát",
"HeaderPodcastsToAdd": "Podcasts để Thêm",
"HeaderPreviewCover": "Xem Trước Bìa",
"HeaderRemoveEpisode": "Xóa Tập",
"HeaderRemoveEpisodes": "Xóa {0} Tập",
"HeaderRSSFeedGeneral": "Chi Tiết RSS",
"HeaderRSSFeedIsOpen": "RSS Feed Đã Mở",
"HeaderRSSFeeds": "RSS Feeds",
"HeaderSavedMediaProgress": "Tiến Trình Phương Tiện Đã Lưu",
"HeaderSchedule": "Lịch Trình",
"HeaderScheduleLibraryScans": "Lên Lịch Quét Tự Động Thư Viện",
"HeaderSession": "Phiên",
"HeaderSetBackupSchedule": "Đặt Lịch Sao Lưu",
"HeaderSettings": "Cài Đặt",
"HeaderSettingsDisplay": "Hiển Thị",
"HeaderSettingsExperimental": "Tính Năng Thử Nghiệm",
"HeaderSettingsGeneral": "Chung",
"HeaderSettingsScanner": "Máy Quét",
"HeaderSleepTimer": "Hẹn Giờ Tắt",
"HeaderStatsLargestItems": "Các Mục Lớn Nhất",
"HeaderStatsLongestItems": "Các Mục Dài Nhất (giờ)",
"HeaderStatsMinutesListeningChart": "Thống Kê Thời Gian Nghe (7 ngày gần nhất)",
"HeaderStatsRecentSessions": "Các Phiên Gần Đây",
"HeaderStatsTop10Authors": "10 Tác Giả Hàng Đầu",
"HeaderStatsTop5Genres": "5 Thể Loại Hàng Đầu",
"HeaderTableOfContents": "Mục Lục",
"HeaderTools": "Công Cụ",
"HeaderUpdateAccount": "Cập Nhật Tài Khoản",
"HeaderUpdateAuthor": "Cập Nhật Tác Giả",
"HeaderUpdateDetails": "Cập Nhật Chi Tiết",
"HeaderUpdateLibrary": "Cập Nhật Thư Viện",
"HeaderUsers": "Người Dùng",
"HeaderYearReview": "Năm {0} trong Xem Xét",
"HeaderYourStats": "Thống Kê Của Bạn",
"LabelAbridged": "Rút Gọn",
"LabelAccountType": "Loại Tài Khoản",
"LabelAccountTypeAdmin": "Quản Trị Viên",
"LabelAccountTypeGuest": "Khách",
"LabelAccountTypeUser": "Người Dùng",
"LabelActivity": "Hoạt Động",
"LabelAdded": "Đã Thêm",
"LabelAddedAt": "Đã Thêm Lúc",
"LabelAddToCollection": "Thêm vào Bộ Sưu Tập",
"LabelAddToCollectionBatch": "Thêm {0} Sách vào Bộ Sưu Tập",
"LabelAddToPlaylist": "Thêm vào Danh Sách Phát",
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "All",
"LabelAllUsers": "All Users",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library",
"LabelAppend": "Append",
"LabelAuthor": "Author",
"LabelAuthorFirstLast": "Author (First Last)",
"LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Back to User",
"LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB)",
"LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
"LabelBackupsNumberToKeep": "Number of backups to keep",
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Sách",
"LabelButtonText": "Nút Văn Bản",
"LabelChangePassword": "Đổi Mật Khẩu",
"LabelChannels": "Kênh",
"LabelChapters": "Chương",
"LabelChaptersFound": "chương được tìm thấy",
"LabelChapterTitle": "Tiêu đề Chương",
"LabelClickForMoreInfo": "Nhấn để biết thêm thông tin",
"LabelClosePlayer": "Đóng trình phát",
"LabelCodec": "Mã hóa",
"LabelCollapseSeries": "Thu gọn Series",
"LabelCollection": "Bộ Sưu Tập",
"LabelCollections": "Các Bộ Sưu Tập",
"LabelComplete": "Hoàn Thành",
"LabelConfirmPassword": "Xác Nhận Mật Khẩu",
"LabelContinueListening": "Tiếp Tục Nghe",
"LabelContinueReading": "Tiếp Tục Đọc",
"LabelContinueSeries": "Tiếp Tục Series",
"LabelCover": "Bìa",
"LabelCoverImageURL": "URL Ảnh Bìa",
"LabelCreatedAt": "Được Tạo Lúc",
"LabelCronExpression": "Biểu Thức Cron",
"LabelCurrent": "Hiện tại",
"LabelCurrently": "Hiện tại:",
"LabelCustomCronExpression": "Biểu Thức Cron Tùy Chỉnh:",
"LabelDatetime": "Ngày giờ",
"LabelDeleteFromFileSystemCheckbox": "Xóa khỏi hệ thống tệp (bỏ chọn để chỉ xóa khỏi cơ sở dữ liệu)",
"LabelDescription": "Mô Tả",
"LabelDeselectAll": "Bỏ Chọn Tất Cả",
"LabelDevice": "Thiết Bị",
"LabelDeviceInfo": "Thông Tin Thiết Bị",
"LabelDeviceIsAvailableTo": "Thiết Bị Đã Sẵn Sàng Cho...",
"LabelDirectory": "Thư Mục",
"LabelDiscFromFilename": "Đĩa từ Tên Tệp",
"LabelDiscFromMetadata": "Đĩa từ Metadata",
"LabelDiscover": "Khám Phá",
"LabelDownload": "Tải Xuống",
"LabelDownloadNEpisodes": "Tải Xuống {0} Tập",
"LabelDuration": "Thời Lượng",
"LabelDurationFound": "Thời lượng được tìm thấy:",
"LabelEbook": "Ebook",
"LabelEbooks": "Các Ebook",
"LabelEdit": "Chỉnh Sửa",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Địa chỉ Gửi từ",
"LabelEmailSettingsSecure": "Bảo Mật",
"LabelEmailSettingsSecureHelp": "Nếu đúng thì kết nối sẽ sử dụng TLS khi kết nối đến máy chủ. Nếu sai thì TLS sẽ được sử dụng nếu máy chủ hỗ trợ phần mở rộng STARTTLS. Trong hầu hết các trường hợp, hãy đặt giá trị này là đúng nếu bạn kết nối đến cổng 465. Đối với cổng 587 hoặc 25, giữ nó sai. (từ nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Địa Chỉ Kiểm Tra",
"LabelEmbeddedCover": "Bìa Nội",
"LabelEnable": "Bật",
"LabelEnd": "Kết Thúc",
"LabelEpisode": "Tập",
"LabelEpisodeTitle": "Tiêu Đề Tập",
"LabelEpisodeType": "Loại Tập",
"LabelExample": "Ví Dụ",
"LabelExplicit": "Rõ Ràng",
"LabelFeedURL": "URL Feed",
"LabelFetchingMetadata": "Đang Lấy Metadata",
"LabelFile": "Tệp",
"LabelFileBirthtime": "Thời Gian Tạo Tệp",
"LabelFileModified": "Sửa Đổi Tệp",
"LabelFilename": "Tên Tệp",
"LabelFilterByUser": "Lọc theo Người Dùng",
"LabelFindEpisodes": "Tìm Tập",
"LabelFinished": "Hoàn Thành",
"LabelFolder": "Thư Mục",
"LabelFolders": "Các Thư Mục",
"LabelFontBold": "Đậm",
"LabelFontFamily": "Gia đình font",
"LabelFontItalic": "Nghiêng",
"LabelFontScale": "Tỷ lệ font",
"LabelFontStrikethrough": "Gạch ngang",
"LabelFormat": "Định dạng",
"LabelGenre": "Thể loại",
"LabelGenres": "Các thể loại",
"LabelHardDeleteFile": "Xóa tập tin vĩnh viễn",
"LabelHasEbook": "Có ebook",
"LabelHasSupplementaryEbook": "Có ebook bổ sung",
"LabelHighestPriority": "Ưu tiên cao nhất",
"LabelHost": "Máy chủ",
"LabelHour": "Giờ",
"LabelIcon": "Biểu tượng",
"LabelImageURLFromTheWeb": "URL hình ảnh từ web",
"LabelIncludeInTracklist": "Bao gồm trong danh sách phát",
"LabelIncomplete": "Chưa hoàn thành",
"LabelInProgress": "Đang tiến hành",
"LabelInterval": "Khoảng cách",
"LabelIntervalCustomDailyWeekly": "Tuỳ chỉnh hàng ngày/hàng tuần",
"LabelIntervalEvery12Hours": "Mỗi 12 giờ",
"LabelIntervalEvery15Minutes": "Mỗi 15 phút",
"LabelIntervalEvery2Hours": "Mỗi 2 giờ",
"LabelIntervalEvery30Minutes": "Mỗi 30 phút",
"LabelIntervalEvery6Hours": "Mỗi 6 giờ",
"LabelIntervalEveryDay": "Mỗi ngày",
"LabelIntervalEveryHour": "Mỗi giờ",
"LabelInvalidParts": "Phần không hợp lệ",
"LabelInvert": "Nghịch đảo",
"LabelItem": "Mục",
"LabelLanguage": "Ngôn ngữ",
"LabelLanguageDefaultServer": "Ngôn ngữ Máy chủ mặc định",
"LabelLastBookAdded": "Sách mới nhất được thêm",
"LabelLastBookUpdated": "Sách mới nhất được cập nhật",
"LabelLastSeen": "Lần cuối nhìn thấy",
"LabelLastTime": "Lần cuối",
"LabelLastUpdate": "Cập nhật cuối cùng",
"LabelLayout": "Bố cục",
"LabelLayoutSinglePage": "Một trang",
"LabelLayoutSplitPage": "Chia trang",
"LabelLess": "Ít hơn",
"LabelLibrariesAccessibleToUser": "Thư viện có thể truy cập cho người dùng",
"LabelLibrary": "Thư viện",
"LabelLibraryItem": "Mục thư viện",
"LabelLibraryName": "Tên thư viện",
"LabelLimit": "Giới hạn",
"LabelLineSpacing": "Khoảng cách dòng",
"LabelListenAgain": "Nghe lại",
"LabelLogLevelDebug": "Gỡ lỗi",
"LabelLogLevelInfo": "Thông tin",
"LabelLogLevelWarn": "Cảnh báo",
"LabelLookForNewEpisodesAfterDate": "Tìm tập mới sau ngày này",
"LabelLowestPriority": "Ưu tiên thấp nhất",
"LabelMatchExistingUsersBy": "Kết hợp người dùng hiện có theo",
"LabelMatchExistingUsersByDescription": "Sử dụng để kết nối người dùng hiện có. Khi kết nối, người dùng sẽ được kết hợp bằng một ID duy nhất từ nhà cung cấp SSO của bạn",
"LabelMediaPlayer": "Trình phát đa phương tiện",
"LabelMediaType": "Loại phương tiện",
"LabelMetadataOrderOfPrecedenceDescription": "Nguồn siêu dữ liệu ưu tiên cao hơn sẽ ghi đè lên các nguồn siêu dữ liệu ưu tiên thấp hơn",
"LabelMetadataProvider": "Nhà cung cấp siêu dữ liệu",
"LabelMetaTag": "Thẻ Meta",
"LabelMetaTags": "Các thẻ Meta",
"LabelMinute": "Phút",
"LabelMissing": "Thiếu",
"LabelMissingEbook": "Không có ebook",
"LabelMissingParts": "Các phần thiếu",
"LabelMissingSupplementaryEbook": "Không có ebook bổ sung",
"LabelMobileRedirectURIs": "URI chuyển hướng di động được cho phép",
"LabelMobileRedirectURIsDescription": "Đây là danh sách trắng các URI chuyển hướng hợp lệ cho ứng dụng di động. Mặc định là <code>audiobookshelf://oauth</code>, bạn có thể loại bỏ hoặc bổ sung thêm các URI cho tích hợp ứng dụng bên thứ ba. Sử dụng dấu hoa thị (<code>*</code>) như một mục duy nhất cho phép bất kỳ URI nào.",
"LabelMore": "Thêm",
"LabelMoreInfo": "Thông tin thêm",
"LabelName": "Tên",
"LabelNarrator": "Người kể",
"LabelNarrators": "Các người kể",
"LabelNew": "Mới",
"LabelNewestAuthors": "Nhà văn mới nhất",
"LabelNewestEpisodes": "Tập mới nhất",
"LabelNewPassword": "Mật khẩu mới",
"LabelNextBackupDate": "Ngày sao lưu tiếp theo",
"LabelNextScheduledRun": "Chạy tiếp theo theo lịch trình",
"LabelNoEpisodesSelected": "Không có tập nào được chọn",
"LabelNotes": "Ghi chú",
"LabelNotFinished": "Chưa hoàn thành",
"LabelNotificationAppriseURL": "URL(s) thông báo",
"LabelNotificationAvailableVariables": "Biến có sẵn",
"LabelNotificationBodyTemplate": "Mẫu Nội dung",
"LabelNotificationEvent": "Sự kiện Thông báo",
"LabelNotificationsMaxFailedAttempts": "Số lần thất bại tối đa",
"LabelNotificationsMaxFailedAttemptsHelp": "Thông báo sẽ bị vô hiệu hóa sau khi thất bại gửi số lần này",
"LabelNotificationsMaxQueueSize": "Kích thước hàng đợi tối đa cho sự kiện thông báo",
"LabelNotificationsMaxQueueSizeHelp": "Các sự kiện bị giới hạn mỗi giây chỉ gửi 1 lần. Các sự kiện sẽ bị bỏ qua nếu hàng đợi đạt kích thước tối đa. Điều này ngăn chặn spam thông báo.",
"LabelNotificationTitleTemplate": "Mẫu Tiêu đề",
"LabelNotStarted": "Chưa bắt đầu",
"LabelNumberOfBooks": "Số lượng Sách",
"LabelNumberOfEpisodes": "# của Tập",
"LabelOpenRSSFeed": "Mở RSS Feed",
"LabelOverwrite": "Ghi đè",
"LabelPassword": "Mật khẩu",
"LabelPath": "Đường dẫn",
"LabelPermissionsAccessAllLibraries": "Có Thể Truy Cập Tất Cả Thư Viện",
"LabelPermissionsAccessAllTags": "Có Thể Truy Cập Tất Cả Thẻ",
"LabelPermissionsAccessExplicitContent": "Có Thể Truy Cập Nội Dung Rõ Ràng",
"LabelPermissionsDelete": "Có Thể Xóa",
"LabelPermissionsDownload": "Có Thể Tải Xuống",
"LabelPermissionsUpdate": "Có Thể Cập Nhật",
"LabelPermissionsUpload": "Có Thể Tải Lên",
"LabelPersonalYearReview": "Năm của Bạn trong Bài Đánh Giá ({0})",
"LabelPhotoPathURL": "Đường dẫn/URL ảnh",
"LabelPlaylists": "Danh sách phát",
"LabelPlayMethod": "Phương pháp phát",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Các podcast",
"LabelPodcastSearchRegion": "Vùng tìm kiếm podcast",
"LabelPodcastType": "Loại Podcast",
"LabelPort": "Cổng",
"LabelPrefixesToIgnore": "Tiền tố để bỏ qua (không phân biệt chữ hoa/chữ thường)",
"LabelPreventIndexing": "Ngăn chặn feed của bạn được chỉ mục bởi thư mục podcast của iTunes và Google",
"LabelPrimaryEbook": "Ebook chính",
"LabelProgress": "Tiến độ",
"LabelProvider": "Nhà cung cấp",
"LabelPubDate": "Ngày Xuất bản",
"LabelPublisher": "Nhà xuất bản",
"LabelPublishYear": "Năm Xuất bản",
"LabelRead": "Đọc",
"LabelReadAgain": "Đọc lại",
"LabelReadEbookWithoutProgress": "Đọc ebook mà không giữ tiến độ",
"LabelRecentlyAdded": "Gần đây thêm vào",
"LabelRecentSeries": "Loạt phim gần đây",
"LabelRecommended": "Được khuyến nghị",
"LabelRedo": "Làm lại",
"LabelRegion": "Khu vực",
"LabelReleaseDate": "Ngày Phát hành",
"LabelRemoveCover": "Xóa ảnh bìa",
"LabelRowsPerPage": "Số dòng mỗi trang",
"LabelRSSFeedCustomOwnerEmail": "Email chủ sở hữu tùy chỉnh",
"LabelRSSFeedCustomOwnerName": "Tên chủ sở hữu tùy chỉnh",
"LabelRSSFeedOpen": "Mở RSS Feed",
"LabelRSSFeedPreventIndexing": "Ngăn chặn Chỉ mục RSS Feed",
"LabelRSSFeedSlug": "Slug RSS Feed",
"LabelRSSFeedURL": "URL RSS Feed",
"LabelSearchTerm": "Thuật ngữ tìm kiếm",
"LabelSearchTitle": "Tìm kiếm Tiêu đề",
"LabelSearchTitleOrASIN": "Tìm kiếm Tiêu đề hoặc ASIN",
"LabelSeason": "Mùa",
"LabelSelectAllEpisodes": "Chọn tất cả các tập",
"LabelSelectEpisodesShowing": "Chọn {0} tập đang hiển thị",
"LabelSelectUsers": "Chọn người dùng",
"LabelSendEbookToDevice": "Gửi Ebook tới...",
"LabelSequence": "Trình tự",
"LabelSeries": "Loạt",
"LabelSeriesName": "Tên loạt",
"LabelSeriesProgress": "Tiến độ loạt",
"LabelServerYearReview": "Năm của Máy chủ trong Bài Đánh Giá ({0})",
"LabelSetEbookAsPrimary": "Đặt làm chính",
"LabelSetEbookAsSupplementary": "Đặt là bổ sung",
"LabelSettingsAudiobooksOnly": "Chỉ sách nói",
"LabelSettingsAudiobooksOnlyHelp": "Bật cài đặt này sẽ bỏ qua các tập tin ebook trừ khi chúng ở trong một thư mục sách nói, trong trường hợp đó chúng sẽ được đặt làm ebook bổ sung",
"LabelSettingsBookshelfViewHelp": "Thiết kế giả lập với kệ gỗ",
"LabelSettingsChromecastSupport": "Hỗ trợ Chromecast",
"LabelSettingsDateFormat": "Định dạng Ngày",
"LabelSettingsDisableWatcher": "Tắt Watcher",
"LabelSettingsDisableWatcherForLibrary": "Tắt watcher thư mục cho thư viện",
"LabelSettingsDisableWatcherHelp": "Tắt chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
"LabelSettingsEnableWatcher": "Bật Watcher",
"LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện",
"LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
"LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm",
"LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.",
"LabelSettingsFindCovers": "Tìm ảnh bìa",
"LabelSettingsFindCoversHelp": "Nếu sách nói của bạn không có ảnh bìa nhúng hoặc ảnh bìa trong thư mục, trình quét sẽ cố gắng tìm ảnh bìa.<br>Lưu ý: Điều này sẽ kéo dài thời gian quét",
"LabelSettingsHideSingleBookSeries": "Ẩn loạt sách đơn lẻ",
"LabelSettingsHideSingleBookSeriesHelp": "Các loạt sách chỉ có một cuốn sách sẽ được ẩn khỏi trang loạt sách và kệ trang chủ.",
"LabelSettingsHomePageBookshelfView": "Trang chủ sử dụng chế độ xem kệ sách",
"LabelSettingsLibraryBookshelfView": "Thư viện sử dụng chế độ xem kệ sách",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Phân tích phụ đề",
"LabelSettingsParseSubtitlesHelp": "Trích xuất phụ đề từ tên thư mục sách nói.<br>Phụ đề phải được tách bằng \" - \"<br>i.e. \"Book Title - A Subtitle Here\" có phụ đề \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Ưu tiên siêu dữ liệu phù hợp",
"LabelSettingsPreferMatchedMetadataHelp": "Dữ liệu phù hợp sẽ ghi đè lên chi tiết mục khi sử dụng Kết hợp Nhanh. Theo mặc định, Kết hợp Nhanh chỉ điền vào các chi tiết bị thiếu.",
"LabelSettingsSkipMatchingBooksWithASIN": "Bỏ qua sách khớp có ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Bỏ qua sách khớp có ISBN",
"LabelSettingsSortingIgnorePrefixes": "Bỏ qua tiền tố khi sắp xếp",
"LabelSettingsSortingIgnorePrefixesHelp": "ví dụ. với tiền tố \"the\" tiêu đề sách \"The Book Title\" sẽ được sắp xếp như \"Book Title, The\"",
"LabelSettingsSquareBookCovers": "Sử dụng ảnh bìa vuông",
"LabelSettingsSquareBookCoversHelp": "Ưu tiên sử dụng ảnh bìa vuông hơn ảnh bìa tiêu chuẩn 1.6:1",
"LabelSettingsStoreCoversWithItem": "Lưu trữ ảnh bìa với mục",
"LabelSettingsStoreCoversWithItemHelp": "Theo mặc định, ảnh bìa được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ ảnh bìa trong thư mục mục của thư viện bạn. Chỉ một tệp có tên là \"cover\" sẽ được giữ lại",
"LabelSettingsStoreMetadataWithItem": "Lưu trữ siêu dữ liệu với mục",
"LabelSettingsStoreMetadataWithItemHelp": "Theo mặc định, các tệp siêu dữ liệu được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ các tệp siêu dữ liệu trong các thư mục mục của thư viện bạn",
"LabelSettingsTimeFormat": "Định dạng Thời gian",
"LabelShowAll": "Hiển thị Tất cả",
"LabelSize": "Kích thước",
"LabelSleepTimer": "Hẹn giờ tắt",
"LabelSlug": "Slug",
"LabelStart": "Bắt đầu",
"LabelStarted": "Đã bắt đầu",
"LabelStartedAt": "Bắt đầu vào",
"LabelStartTime": "Thời gian bắt đầu",
"LabelStatsAudioTracks": "Audio Tracks",
"LabelStatsAuthors": "Tác giả",
"LabelStatsBestDay": "Ngày tốt nhất",
"LabelStatsDailyAverage": "Trung bình hàng ngày",
"LabelStatsDays": "Ngày",
"LabelStatsDaysListened": "Ngày đã nghe",
"LabelStatsHours": "Giờ",
"LabelStatsInARow": "liên tiếp",
"LabelStatsItemsFinished": "Mục đã hoàn thành",
"LabelStatsItemsInLibrary": "Mục trong thư viện",
"LabelStatsMinutes": "phút",
"LabelStatsMinutesListening": "Phút Nghe",
"LabelStatsOverallDays": "Tổng số ngày",
"LabelStatsOverallHours": "Tổng số giờ",
"LabelStatsWeekListening": "Tuần nghe",
"LabelSubtitle": "Phụ đề",
"LabelSupportedFileTypes": "Loại tệp được hỗ trợ",
"LabelTag": "Thẻ",
"LabelTags": "Thẻ",
"LabelTagsAccessibleToUser": "Thẻ Có Thể Truy Cập Cho Người Dùng",
"LabelTagsNotAccessibleToUser": "Thẻ Không Thể Truy Cập Cho Người Dùng",
"LabelTasks": "Nhiệm vụ Đang chạy",
"LabelTextEditorBulletedList": "Danh sách có dấu đầu dòng",
"LabelTextEditorLink": "Liên kết",
"LabelTextEditorNumberedList": "Danh sách đánh số",
"LabelTextEditorUnlink": "Gỡ liên kết",
"LabelTheme": "Chủ đề",
"LabelThemeDark": "Tối",
"LabelThemeLight": "Sáng",
"LabelTimeBase": "Thời gian cơ bản",
"LabelTimeListened": "Thời gian đã nghe",
"LabelTimeListenedToday": "Thời gian đã nghe hôm nay",
"LabelTimeRemaining": "{0} còn lại",
"LabelTimeToShift": "Thời gian dời chuyển theo giây",
"LabelTitle": "Tiêu đề",
"LabelToolsEmbedMetadata": "Nhúng siêu dữ liệu",
"LabelToolsEmbedMetadataDescription": "Nhúng siêu dữ liệu vào tệp âm thanh bao gồm ảnh bìa và chương.",
"LabelToolsMakeM4b": "Tạo Tệp Audiobook M4B",
"LabelToolsMakeM4bDescription": "Tạo tệp audiobook .M4B với siêu dữ liệu nhúng, ảnh bìa và chương.",
"LabelToolsSplitM4b": "Chia M4B thành MP3",
"LabelToolsSplitM4bDescription": "Tạo MP3 từ M4B được chia theo chương với siêu dữ liệu nhúng, ảnh bìa và chương.",
"LabelTotalDuration": "Tổng thời lượng",
"LabelTotalTimeListened": "Tổng thời gian đã nghe",
"LabelTrackFromFilename": "Từ tên tệp",
"LabelTrackFromMetadata": "Từ siêu dữ liệu",
"LabelTracks": "Bài hát",
"LabelTracksMultiTrack": "Nhiều track",
"LabelTracksNone": "Không có track",
"LabelTracksSingleTrack": "Một track",
"LabelType": "Loại",
"LabelUnabridged": "Không rút gọn",
"LabelUndo": "Hoàn tác",
"LabelUnknown": "Không xác định",
"LabelUpdateCover": "Cập nhật ảnh bìa",
"LabelUpdateCoverHelp": "Cho phép ghi đè lên các ảnh bìa hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp",
"LabelUpdatedAt": "Cập nhật lúc",
"LabelUpdateDetails": "Cập nhật chi tiết",
"LabelUpdateDetailsHelp": "Cho phép ghi đè lên các chi tiết hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp",
"LabelUploaderDragAndDrop": "Kéo và thả tệp hoặc thư mục",
"LabelUploaderDropFiles": "Thả tệp",
"LabelUploaderItemFetchMetadataHelp": "Tự động lấy tiêu đề, tác giả và loạt",
"LabelUseChapterTrack": "Sử dụng track chương",
"LabelUseFullTrack": "Sử dụng toàn bộ track",
"LabelUser": "Người dùng",
"LabelUsername": "Tên người dùng",
"LabelValue": "Giá trị",
"LabelVersion": "Phiên bản",
"LabelViewBookmarks": "Xem các đánh dấu",
"LabelViewChapters": "Xem các chương",
"LabelViewQueue": "Xem hàng đợi phát",
"LabelVolume": "Âm lượng",
"LabelWeekdaysToRun": "Ngày trong tuần để chạy",
"LabelYearReviewHide": "Ẩn Năm trong Bài Đánh Giá",
"LabelYearReviewShow": "Xem Năm trong Bài Đánh Giá",
"LabelYourAudiobookDuration": "Thời lượng sách nói của bạn",
"LabelYourBookmarks": "Đánh dấu của bạn",
"LabelYourPlaylists": "Danh sách phát của bạn",
"LabelYourProgress": "Tiến trình của bạn",
"MessageAddToPlayerQueue": "Thêm vào hàng đợi phát",
"MessageAppriseDescription": "Để sử dụng tính năng này, bạn cần có một phiên bản của <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> đang chạy hoặc một api sẽ xử lý các yêu cầu tương tự. <br /> Địa chỉ URL của Apprise API nên là đường dẫn URL đầy đủ để gửi thông báo, ví dụ, nếu phiên bản API của bạn được phục vụ tại <code>http://192.168.1.1:8337</code> thì bạn sẽ đặt <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Bản sao bao gồm người dùng, tiến độ của người dùng, chi tiết mục thư viện, cài đặt máy chủ và hình ảnh được lưu trữ trong <code>/metadata/items</code> & <code>/metadata/authors</code>. Bản sao <strong>không</strong> bao gồm bất kỳ tệp nào được lưu trữ trong các thư mục thư viện của bạn.",
"MessageBatchQuickMatchDescription": "Quick Match sẽ cố gắng thêm các ảnh bìa và siêu dữ liệu bị thiếu cho các mục đã chọn. Bật các tùy chọn dưới đây để cho phép Quick Match ghi đè lên các ảnh bìa hiện có và / hoặc siêu dữ liệu.",
"MessageBookshelfNoCollections": "Bạn chưa tạo bất kỳ bộ sưu tập nào",
"MessageBookshelfNoResultsForFilter": "Không có Kết quả cho bộ lọc \"{0}: {1}\"",
"MessageBookshelfNoRSSFeeds": "Không có nguồn cung cấp RSS nào đang mở",
"MessageBookshelfNoSeries": "Bạn không có bộ sách",
"MessageChapterEndIsAfter": "Kết thúc chương sau khi kết thúc sách nói của bạn",
"MessageChapterErrorFirstNotZero": "Chương đầu tiên phải bắt đầu từ 0",
"MessageChapterErrorStartGteDuration": "Thời gian bắt đầu không hợp lệ phải nhỏ hơn thời lượng sách nói",
"MessageChapterErrorStartLtPrev": "Thời gian bắt đầu không hợp lệ phải lớn hơn hoặc bằng thời gian bắt đầu của chương trước",
"MessageChapterStartIsAfter": "Bắt đầu chương sau khi kết thúc sách nói của bạn",
"MessageCheckingCron": "Kiểm tra cron...",
"MessageConfirmCloseFeed": "Bạn có chắc chắn muốn đóng nguồn cung cấp này không?",
"MessageConfirmDeleteBackup": "Bạn có chắc chắn muốn xóa bản sao lưu cho {0} không?",
"MessageConfirmDeleteFile": "Điều này sẽ xóa tệp khỏi hệ thống tệp của bạn. Bạn có chắc chắn không?",
"MessageConfirmDeleteLibrary": "Bạn có chắc chắn muốn xóa vĩnh viễn thư viện \"{0}\" không?",
"MessageConfirmDeleteLibraryItem": "Điều này sẽ xóa mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?",
"MessageConfirmDeleteLibraryItems": "Điều này sẽ xóa {0} mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?",
"MessageConfirmDeleteSession": "Bạn có chắc chắn muốn xóa phiên này không?",
"MessageConfirmForceReScan": "Bạn có chắc chắn muốn buộc quét lại không?",
"MessageConfirmMarkAllEpisodesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim đã kết thúc không?",
"MessageConfirmMarkAllEpisodesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim chưa kết thúc không?",
"MessageConfirmMarkSeriesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này đã kết thúc không?",
"MessageConfirmMarkSeriesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này chưa kết thúc không?",
"MessageConfirmQuickEmbed": "Cảnh báo! Quick embed sẽ không sao lưu các tệp âm thanh của bạn. Đảm bảo bạn có một bản sao lưu của các tệp âm thanh của bạn. <br><br>Bạn có muốn tiếp tục không?",
"MessageConfirmRemoveAllChapters": "Bạn có chắc chắn muốn xóa tất cả các chương không?",
"MessageConfirmRemoveAuthor": "Bạn có chắc chắn muốn xóa tác giả \"{0}\" không?",
"MessageConfirmRemoveCollection": "Bạn có chắc chắn muốn xóa bộ sưu tập \"{0}\" không?",
"MessageConfirmRemoveEpisode": "Bạn có chắc chắn muốn xóa tập phim \"{0}\" không?",
"MessageConfirmRemoveEpisodes": "Bạn có chắc chắn muốn xóa {0} tập phim không?",
"MessageConfirmRemoveListeningSessions": "Bạn có chắc chắn muốn xóa {0} phiên nghe không?",
"MessageConfirmRemoveNarrator": "Bạn có chắc chắn muốn xóa người kể chuyện \"{0}\" không?",
"MessageConfirmRemovePlaylist": "Bạn có chắc chắn muốn xóa danh sách phát của bạn \"{0}\" không?",
"MessageConfirmRenameGenre": "Bạn có chắc chắn muốn đổi tên thể loại \"{0}\" thành \"{1}\" cho tất cả các mục không?",
"MessageConfirmRenameGenreMergeNote": "Lưu ý: Thể loại này đã tồn tại nên chúng sẽ được hợp nhất.",
"MessageConfirmRenameGenreWarning": "Cảnh báo! Một thể loại tương tự với kiểu chữ khác đã tồn tại \"{0}\".",
"MessageConfirmRenameTag": "Bạn có chắc chắn muốn đổi tên tag \"{0}\" thành \"{1}\" cho tất cả các mục không?",
"MessageConfirmRenameTagMergeNote": "Lưu ý: Thẻ này đã tồn tại nên chúng sẽ được hợp nhất.",
"MessageConfirmRenameTagWarning": "Cảnh báo! Một thẻ tương tự với kiểu chữ khác đã tồn tại \"{0}\".",
"MessageConfirmReScanLibraryItems": "Bạn có chắc chắn muốn quét lại {0} mục không?",
"MessageConfirmSendEbookToDevice": "Bạn có chắc chắn muốn gửi {0} ebook \"{1}\" đến thiết bị \"{2}\" không?",
"MessageDownloadingEpisode": "Đang tải tập phim",
"MessageDragFilesIntoTrackOrder": "Kéo tệp vào thứ tự track đúng",
"MessageEmbedFinished": "Nhúng Hoàn thành!",
"MessageEpisodesQueuedForDownload": "{0} Tập(s) đã được thêm vào hàng đợi để tải xuống",
"MessageFeedURLWillBe": "URL nguồn cấp sẽ là {0}",
"MessageFetching": "Đang tìm...",
"MessageForceReScanDescription": "sẽ quét lại tất cả các tệp như một quét mới. Các thẻ ID3 của tệp âm thanh, tệp OPF và tệp văn bản sẽ được quét làm mới.",
"MessageImportantNotice": "Thông báo quan trọng!",
"MessageInsertChapterBelow": "Chèn chương dưới đây",
"MessageItemsSelected": "{0} Mục Đã Chọn",
"MessageItemsUpdated": "{0} Mục Đã Cập Nhật",
"MessageJoinUsOn": "Tham gia cùng chúng tôi trên",
"MessageListeningSessionsInTheLastYear": "{0} phiên nghe trong năm qua",
"MessageLoading": "Đang tải...",
"MessageLoadingFolders": "Đang tải các thư mục...",
"MessageM4BFailed": "M4B thất bại!",
"MessageM4BFinished": "M4B Hoàn thành!",
"MessageMapChapterTitles": "Ánh xạ tiêu đề chương với các chương hiện có của sách audio của bạn mà không điều chỉnh thời gian",
"MessageMarkAllEpisodesFinished": "Đánh dấu tất cả các tập phim đã kết thúc",
"MessageMarkAllEpisodesNotFinished": "Đánh dấu tất cả các tập phim chưa kết thúc",
"MessageMarkAsFinished": "Đánh dấu là Đã Kết Thúc",
"MessageMarkAsNotFinished": "Đánh dấu là Chưa Kết Thúc",
"MessageMatchBooksDescription": "sẽ cố gắng kết hợp các sách trong thư viện với một cuốn sách từ nhà cung cấp tìm kiếm được chọn và điền vào các chi tiết trống và ảnh bìa. Không ghi đè các chi tiết.",
"MessageNoAudioTracks": "Không có track âm thanh",
"MessageNoAuthors": "Không có Tác giả",
"MessageNoBackups": "Không có Bản sao lưu",
"MessageNoBookmarks": "Không có Đánh dấu",
"MessageNoChapters": "Không có Chương",
"MessageNoCollections": "Không có Bộ sưu tập",
"MessageNoCoversFound": "Không tìm thấy Ảnh bìa",
"MessageNoDescription": "Không có mô tả",
"MessageNoDownloadsInProgress": "Không có tải xuống đang tiến hành",
"MessageNoDownloadsQueued": "Không có tải xuống được xếp hàng",
"MessageNoEpisodeMatchesFound": "Không tìm thấy tập phim nào phù hợp",
"MessageNoEpisodes": "Không có Tập phim",
"MessageNoFoldersAvailable": "Không có Thư mục nào có sẵn",
"MessageNoGenres": "Không có Thể loại",
"MessageNoIssues": "Không có Vấn đề",
"MessageNoItems": "Không có Mục",
"MessageNoItemsFound": "Không tìm thấy mục nào",
"MessageNoListeningSessions": "Không có Phiên Nghe",
"MessageNoLogs": "Không có Log",
"MessageNoMediaProgress": "Không có Tiến độ Phương tiện",
"MessageNoNotifications": "Không có Thông báo",
"MessageNoPodcastsFound": "Không tìm thấy podcast nào",
"MessageNoResults": "Không có Kết quả",
"MessageNoSearchResultsFor": "Không có kết quả tìm kiếm cho \"{0}\"",
"MessageNoSeries": "Không có Bộ",
"MessageNoTags": "Không có Thẻ",
"MessageNoTasksRunning": "Không có Công việc đang chạy",
"MessageNotYetImplemented": "Chưa được triển khai",
"MessageNoUpdateNecessary": "Không cần cập nhật",
"MessageNoUpdatesWereNecessary": "Không cần cập nhật",
"MessageNoUserPlaylists": "Bạn chưa có danh sách phát",
"MessageOr": "hoặc",
"MessagePauseChapter": "Tạm dừng phát chương",
"MessagePlayChapter": "Nghe từ đầu chương",
"MessagePlaylistCreateFromCollection": "Tạo danh sách phát từ bộ sưu tập",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast không có RSS feed để sử dụng cho việc kết hợp",
"MessageQuickMatchDescription": "Điền chi tiết mục trống và ảnh bìa với kết quả phù hợp đầu tiên từ '{0}'. Không ghi đè chi tiết trừ khi cài đặt máy chủ 'Ưu tiên dữ liệu phù hợp' được bật.",
"MessageRemoveChapter": "Xóa chương",
"MessageRemoveEpisodes": "Xóa {0} tập",
"MessageRemoveFromPlayerQueue": "Xóa khỏi hàng đợi phát",
"MessageRemoveUserWarning": "Bạn có chắc chắn muốn xóa người dùng \"{0}\" một cách vĩnh viễn không?",
"MessageReportBugsAndContribute": "Báo cáo lỗi, yêu cầu tính năng và đóng góp tại",
"MessageResetChaptersConfirm": "Bạn có chắc chắn muốn đặt lại các chương và hủy những thay đổi bạn đã thực hiện không?",
"MessageRestoreBackupConfirm": "Bạn có chắc chắn muốn khôi phục bản sao lưu được tạo vào",
"MessageRestoreBackupWarning": "Việc khôi phục bản sao lưu sẽ ghi đè lên toàn bộ cơ sở dữ liệu được đặt tại /config và ảnh bìa trong /metadata/items & /metadata/authors.<br /><br />Bản sao lưu không sửa đổi bất kỳ tệp nào trong các thư mục thư viện của bạn. Nếu bạn đã bật các cài đặt máy chủ để lưu ảnh bìa và dữ liệu phần mềm trong các thư mục thư viện của mình thì chúng sẽ không được sao lưu hoặc ghi đè.<br /><br />Tất cả các máy khách sử dụng máy chủ của bạn sẽ được làm mới tự động.",
"MessageSearchResultsFor": "Kết quả tìm kiếm cho",
"MessageSelected": "{0} đã được chọn",
"MessageServerCouldNotBeReached": "Không thể kết nối đến máy chủ",
"MessageSetChaptersFromTracksDescription": "Đặt chương sử dụng mỗi tệp âm thanh là một chương và tiêu đề chương là tên tệp âm thanh",
"MessageStartPlaybackAtTime": "Bắt đầu phát \"{0}\" tại thời điểm {1}?",
"MessageThinking": "Đang suy nghĩ...",
"MessageUploaderItemFailed": "Không thể tải lên",
"MessageUploaderItemSuccess": "Tải lên thành công!",
"MessageUploading": "Đang tải lên...",
"MessageValidCronExpression": "Biểu thức cron hợp lệ",
"MessageWatcherIsDisabledGlobally": "Watcher đã bị vô hiệu hóa toàn cầu trong cài đặt máy chủ",
"MessageXLibraryIsEmpty": "Thư viện {0} rỗng!",
"MessageYourAudiobookDurationIsLonger": "Thời lượng sách nói của bạn dài hơn so với thời lượng tìm thấy",
"MessageYourAudiobookDurationIsShorter": "Thời lượng sách nói của bạn ngắn hơn so với thời lượng tìm thấy",
"NoteChangeRootPassword": "Người dùng Root là người dùng duy nhất có thể có mật khẩu trống",
"NoteChapterEditorTimes": "Lưu ý: Thời gian bắt đầu của chương đầu tiên phải ở 0:00 và thời gian bắt đầu của chương cuối cùng không thể vượt quá thời lượng của sách nói này.",
"NoteFolderPicker": "Lưu ý: các thư mục đã được ánh xạ trước đó sẽ không được hiển thị",
"NoteRSSFeedPodcastAppsHttps": "Cảnh báo: Hầu hết các ứng dụng podcast sẽ yêu cầu URL của RSS feed sử dụng HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Cảnh báo: 1 hoặc nhiều tập của bạn không có Pub Date. Một số ứng dụng podcast yêu cầu điều này.",
"NoteUploaderFoldersWithMediaFiles": "Các thư mục có tệp phương tiện sẽ được xử lý như các mục thư viện riêng biệt.",
"NoteUploaderOnlyAudioFiles": "Nếu chỉ tải lên các tệp âm thanh thì mỗi tệp âm thanh sẽ được xử lý như một cuốn sách nói riêng biệt.",
"NoteUploaderUnsupportedFiles": "Các tệp không được hỗ trợ sẽ bị bỏ qua. Khi chọn hoặc thả một thư mục, các tệp khác không có trong thư mục mục sẽ bị bỏ qua.",
"PlaceholderNewCollection": "Tên bộ sưu tập mới",
"PlaceholderNewFolderPath": "Đường dẫn thư mục mới",
"PlaceholderNewPlaylist": "Tên danh sách phát mới",
"PlaceholderSearch": "Tìm kiếm..",
"PlaceholderSearchEpisode": "Tìm kiếm tập..",
"ToastAccountUpdateFailed": "Cập nhật tài khoản thất bại",
"ToastAccountUpdateSuccess": "Tài khoản đã được cập nhật",
"ToastAuthorImageRemoveFailed": "Không thể xóa ảnh tác giả",
"ToastAuthorImageRemoveSuccess": "Ảnh tác giả đã được xóa",
"ToastAuthorUpdateFailed": "Cập nhật tác giả thất bại",
"ToastAuthorUpdateMerged": "Tác giả đã được hợp nhất",
"ToastAuthorUpdateSuccess": "Cập nhật tác giả thành công",
"ToastAuthorUpdateSuccessNoImageFound": "Cập nhật tác giả thành công (không tìm thấy ảnh)",
"ToastBackupCreateFailed": "Tạo bản sao lưu thất bại",
"ToastBackupCreateSuccess": "Bản sao lưu được tạo",
"ToastBackupDeleteFailed": "Xóa bản sao lưu thất bại",
"ToastBackupDeleteSuccess": "Bản sao lưu đã được xóa",
"ToastBackupRestoreFailed": "Khôi phục bản sao lưu thất bại",
"ToastBackupUploadFailed": "Tải lên bản sao lưu thất bại",
"ToastBackupUploadSuccess": "Bản sao lưu đã được tải lên",
"ToastBatchUpdateFailed": "Cập nhật nhóm thất bại",
"ToastBatchUpdateSuccess": "Cập nhật nhóm thành công",
"ToastBookmarkCreateFailed": "Tạo đánh dấu thất bại",
"ToastBookmarkCreateSuccess": "Đã thêm đánh dấu",
"ToastBookmarkRemoveFailed": "Xóa đánh dấu thất bại",
"ToastBookmarkRemoveSuccess": "Đánh dấu đã được xóa",
"ToastBookmarkUpdateFailed": "Cập nhật đánh dấu thất bại",
"ToastBookmarkUpdateSuccess": "Đánh dấu đã được cập nhật",
"ToastChaptersHaveErrors": "Các chương có lỗi",
"ToastChaptersMustHaveTitles": "Các chương phải có tiêu đề",
"ToastCollectionItemsRemoveFailed": "Xóa mục từ bộ sưu tập thất bại",
"ToastCollectionItemsRemoveSuccess": "Mục đã được xóa khỏi bộ sưu tập",
"ToastCollectionRemoveFailed": "Xóa bộ sưu tập thất bại",
"ToastCollectionRemoveSuccess": "Bộ sưu tập đã được xóa",
"ToastCollectionUpdateFailed": "Cập nhật bộ sưu tập thất bại",
"ToastCollectionUpdateSuccess": "Bộ sưu tập đã được cập nhật",
"ToastItemCoverUpdateFailed": "Cập nhật ảnh bìa mục thất bại",
"ToastItemCoverUpdateSuccess": "Ảnh bìa mục đã được cập nhật",
"ToastItemDetailsUpdateFailed": "Cập nhật chi tiết mục thất bại",
"ToastItemDetailsUpdateSuccess": "Chi tiết mục đã được cập nhật",
"ToastItemDetailsUpdateUnneeded": "Không cần cập nhật chi tiết mục",
"ToastItemMarkedAsFinishedFailed": "Đánh dấu mục là Hoàn thành thất bại",
"ToastItemMarkedAsFinishedSuccess": "Mục đã được đánh dấu là Hoàn thành",
"ToastItemMarkedAsNotFinishedFailed": "Đánh dấu mục là Chưa hoàn thành thất bại",
"ToastItemMarkedAsNotFinishedSuccess": "Mục đã được đánh dấu là Chưa hoàn thành",
"ToastLibraryCreateFailed": "Tạo thư viện thất bại",
"ToastLibraryCreateSuccess": "Thư viện \"{0}\" đã được tạo",
"ToastLibraryDeleteFailed": "Xóa thư viện thất bại",
"ToastLibraryDeleteSuccess": "Thư viện đã được xóa",
"ToastLibraryScanFailedToStart": "Không thể bắt đầu quét thư viện",
"ToastLibraryScanStarted": "Quét thư viện đã được bắt đầu",
"ToastLibraryUpdateFailed": "Cập nhật thư viện thất bại",
"ToastLibraryUpdateSuccess": "Thư viện \"{0}\" đã được cập nhật",
"ToastPlaylistCreateFailed": "Tạo danh sách phát thất bại",
"ToastPlaylistCreateSuccess": "Danh sách phát đã được tạo",
"ToastPlaylistRemoveFailed": "Xóa danh sách phát thất bại",
"ToastPlaylistRemoveSuccess": "Danh sách phát đã được xóa",
"ToastPlaylistUpdateFailed": "Cập nhật danh sách phát thất bại",
"ToastPlaylistUpdateSuccess": "Danh sách phát đã được cập nhật",
"ToastPodcastCreateFailed": "Tạo podcast thất bại",
"ToastPodcastCreateSuccess": "Podcast đã được tạo thành công",
"ToastRemoveItemFromCollectionFailed": "Xóa mục khỏi bộ sưu tập thất bại",
"ToastRemoveItemFromCollectionSuccess": "Mục đã được xóa khỏi bộ sưu tập",
"ToastRSSFeedCloseFailed": "Đóng nguồn cấp RSS thất bại",
"ToastRSSFeedCloseSuccess": "Nguồn cấp RSS đã được đóng",
"ToastSendEbookToDeviceFailed": "Gửi ebook đến thiết bị thất bại",
"ToastSendEbookToDeviceSuccess": "Ebook đã được gửi đến thiết bị \"{0}\"",
"ToastSeriesUpdateFailed": "Cập nhật loạt truyện thất bại",
"ToastSeriesUpdateSuccess": "Cập nhật loạt truyện thành công",
"ToastSessionDeleteFailed": "Xóa phiên thất bại",
"ToastSessionDeleteSuccess": "Phiên đã được xóa",
"ToastSocketConnected": "Kết nối socket",
"ToastSocketDisconnected": "Ngắt kết nối socket",
"ToastSocketFailedToConnect": "Không thể kết nối socket",
"ToastUserDeleteFailed": "Xóa người dùng thất bại",
"ToastUserDeleteSuccess": "Người dùng đã được xóa"
}

View File

@ -356,7 +356,9 @@
"LabelMetaTags": "元标签",
"LabelMinute": "分钟",
"LabelMissing": "丢失",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "丢失的部分",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "允许移动应用重定向 URI",
"LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.",
"LabelMore": "更多",
@ -464,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "只有一本书的系列将从系列页面和主页书架中隐藏.",
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
"LabelSettingsLibraryBookshelfView": "媒体库使用书架视图",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "解析副标题",
"LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"",
"LabelSettingsPreferMatchedMetadata": "首选匹配的元数据",

779
client/strings/zh-tw.json Normal file
View File

@ -0,0 +1,779 @@
{
"ButtonAdd": "增加",
"ButtonAddChapters": "新增章節",
"ButtonAddDevice": "新增設備",
"ButtonAddLibrary": "新增庫",
"ButtonAddPodcasts": "新增播客",
"ButtonAddUser": "新增使用者",
"ButtonAddYourFirstLibrary": "新增第一個媒體庫",
"ButtonApply": "應用",
"ButtonApplyChapters": "應用到章節",
"ButtonAuthors": "作者",
"ButtonBrowseForFolder": "瀏覽資料夾",
"ButtonCancel": "取消",
"ButtonCancelEncode": "取消編碼",
"ButtonChangeRootPassword": "更改 Root 密碼",
"ButtonCheckAndDownloadNewEpisodes": "檢查並下載新劇集",
"ButtonChooseAFolder": "選擇資料夾",
"ButtonChooseFiles": "選擇檔案",
"ButtonClearFilter": "清除過濾器",
"ButtonCloseFeed": "關閉源",
"ButtonCollections": "收藏",
"ButtonConfigureScanner": "配置掃描",
"ButtonCreate": "創建",
"ButtonCreateBackup": "創建備份",
"ButtonDelete": "刪除",
"ButtonDownloadQueue": "下載佇列",
"ButtonEdit": "編輯",
"ButtonEditChapters": "編輯章節",
"ButtonEditPodcast": "編輯播客",
"ButtonForceReScan": "強制重新掃描",
"ButtonFullPath": "完整路徑",
"ButtonHide": "隱藏",
"ButtonHome": "首頁",
"ButtonIssues": "問題",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "最新",
"ButtonLibrary": "媒體庫",
"ButtonLogout": "登出",
"ButtonLookup": "查找",
"ButtonManageTracks": "管理音軌",
"ButtonMapChapterTitles": "章節標題結構",
"ButtonMatchAllAuthors": "匹配所有作者",
"ButtonMatchBooks": "匹配圖書",
"ButtonNevermind": "沒關係",
"ButtonNext": "下個",
"ButtonNextChapter": "下個章節",
"ButtonOk": "確定",
"ButtonOpenFeed": "打開源",
"ButtonOpenManager": "打開管理器",
"ButtonPause": "暫停",
"ButtonPlay": "播放",
"ButtonPlaying": "正在播放",
"ButtonPlaylists": "播放列表",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "過去的章節",
"ButtonPurgeAllCache": "清理所有快取",
"ButtonPurgeItemsCache": "清理項目快取",
"ButtonPurgeMediaProgress": "清理媒體進度",
"ButtonQueueAddItem": "新增到佇列",
"ButtonQueueRemoveItem": "從佇列中移除",
"ButtonQuickMatch": "快速匹配",
"ButtonRead": "讀取",
"ButtonRefresh": "重整",
"ButtonRemove": "移除",
"ButtonRemoveAll": "移除所有",
"ButtonRemoveAllLibraryItems": "移除所有媒體庫項目",
"ButtonRemoveFromContinueListening": "從繼續收聽中刪除",
"ButtonRemoveFromContinueReading": "從繼續閱讀中刪除",
"ButtonRemoveSeriesFromContinueSeries": "從繼續收聽系列中刪除",
"ButtonReScan": "重新掃描",
"ButtonReset": "重置",
"ButtonResetToDefault": "重置為預設",
"ButtonRestore": "恢復",
"ButtonSave": "保存",
"ButtonSaveAndClose": "保存並關閉",
"ButtonSaveTracklist": "保存音軌列表",
"ButtonScan": "掃描",
"ButtonScanLibrary": "掃描庫",
"ButtonSearch": "查找",
"ButtonSelectFolderPath": "選擇資料夾路徑",
"ButtonSeries": "系列",
"ButtonSetChaptersFromTracks": "將音軌設定為章節",
"ButtonShare": "Share",
"ButtonShiftTimes": "快速調整時間",
"ButtonShow": "顯示",
"ButtonStartM4BEncode": "開始 M4B 編碼",
"ButtonStartMetadataEmbed": "開始嵌入元數據",
"ButtonSubmit": "提交",
"ButtonTest": "測試",
"ButtonUpload": "上傳",
"ButtonUploadBackup": "上傳備份",
"ButtonUploadCover": "上傳封面",
"ButtonUploadOPMLFile": "上傳 OPML 檔",
"ButtonUserDelete": "刪除使用者 {0}",
"ButtonUserEdit": "編輯使用者 {0}",
"ButtonViewAll": "查看全部",
"ButtonYes": "確定",
"ErrorUploadFetchMetadataAPI": "獲取元數據時出錯",
"ErrorUploadFetchMetadataNoResults": "無法獲取元數據 - 嘗試更新標題和/或作者",
"ErrorUploadLacksTitle": "必須有標題",
"HeaderAccount": "帳號",
"HeaderAdvanced": "高級",
"HeaderAppriseNotificationSettings": "測試通知設定",
"HeaderAudiobookTools": "有聲書檔案管理工具",
"HeaderAudioTracks": "音軌",
"HeaderAuthentication": "身份驗證",
"HeaderBackups": "備份",
"HeaderChangePassword": "更改密碼",
"HeaderChapters": "章節",
"HeaderChooseAFolder": "選擇資料夾",
"HeaderCollection": "收藏",
"HeaderCollectionItems": "收藏項目",
"HeaderCover": "封面",
"HeaderCurrentDownloads": "當前下載",
"HeaderCustomMetadataProviders": "自訂 Metadata 提供者",
"HeaderDetails": "詳情",
"HeaderDownloadQueue": "下載佇列",
"HeaderEbookFiles": "電子書檔",
"HeaderEmail": "郵箱",
"HeaderEmailSettings": "郵箱設定",
"HeaderEpisodes": "劇集",
"HeaderEreaderDevices": "Ereader 設備",
"HeaderEreaderSettings": "Ereader 設定",
"HeaderFiles": "檔案",
"HeaderFindChapters": "查找章節",
"HeaderIgnoredFiles": "忽略的檔案",
"HeaderItemFiles": "項目檔案",
"HeaderItemMetadataUtils": "項目元數據管理",
"HeaderLastListeningSession": "最後一次收聽會話",
"HeaderLatestEpisodes": "最新劇集",
"HeaderLibraries": "媒體庫",
"HeaderLibraryFiles": "媒體庫檔案",
"HeaderLibraryStats": "媒體庫統計數據",
"HeaderListeningSessions": "收聽會話",
"HeaderListeningStats": "收聽統計數據",
"HeaderLogin": "登入",
"HeaderLogs": "日誌",
"HeaderManageGenres": "管理流派",
"HeaderManageTags": "管理標籤",
"HeaderMapDetails": "編輯詳情",
"HeaderMatch": "匹配",
"HeaderMetadataOrderOfPrecedence": "元數據優先級",
"HeaderMetadataToEmbed": "嵌入元數據",
"HeaderNewAccount": "新建帳號",
"HeaderNewLibrary": "新建媒體庫",
"HeaderNotifications": "通知",
"HeaderOpenIDConnectAuthentication": "OpenID 連接身份驗證",
"HeaderOpenRSSFeed": "打開 RSS 源",
"HeaderOtherFiles": "其他檔案",
"HeaderPasswordAuthentication": "密碼認證",
"HeaderPermissions": "權限",
"HeaderPlayerQueue": "播放佇列",
"HeaderPlaylist": "播放列表",
"HeaderPlaylistItems": "播放列表項目",
"HeaderPodcastsToAdd": "要新增的播客",
"HeaderPreviewCover": "預覽封面",
"HeaderRemoveEpisode": "移除劇集",
"HeaderRemoveEpisodes": "移除 {0} 劇集",
"HeaderRSSFeedGeneral": "RSS 詳細信息",
"HeaderRSSFeedIsOpen": "RSS 源已打開",
"HeaderRSSFeeds": "RSS 訂閱",
"HeaderSavedMediaProgress": "保存媒體進度",
"HeaderSchedule": "計劃任務",
"HeaderScheduleLibraryScans": "自動掃描媒體庫",
"HeaderSession": "會話",
"HeaderSetBackupSchedule": "設定備份計劃任務",
"HeaderSettings": "設定",
"HeaderSettingsDisplay": "顯示",
"HeaderSettingsExperimental": "實驗功能",
"HeaderSettingsGeneral": "通用",
"HeaderSettingsScanner": "掃描",
"HeaderSleepTimer": "睡眠計時",
"HeaderStatsLargestItems": "最大的項目",
"HeaderStatsLongestItems": "項目時長(小時)",
"HeaderStatsMinutesListeningChart": "收聽分鐘數(最近7天)",
"HeaderStatsRecentSessions": "歷史會話",
"HeaderStatsTop10Authors": "前 10 位作者",
"HeaderStatsTop5Genres": "前 5 種流派",
"HeaderTableOfContents": "目錄",
"HeaderTools": "工具",
"HeaderUpdateAccount": "更新帳號",
"HeaderUpdateAuthor": "更新作者",
"HeaderUpdateDetails": "更新詳情",
"HeaderUpdateLibrary": "更新媒體庫",
"HeaderUsers": "使用者",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "你的統計數據",
"LabelAbridged": "概要",
"LabelAccountType": "帳號類型",
"LabelAccountTypeAdmin": "管理員",
"LabelAccountTypeGuest": "來賓",
"LabelAccountTypeUser": "使用者",
"LabelActivity": "活動",
"LabelAdded": "新增",
"LabelAddedAt": "新增於",
"LabelAddToCollection": "新增到收藏",
"LabelAddToCollectionBatch": "批量新增 {0} 個媒體到收藏",
"LabelAddToPlaylist": "新增到播放列表",
"LabelAddToPlaylistBatch": "新增 {0} 個項目到播放列表",
"LabelAdminUsersOnly": "僅限管理員使用者",
"LabelAll": "全部",
"LabelAllUsers": "所有使用者",
"LabelAllUsersExcludingGuests": "除訪客外的所有使用者",
"LabelAllUsersIncludingGuests": "包括訪客的所有使用者",
"LabelAlreadyInYourLibrary": "已存在你的庫中",
"LabelAppend": "附加",
"LabelAuthor": "作者",
"LabelAuthorFirstLast": "作者 (姓 名)",
"LabelAuthorLastFirst": "作者 (名, 姓)",
"LabelAuthors": "作者",
"LabelAutoDownloadEpisodes": "自動下載劇集",
"LabelAutoFetchMetadata": "自動獲取元數據",
"LabelAutoFetchMetadataHelp": "獲取標題, 作者和系列的元數據以簡化上傳. 上傳後可能需要匹配其他元數據.",
"LabelAutoLaunch": "自動啟動",
"LabelAutoLaunchDescription": "導航到登入頁面時自動重定向到身份驗證提供程序 (手動覆蓋路徑 <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "自動註冊",
"LabelAutoRegisterDescription": "登入後自動創建新使用者",
"LabelBackToUser": "返回到使用者",
"LabelBackupLocation": "備份位置",
"LabelBackupsEnableAutomaticBackups": "啟用自動備份",
"LabelBackupsEnableAutomaticBackupsHelp": "備份保存到 /metadata/backups",
"LabelBackupsMaxBackupSize": "最大備份大小 (GB)",
"LabelBackupsMaxBackupSizeHelp": "為了防止錯誤配置, 如果備份超過配置的大小, 備份將失敗.",
"LabelBackupsNumberToKeep": "要保留的備份個數",
"LabelBackupsNumberToKeepHelp": "一次只能刪除一個備份, 因此如果你已經有超過此數量的備份, 則應手動刪除它們.",
"LabelBitrate": "位元率",
"LabelBooks": "圖書",
"LabelButtonText": "按鈕文本",
"LabelChangePassword": "修改密碼",
"LabelChannels": "聲道",
"LabelChapters": "章節",
"LabelChaptersFound": "找到的章節",
"LabelChapterTitle": "章節標題",
"LabelClickForMoreInfo": "點擊了解更多資訊",
"LabelClosePlayer": "關閉播放器",
"LabelCodec": "編解碼",
"LabelCollapseSeries": "折疊系列",
"LabelCollection": "收藏",
"LabelCollections": "收藏",
"LabelComplete": "已完成",
"LabelConfirmPassword": "確認密碼",
"LabelContinueListening": "繼續收聽",
"LabelContinueReading": "繼續閱讀",
"LabelContinueSeries": "繼續收聽系列",
"LabelCover": "封面",
"LabelCoverImageURL": "封面圖像 URL",
"LabelCreatedAt": "創建時間",
"LabelCronExpression": "計劃任務表達式",
"LabelCurrent": "當前",
"LabelCurrently": "當前:",
"LabelCustomCronExpression": "自定義計劃任務表達式:",
"LabelDatetime": "日期時間",
"LabelDeleteFromFileSystemCheckbox": "從檔案系統刪除 (取消選中僅從資料庫中刪除)",
"LabelDescription": "描述",
"LabelDeselectAll": "全部取消選擇",
"LabelDevice": "設備",
"LabelDeviceInfo": "設備資訊",
"LabelDeviceIsAvailableTo": "設備可用於...",
"LabelDirectory": "目錄",
"LabelDiscFromFilename": "從檔名獲取光碟",
"LabelDiscFromMetadata": "從元數據獲取光碟",
"LabelDiscover": "發現",
"LabelDownload": "下載",
"LabelDownloadNEpisodes": "下載 {0} 集",
"LabelDuration": "持續時間",
"LabelDurationFound": "找到持續時間:",
"LabelEbook": "電子書",
"LabelEbooks": "電子書",
"LabelEdit": "編輯",
"LabelEmail": "郵箱",
"LabelEmailSettingsFromAddress": "發件人位址",
"LabelEmailSettingsSecure": "安全",
"LabelEmailSettingsSecureHelp": "如果選是, 則連接將在連接到伺服器時使用TLS. 如果選否, 則若伺服器支援STARTTLS擴展, 則使用TLS. 在大多數情況下, 如果連接到465埠, 請將該值設定為是. 對於587或25埠, 請保持為否. (來自nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "測試位址",
"LabelEmbeddedCover": "嵌入封面",
"LabelEnable": "啟用",
"LabelEnd": "結束",
"LabelEpisode": "劇集",
"LabelEpisodeTitle": "劇集標題",
"LabelEpisodeType": "劇集類型",
"LabelExample": "示例",
"LabelExplicit": "信息準確",
"LabelFeedURL": "源 URL",
"LabelFetchingMetadata": "正在獲取元數據",
"LabelFile": "檔案",
"LabelFileBirthtime": "檔案創建時間",
"LabelFileModified": "檔案修改時間",
"LabelFilename": "檔名",
"LabelFilterByUser": "按使用者篩選",
"LabelFindEpisodes": "查找劇集",
"LabelFinished": "已聽完",
"LabelFolder": "資料夾",
"LabelFolders": "資料夾",
"LabelFontBold": "Bold",
"LabelFontFamily": "字體系列",
"LabelFontItalic": "斜體",
"LabelFontScale": "字體比例",
"LabelFontStrikethrough": "刪除線",
"LabelFormat": "編碼格式",
"LabelGenre": "流派",
"LabelGenres": "流派",
"LabelHardDeleteFile": "完全刪除檔案",
"LabelHasEbook": "有電子書",
"LabelHasSupplementaryEbook": "有補充電子書",
"LabelHighestPriority": "最高優先級",
"LabelHost": "主機",
"LabelHour": "小時",
"LabelIcon": "圖標",
"LabelImageURLFromTheWeb": "來自 Web 圖像的 URL",
"LabelIncludeInTracklist": "包含在音軌列表中",
"LabelIncomplete": "未聽完",
"LabelInProgress": "正在聽",
"LabelInterval": "間隔",
"LabelIntervalCustomDailyWeekly": "自定義 每天 / 每周",
"LabelIntervalEvery12Hours": "每 12 小時",
"LabelIntervalEvery15Minutes": "每 15 分鐘",
"LabelIntervalEvery2Hours": "每 2 小時",
"LabelIntervalEvery30Minutes": "每 30 分鐘",
"LabelIntervalEvery6Hours": "每 6 小時",
"LabelIntervalEveryDay": "每天",
"LabelIntervalEveryHour": "每小時",
"LabelInvalidParts": "無效部件",
"LabelInvert": "倒轉",
"LabelItem": "項目",
"LabelLanguage": "語言",
"LabelLanguageDefaultServer": "預設伺服器語言",
"LabelLastBookAdded": "最後新增的書",
"LabelLastBookUpdated": "最後更新的書",
"LabelLastSeen": "上次查看時間",
"LabelLastTime": "最近一次",
"LabelLastUpdate": "最近更新",
"LabelLayout": "布局",
"LabelLayoutSinglePage": "單頁",
"LabelLayoutSplitPage": "分頁",
"LabelLess": "較少",
"LabelLibrariesAccessibleToUser": "使用者可存取的媒體庫",
"LabelLibrary": "媒體庫",
"LabelLibraryItem": "媒體庫項目",
"LabelLibraryName": "媒體庫名稱",
"LabelLimit": "限制",
"LabelLineSpacing": "行間距",
"LabelListenAgain": "再次收聽",
"LabelLogLevelDebug": "調試",
"LabelLogLevelInfo": "信息",
"LabelLogLevelWarn": "警告",
"LabelLookForNewEpisodesAfterDate": "在此日期後查找新劇集",
"LabelLowestPriority": "最低優先級",
"LabelMatchExistingUsersBy": "匹配現有使用者",
"LabelMatchExistingUsersByDescription": "用於連接現有使用者. 連接後, 使用者將通過SSO提供商提供的唯一 id 進行匹配",
"LabelMediaPlayer": "媒體播放器",
"LabelMediaType": "媒體類型",
"LabelMetadataOrderOfPrecedenceDescription": "較高優先級的元數據源將覆蓋較低優先級的元數據源",
"LabelMetadataProvider": "元數據提供者",
"LabelMetaTag": "元數據標籤",
"LabelMetaTags": "元標籤",
"LabelMinute": "分鐘",
"LabelMissing": "丟失",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "丟失的部分",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "允許移動應用重定向 URI",
"LabelMobileRedirectURIsDescription": "這是移動應用程序的有效重定向 URI 白名單. 預設值為 <code>audiobookshelf://oauth</code>,您可以刪除它或加入其他 URI 以進行第三方應用集成. 使用星號 (<code>*</code>) 作為唯一條目允許任何 URI.",
"LabelMore": "更多",
"LabelMoreInfo": "更多..",
"LabelName": "名稱",
"LabelNarrator": "講述者",
"LabelNarrators": "講述者",
"LabelNew": "新建",
"LabelNewestAuthors": "最新作者",
"LabelNewestEpisodes": "最新劇集",
"LabelNewPassword": "新密碼",
"LabelNextBackupDate": "下次備份日期",
"LabelNextScheduledRun": "下次任務運行",
"LabelNoEpisodesSelected": "未選擇任何劇集",
"LabelNotes": "注釋",
"LabelNotFinished": "未聽完",
"LabelNotificationAppriseURL": "通知 URL(s)",
"LabelNotificationAvailableVariables": "可用變量",
"LabelNotificationBodyTemplate": "正文模板",
"LabelNotificationEvent": "通知事件",
"LabelNotificationsMaxFailedAttempts": "最大失敗嘗試次數",
"LabelNotificationsMaxFailedAttemptsHelp": "如果多次發送失敗,通知將被禁用",
"LabelNotificationsMaxQueueSize": "通知事件的最大佇列大小",
"LabelNotificationsMaxQueueSizeHelp": "通知事件被限制為每秒觸發 1 個. 如果佇列處於最大大小, 則將忽略事件. 這可以防止通知垃圾郵件.",
"LabelNotificationTitleTemplate": "標題模板",
"LabelNotStarted": "未開始",
"LabelNumberOfBooks": "圖書數量",
"LabelNumberOfEpisodes": "# 集",
"LabelOpenRSSFeed": "打開 RSS 源",
"LabelOverwrite": "覆蓋",
"LabelPassword": "密碼",
"LabelPath": "路徑",
"LabelPermissionsAccessAllLibraries": "可以存取所有媒體庫",
"LabelPermissionsAccessAllTags": "可以存取所有標籤",
"LabelPermissionsAccessExplicitContent": "可以存取顯式內容",
"LabelPermissionsDelete": "可以刪除",
"LabelPermissionsDownload": "可以下載",
"LabelPermissionsUpdate": "可以更新",
"LabelPermissionsUpload": "可以上傳",
"LabelPersonalYearReview": "你的年度回顧 ({0})",
"LabelPhotoPathURL": "圖片路徑或 URL",
"LabelPlaylists": "播放列表",
"LabelPlayMethod": "播放方法",
"LabelPodcast": "播客",
"LabelPodcasts": "播客",
"LabelPodcastSearchRegion": "播客搜尋地區",
"LabelPodcastType": "播客類型",
"LabelPort": "埠",
"LabelPrefixesToIgnore": "忽略的前綴 (不區分大小寫)",
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目錄對你的源進行索引",
"LabelPrimaryEbook": "主電子書",
"LabelProgress": "進度",
"LabelProvider": "供應商",
"LabelPubDate": "出版日期",
"LabelPublisher": "出版商",
"LabelPublishYear": "發布年份",
"LabelRead": "閱讀",
"LabelReadAgain": "再次閱讀",
"LabelReadEbookWithoutProgress": "閱讀電子書而不保存進度",
"LabelRecentlyAdded": "最近新增",
"LabelRecentSeries": "最近新增系列",
"LabelRecommended": "推薦內容",
"LabelRedo": "重做",
"LabelRegion": "區域",
"LabelReleaseDate": "發布日期",
"LabelRemoveCover": "移除封面",
"LabelRowsPerPage": "每頁行數",
"LabelRSSFeedCustomOwnerEmail": "自定義所有者電子郵件",
"LabelRSSFeedCustomOwnerName": "自定義所有者名稱",
"LabelRSSFeedOpen": "打開 RSS 源",
"LabelRSSFeedPreventIndexing": "防止索引",
"LabelRSSFeedSlug": "RSS 源段",
"LabelRSSFeedURL": "RSS 源 URL",
"LabelSearchTerm": "搜尋項",
"LabelSearchTitle": "搜尋標題",
"LabelSearchTitleOrASIN": "搜尋標題或 ASIN",
"LabelSeason": "季",
"LabelSelectAllEpisodes": "選擇所有劇集",
"LabelSelectEpisodesShowing": "選擇正在播放的 {0} 劇集",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "發送電子書到...",
"LabelSequence": "序列",
"LabelSeries": "系列",
"LabelSeriesName": "系列名稱",
"LabelSeriesProgress": "系列進度",
"LabelServerYearReview": "伺服器年度回顧 ({0})",
"LabelSetEbookAsPrimary": "設定為主",
"LabelSetEbookAsSupplementary": "設定為補充",
"LabelSettingsAudiobooksOnly": "僅有聲書",
"LabelSettingsAudiobooksOnlyHelp": "啟用此設定將忽略電子書檔, 除非它們位於有聲書資料夾中, 在這種情況下, 它們將被設定為補充電子書",
"LabelSettingsBookshelfViewHelp": "帶有木架子的擬物化設計",
"LabelSettingsChromecastSupport": "Chromecast 支援",
"LabelSettingsDateFormat": "日期格式",
"LabelSettingsDisableWatcher": "禁用監視程序",
"LabelSettingsDisableWatcherForLibrary": "禁用媒體庫的資料夾監視程序",
"LabelSettingsDisableWatcherHelp": "檢測到檔案更改時禁用自動新增和更新項目. *需要重啟伺服器",
"LabelSettingsEnableWatcher": "啟用監視程序",
"LabelSettingsEnableWatcherForLibrary": "為庫啟用資料夾監視程序",
"LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器",
"LabelSettingsExperimentalFeatures": "實驗功能",
"LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.",
"LabelSettingsFindCovers": "查找封面",
"LabelSettingsFindCoversHelp": "如果你的有聲書在資料夾中沒有嵌入封面或封面圖像, 掃描將嘗試查找封面.<br>注意: 這將延長掃描時間",
"LabelSettingsHideSingleBookSeries": "隱藏單書系列",
"LabelSettingsHideSingleBookSeriesHelp": "只有一本書的系列將從系列頁面和主頁書架中隱藏.",
"LabelSettingsHomePageBookshelfView": "首頁使用書架視圖",
"LabelSettingsLibraryBookshelfView": "媒體庫使用書架視圖",
"LabelSettingsParseSubtitles": "解析副標題",
"LabelSettingsParseSubtitlesHelp": "從有聲書資料夾中提取副標題.<br>副標題必須用 \" - \" 分隔.<br>例: \"書名 - 這裡是副標題\" 則顯示副標題 \"這裡是副標題\"",
"LabelSettingsPreferMatchedMetadata": "首選匹配的元數據",
"LabelSettingsPreferMatchedMetadataHelp": "使用快速匹配時, 匹配的數據將覆蓋項目詳細信息. 預設情況下, 快速匹配將只填充缺少的詳細信息.",
"LabelSettingsSkipMatchingBooksWithASIN": "跳過匹配已有 ASIN 的圖書",
"LabelSettingsSkipMatchingBooksWithISBN": "跳過匹配已有 ISBN 的圖書",
"LabelSettingsSortingIgnorePrefixes": "排序時忽略前綴",
"LabelSettingsSortingIgnorePrefixesHelp": "例如: 前綴為 \"The\" 的圖書標題 \"The Book Title\" 將按 \"Book Title, The\" 進行排序",
"LabelSettingsSquareBookCovers": "使用者方形圖書封面",
"LabelSettingsSquareBookCoversHelp": "比起標準的 1.6:1 圖書封面,更喜歡使用方形封面",
"LabelSettingsStoreCoversWithItem": "存儲項目封面",
"LabelSettingsStoreCoversWithItemHelp": "預設情況下封面存儲在/metadata/items資料夾中, 啟用此設定將存儲封面在你媒體項目資料夾中. 只保留一個名為 \"cover\" 的檔案",
"LabelSettingsStoreMetadataWithItem": "存儲項目元數據",
"LabelSettingsStoreMetadataWithItemHelp": "預設情況下元數據檔案存儲在/metadata/items資料夾中, 啟用此設定將存儲元數據在你媒體項目資料夾中",
"LabelSettingsTimeFormat": "時間格式",
"LabelShowAll": "全部顯示",
"LabelSize": "檔案大小",
"LabelSleepTimer": "睡眠定時",
"LabelSlug": "Slug",
"LabelStart": "開始",
"LabelStarted": "開始於",
"LabelStartedAt": "從這開始",
"LabelStartTime": "開始時間",
"LabelStatsAudioTracks": "音軌",
"LabelStatsAuthors": "作者",
"LabelStatsBestDay": "最好的一天",
"LabelStatsDailyAverage": "每日平均值",
"LabelStatsDays": "天",
"LabelStatsDaysListened": "收聽天數",
"LabelStatsHours": "小時",
"LabelStatsInARow": "在一行",
"LabelStatsItemsFinished": "已完成的項目",
"LabelStatsItemsInLibrary": "媒體庫中的項目",
"LabelStatsMinutes": "分鐘",
"LabelStatsMinutesListening": "收聽分鐘數",
"LabelStatsOverallDays": "總計天數",
"LabelStatsOverallHours": "總計小時",
"LabelStatsWeekListening": "每周收聽",
"LabelSubtitle": "副標題",
"LabelSupportedFileTypes": "支援的檔案類型",
"LabelTag": "標籤",
"LabelTags": "標籤",
"LabelTagsAccessibleToUser": "使用者可存取的標籤",
"LabelTagsNotAccessibleToUser": "使用者無法存取標籤",
"LabelTasks": "正在運行的任務",
"LabelTextEditorBulletedList": "項目符號列表",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "編號列表",
"LabelTextEditorUnlink": "取消連結",
"LabelTheme": "主題",
"LabelThemeDark": "黑暗",
"LabelThemeLight": "明亮",
"LabelTimeBase": "時間基準",
"LabelTimeListened": "收聽時間",
"LabelTimeListenedToday": "今日收聽的時間",
"LabelTimeRemaining": "剩餘 {0}",
"LabelTimeToShift": "快速調整時間以秒為單位",
"LabelTitle": "標題",
"LabelToolsEmbedMetadata": "嵌入元數據",
"LabelToolsEmbedMetadataDescription": "將元數據嵌入音頻檔案, 包括封面圖像和章節.",
"LabelToolsMakeM4b": "制作 M4B 有聲書檔案",
"LabelToolsMakeM4bDescription": "生成帶有嵌入元數據, 封面圖像和章節的 .M4B 有聲書檔.",
"LabelToolsSplitM4b": "將 M4B 檔拆分為 MP3 檔",
"LabelToolsSplitM4bDescription": "從 M4B 檔創建 MP3 檔, 按章節分割, 並嵌入元數據, 封面圖像和章節.",
"LabelTotalDuration": "總持續時間",
"LabelTotalTimeListened": "總收聽時間",
"LabelTrackFromFilename": "從檔案名獲取音軌",
"LabelTrackFromMetadata": "從源數據獲取音軌",
"LabelTracks": "音軌",
"LabelTracksMultiTrack": "多軌",
"LabelTracksNone": "沒有音軌",
"LabelTracksSingleTrack": "單軌",
"LabelType": "類型",
"LabelUnabridged": "未刪節",
"LabelUndo": "Undo",
"LabelUnknown": "未知",
"LabelUpdateCover": "更新封面",
"LabelUpdateCoverHelp": "找到匹配項時允許覆蓋所選書籍存在的封面",
"LabelUpdatedAt": "更新時間",
"LabelUpdateDetails": "更新詳細信息",
"LabelUpdateDetailsHelp": "找到匹配項時允許覆蓋所選書籍存在的詳細信息",
"LabelUploaderDragAndDrop": "拖放檔案或資料夾",
"LabelUploaderDropFiles": "刪除檔案",
"LabelUploaderItemFetchMetadataHelp": "自動獲取標題, 作者和系列",
"LabelUseChapterTrack": "使用章節音軌",
"LabelUseFullTrack": "使用完整音軌",
"LabelUser": "使用者",
"LabelUsername": "使用者名",
"LabelValue": "值",
"LabelVersion": "版本",
"LabelViewBookmarks": "查看書籤",
"LabelViewChapters": "查看章節",
"LabelViewQueue": "查看播放列表",
"LabelVolume": "音量",
"LabelWeekdaysToRun": "工作日運行",
"LabelYearReviewHide": "隱藏年度回顧",
"LabelYearReviewShow": "顯示年度回顧",
"LabelYourAudiobookDuration": "你的有聲書持續時間",
"LabelYourBookmarks": "你的書籤",
"LabelYourPlaylists": "你的播放列表",
"LabelYourProgress": "你的進度",
"MessageAddToPlayerQueue": "新增到播放佇列",
"MessageAppriseDescription": "要使用此功能,您需要運行一個 <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> 實例或一個可以處理這些相同請求的 API. <br />Apprise API Url 應該是發送通知的完整 URL 路徑, 例如: 如果你的 API 實例運行在 <code>http://192.168.1.1:8337</code>, 那么你可以輸入 <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "備份包括使用者, 使用者進度, 媒體庫項目詳細信息, 伺服器設定和圖像, 存儲在 <code>/metadata/items</code> & <code>/metadata/authors</code>. 備份不包括存儲在您的媒體庫資料夾中的任何檔案.",
"MessageBatchQuickMatchDescription": "快速匹配將嘗試為所選項目新增缺少的封面和元數據. 啟用以下選項以允許快速匹配覆蓋現有封面和或元數據.",
"MessageBookshelfNoCollections": "你尚未進行任何收藏",
"MessageBookshelfNoResultsForFilter": "過濾器無結果 \"{0}: {1}\"",
"MessageBookshelfNoRSSFeeds": "沒有打開的 RSS 源",
"MessageBookshelfNoSeries": "你沒有系列",
"MessageChapterEndIsAfter": "章節結束是在有聲書結束之後",
"MessageChapterErrorFirstNotZero": "第一章節必須從 0 開始",
"MessageChapterErrorStartGteDuration": "無效的開始時間, 必須小於有聲書持續時間",
"MessageChapterErrorStartLtPrev": "無效的開始時間, 必須大於或等於上一章節的開始時間",
"MessageChapterStartIsAfter": "章節開始是在有聲書結束之後",
"MessageCheckingCron": "檢查計劃任務...",
"MessageConfirmCloseFeed": "你確定要關閉此訂閱源嗎?",
"MessageConfirmDeleteBackup": "你確定要刪除備份 {0}?",
"MessageConfirmDeleteFile": "這將從檔案系統中刪除該檔案. 你確定嗎?",
"MessageConfirmDeleteLibrary": "你確定要永久刪除媒體庫 \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "這將從資料庫和檔案系統中刪除庫項目. 你確定嗎?",
"MessageConfirmDeleteLibraryItems": "這將從資料庫和檔案系統中刪除 {0} 個庫項目. 你確定嗎?",
"MessageConfirmDeleteSession": "你確定要刪除此會話嗎?",
"MessageConfirmForceReScan": "你確定要強制重新掃描嗎?",
"MessageConfirmMarkAllEpisodesFinished": "你確定要將所有劇集都標記為已完成嗎?",
"MessageConfirmMarkAllEpisodesNotFinished": "你確定要將所有劇集都標記為未完成嗎?",
"MessageConfirmMarkSeriesFinished": "你確定要將此系列中的所有書籍都標記為已聽完嗎?",
"MessageConfirmMarkSeriesNotFinished": "你確定要將此系列中的所有書籍都標記為未聽完嗎?",
"MessageConfirmQuickEmbed": "警告! 快速嵌入不會備份你的音頻檔案. 確保你有音頻檔案的備份. <br><br>你是否想繼續嗎?",
"MessageConfirmRemoveAllChapters": "你確定要移除所有章節嗎?",
"MessageConfirmRemoveAuthor": "你確定要刪除作者 \"{0}\"?",
"MessageConfirmRemoveCollection": "你確定要移除收藏 \"{0}\"?",
"MessageConfirmRemoveEpisode": "你確定要移除劇集 \"{0}\"?",
"MessageConfirmRemoveEpisodes": "你確定要移除 {0} 劇集?",
"MessageConfirmRemoveListeningSessions": "你確定要移除 {0} 收聽會話嗎?",
"MessageConfirmRemoveNarrator": "你確定要刪除演播者 \"{0}\"?",
"MessageConfirmRemovePlaylist": "你確定要移除播放列表 \"{0}\"?",
"MessageConfirmRenameGenre": "你確定要將所有項目流派 \"{0}\" 重命名到 \"{1}\"?",
"MessageConfirmRenameGenreMergeNote": "注意: 該流派已經存在, 因此它們將被合併.",
"MessageConfirmRenameGenreWarning": "警告! 已經存在有大小寫不同的類似流派 \"{0}\".",
"MessageConfirmRenameTag": "你確定要將所有項目標籤 \"{0}\" 重命名到 \"{1}\"?",
"MessageConfirmRenameTagMergeNote": "注意: 該標籤已經存在, 因此它們將被合併.",
"MessageConfirmRenameTagWarning": "警告! 已經存在有大小寫不同的類似標籤 \"{0}\".",
"MessageConfirmReScanLibraryItems": "你確定要重新掃描 {0} 個項目嗎?",
"MessageConfirmSendEbookToDevice": "你確定要發送 {0} 電子書 \"{1}\" 到設備 \"{2}\"?",
"MessageDownloadingEpisode": "正在下載劇集",
"MessageDragFilesIntoTrackOrder": "將檔案拖動到正確的音軌順序",
"MessageEmbedFinished": "嵌入完成!",
"MessageEpisodesQueuedForDownload": "{0} 個劇集排隊等待下載",
"MessageFeedURLWillBe": "源 URL 將改為 {0}",
"MessageFetching": "正在獲取...",
"MessageForceReScanDescription": "將像重新掃描一樣再次掃描所有檔案. 音頻檔 ID3 標籤, OPF 檔和文本檔將被掃描為新檔案.",
"MessageImportantNotice": "重要通知!",
"MessageInsertChapterBelow": "在下面插入章節",
"MessageItemsSelected": "已選定 {0} 個項目",
"MessageItemsUpdated": "已更新 {0} 個項目",
"MessageJoinUsOn": "加入我們",
"MessageListeningSessionsInTheLastYear": "去年收聽 {0} 個會話",
"MessageLoading": "讀取...",
"MessageLoadingFolders": "讀取資料夾...",
"MessageM4BFailed": "M4B 失敗!",
"MessageM4BFinished": "M4B 完成!",
"MessageMapChapterTitles": "將章節標題映射到現有的有聲書章節, 無需調整時間戳",
"MessageMarkAllEpisodesFinished": "標記所有劇集為已完成",
"MessageMarkAllEpisodesNotFinished": "標記所有劇集為未完成",
"MessageMarkAsFinished": "標記為已聽完",
"MessageMarkAsNotFinished": "標記為未聽完",
"MessageMatchBooksDescription": "嘗試將媒體庫中的圖書與所選搜尋提供商的圖書進行匹配, 並填寫空白的詳細信息和封面. 不覆蓋詳細信息.",
"MessageNoAudioTracks": "沒有音軌",
"MessageNoAuthors": "沒有作者",
"MessageNoBackups": "沒有備份",
"MessageNoBookmarks": "沒有書籤",
"MessageNoChapters": "沒有章節",
"MessageNoCollections": "沒有收藏",
"MessageNoCoversFound": "沒有找到封面",
"MessageNoDescription": "沒有描述",
"MessageNoDownloadsInProgress": "當前沒有正在進行的下載",
"MessageNoDownloadsQueued": "下載佇列無任務",
"MessageNoEpisodeMatchesFound": "沒有找到任何劇集匹配項",
"MessageNoEpisodes": "沒有劇集",
"MessageNoFoldersAvailable": "沒有可用資料夾",
"MessageNoGenres": "無流派",
"MessageNoIssues": "無問題",
"MessageNoItems": "無項目",
"MessageNoItemsFound": "未找到任何項目",
"MessageNoListeningSessions": "無收聽會話",
"MessageNoLogs": "無日誌",
"MessageNoMediaProgress": "無媒體進度",
"MessageNoNotifications": "無通知",
"MessageNoPodcastsFound": "未找到播客",
"MessageNoResults": "無結果",
"MessageNoSearchResultsFor": "沒有搜尋到結果 \"{0}\"",
"MessageNoSeries": "無系列",
"MessageNoTags": "無標籤",
"MessageNoTasksRunning": "沒有正在運行的任務",
"MessageNotYetImplemented": "尚未實施",
"MessageNoUpdateNecessary": "無需更新",
"MessageNoUpdatesWereNecessary": "無需更新",
"MessageNoUserPlaylists": "你沒有播放列表",
"MessageOr": "或",
"MessagePauseChapter": "暫停章節播放",
"MessagePlayChapter": "開始章節播放",
"MessagePlaylistCreateFromCollection": "從收藏中創建播放列表",
"MessagePodcastHasNoRSSFeedForMatching": "播客沒有可用於匹配 RSS 源的 url",
"MessageQuickMatchDescription": "使用來自 '{0}' 的第一個匹配結果填充空白詳細信息和封面. 除非啟用 '首選匹配元數據' 伺服器設定, 否則不會覆蓋詳細信息.",
"MessageRemoveChapter": "移除章節",
"MessageRemoveEpisodes": "移除 {0} 劇集",
"MessageRemoveFromPlayerQueue": "從播放佇列中移除",
"MessageRemoveUserWarning": "是否確實要永久刪除使用者 \"{0}\"?",
"MessageReportBugsAndContribute": "報告錯誤、請求功能和貢獻在",
"MessageResetChaptersConfirm": "你確定要重置章節並撤消你所做的更改嗎?",
"MessageRestoreBackupConfirm": "你確定要恢復創建的這個備份",
"MessageRestoreBackupWarning": "恢復備份將覆蓋位於 /config 的整個資料庫並覆蓋 /metadata/items & /metadata/authors 中的圖像.<br /><br />備份不會修改媒體庫資料夾中的任何檔案. 如果您已啟用伺服器設定將封面和元數據存儲在庫資料夾中,則不會備份或覆蓋這些內容.<br /><br />將自動刷新使用伺服器的所有客戶端.",
"MessageSearchResultsFor": "搜尋結果",
"MessageSelected": "{0} 被選取",
"MessageServerCouldNotBeReached": "無法連接伺服器",
"MessageSetChaptersFromTracksDescription": "把每個音頻檔設定為章節並將章節標題設定為音頻檔名",
"MessageStartPlaybackAtTime": "開始播放 \"{0}\" 在 {1}?",
"MessageThinking": "正在查找...",
"MessageUploaderItemFailed": "上傳失敗",
"MessageUploaderItemSuccess": "上傳成功!",
"MessageUploading": "正在上傳...",
"MessageValidCronExpression": "有效的計劃任務表達式",
"MessageWatcherIsDisabledGlobally": "在伺服器設定中禁用全域監視程序",
"MessageXLibraryIsEmpty": "{0} 庫為空!",
"MessageYourAudiobookDurationIsLonger": "您的有聲書持續時間比找到的持續時間長",
"MessageYourAudiobookDurationIsShorter": "您的有聲書持續時間比找到的持續時間短",
"NoteChangeRootPassword": "Root 是唯一可以擁有空密碼的使用者",
"NoteChapterEditorTimes": "注意: 第一章開始時間必須保持在 0:00, 最後一章開始時間不能超過有聲書持續時間.",
"NoteFolderPicker": "注意: 將不顯示已映射的資料夾",
"NoteRSSFeedPodcastAppsHttps": "警告: 大多數播客應用程序都需要 RSS 源 URL 使用 HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "警告: 您的一集或多集沒有發布日期. 一些播客應用程序要求這樣做.",
"NoteUploaderFoldersWithMediaFiles": "包含媒體檔案的資料夾將作為單獨的媒體庫項目處理.",
"NoteUploaderOnlyAudioFiles": "如果只上傳音頻檔, 則每個音頻檔將作為單獨的有聲書處理.",
"NoteUploaderUnsupportedFiles": "不支援的檔案將被忽略. 選擇或刪除資料夾時, 將忽略不在項目資料夾中的其他檔案.",
"PlaceholderNewCollection": "輸入收藏夾名稱",
"PlaceholderNewFolderPath": "輸入資料夾路徑",
"PlaceholderNewPlaylist": "輸入播放列表名稱",
"PlaceholderSearch": "查找..",
"PlaceholderSearchEpisode": "搜尋劇集..",
"ToastAccountUpdateFailed": "帳號更新失敗",
"ToastAccountUpdateSuccess": "帳號已更新",
"ToastAuthorImageRemoveFailed": "作者圖像刪除失敗",
"ToastAuthorImageRemoveSuccess": "作者圖像已刪除",
"ToastAuthorUpdateFailed": "作者更新失敗",
"ToastAuthorUpdateMerged": "作者已合併",
"ToastAuthorUpdateSuccess": "作者已更新",
"ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到圖像)",
"ToastBackupCreateFailed": "備份創建失敗",
"ToastBackupCreateSuccess": "備份已創建",
"ToastBackupDeleteFailed": "備份刪除失敗",
"ToastBackupDeleteSuccess": "備份已刪除",
"ToastBackupRestoreFailed": "備份還原失敗",
"ToastBackupUploadFailed": "上傳備份失敗",
"ToastBackupUploadSuccess": "備份已上傳",
"ToastBatchUpdateFailed": "批量更新失敗",
"ToastBatchUpdateSuccess": "批量更新成功",
"ToastBookmarkCreateFailed": "創建書籤失敗",
"ToastBookmarkCreateSuccess": "書籤已新增",
"ToastBookmarkRemoveFailed": "書籤刪除失敗",
"ToastBookmarkRemoveSuccess": "書籤已刪除",
"ToastBookmarkUpdateFailed": "書籤更新失敗",
"ToastBookmarkUpdateSuccess": "書籤已更新",
"ToastChaptersHaveErrors": "章節有錯誤",
"ToastChaptersMustHaveTitles": "章節必須有標題",
"ToastCollectionItemsRemoveFailed": "從收藏夾移除項目失敗",
"ToastCollectionItemsRemoveSuccess": "項目從收藏夾移除",
"ToastCollectionRemoveFailed": "刪除收藏夾失敗",
"ToastCollectionRemoveSuccess": "收藏夾已刪除",
"ToastCollectionUpdateFailed": "更新收藏夾失敗",
"ToastCollectionUpdateSuccess": "收藏夾已更新",
"ToastItemCoverUpdateFailed": "更新項目封面失敗",
"ToastItemCoverUpdateSuccess": "項目封面已更新",
"ToastItemDetailsUpdateFailed": "更新項目詳細信息失敗",
"ToastItemDetailsUpdateSuccess": "項目詳細信息已更新",
"ToastItemDetailsUpdateUnneeded": "項目詳細信息無需更新",
"ToastItemMarkedAsFinishedFailed": "標記為聽完失敗",
"ToastItemMarkedAsFinishedSuccess": "標記為聽完的項目",
"ToastItemMarkedAsNotFinishedFailed": "標記為未聽完失敗",
"ToastItemMarkedAsNotFinishedSuccess": "標記為未聽完的項目",
"ToastLibraryCreateFailed": "創建媒體庫失敗",
"ToastLibraryCreateSuccess": "媒體庫 \"{0}\" 創建成功",
"ToastLibraryDeleteFailed": "刪除媒體庫失敗",
"ToastLibraryDeleteSuccess": "媒體庫已刪除",
"ToastLibraryScanFailedToStart": "無法啟動掃描",
"ToastLibraryScanStarted": "媒體庫掃描已啟動",
"ToastLibraryUpdateFailed": "更新圖書庫失敗",
"ToastLibraryUpdateSuccess": "媒體庫 \"{0}\" 已更新",
"ToastPlaylistCreateFailed": "創建播放列表失敗",
"ToastPlaylistCreateSuccess": "已成功創建播放列表",
"ToastPlaylistRemoveFailed": "刪除播放列表失敗",
"ToastPlaylistRemoveSuccess": "播放列表已刪除",
"ToastPlaylistUpdateFailed": "更新播放列表失敗",
"ToastPlaylistUpdateSuccess": "播放列表已更新",
"ToastPodcastCreateFailed": "創建播客失敗",
"ToastPodcastCreateSuccess": "已成功創建播客",
"ToastRemoveItemFromCollectionFailed": "從收藏中刪除項目失敗",
"ToastRemoveItemFromCollectionSuccess": "項目已從收藏中刪除",
"ToastRSSFeedCloseFailed": "關閉 RSS 源失敗",
"ToastRSSFeedCloseSuccess": "RSS 源已關閉",
"ToastSendEbookToDeviceFailed": "發送電子書到設備失敗",
"ToastSendEbookToDeviceSuccess": "電子書已經發送到設備 \"{0}\"",
"ToastSeriesUpdateFailed": "更新系列失敗",
"ToastSeriesUpdateSuccess": "系列已更新",
"ToastSessionDeleteFailed": "刪除會話失敗",
"ToastSessionDeleteSuccess": "會話已刪除",
"ToastSocketConnected": "網路已連接",
"ToastSocketDisconnected": "網路已斷開",
"ToastSocketFailedToConnect": "網路連接失敗",
"ToastUserDeleteFailed": "刪除使用者失敗",
"ToastUserDeleteSuccess": "使用者已刪除"
}

View File

@ -10,6 +10,7 @@ if (isDev) {
if (devEnv.MetadataPath) process.env.METADATA_PATH = devEnv.MetadataPath
if (devEnv.FFmpegPath) process.env.FFMPEG_PATH = devEnv.FFmpegPath
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
process.env.SOURCE = 'local'
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
}
@ -18,12 +19,10 @@ const PORT = process.env.PORT || 80
const HOST = process.env.HOST
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
const UID = process.env.AUDIOBOOKSHELF_UID
const GID = process.env.AUDIOBOOKSHELF_GID
const SOURCE = process.env.SOURCE || 'docker'
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
console.log('Config', CONFIG_PATH, METADATA_PATH)
const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
Server.start()

View File

@ -23,13 +23,11 @@ const PORT = options.port || process.env.PORT || 3333
const HOST = options.host || process.env.HOST
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
const UID = process.env.AUDIOBOOKSHELF_UID
const GID = process.env.AUDIOBOOKSHELF_GID
const SOURCE = options.source || process.env.SOURCE || 'debian'
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
Server.start()

View File

@ -429,7 +429,7 @@ class Auth {
// Depending on the error, it can also have a body
// We also log the request header the passport plugin sents for the URL
const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED')
Logger.debug(header + '\n' + response.body?.toString())
Logger.debug(header + '\n' + response.body?.toString() + '\n' + JSON.stringify(response.body, null, 2))
}
if (isMobile) {
@ -533,42 +533,45 @@ class Auth {
res.clearCookie('auth_method')
let logoutUrl = null
if (authMethod === 'openid' || authMethod === 'openid-mobile') {
// If we are using openid, we need to redirect to the logout endpoint
// node-openid-client does not support doing it over passport
const oidcStrategy = passport._strategy('openid-client')
const client = oidcStrategy._client
let postLogoutRedirectUri = null
if (client.issuer.end_session_endpoint && client.issuer.end_session_endpoint.length > 0) {
let postLogoutRedirectUri = null
if (authMethod === 'openid') {
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
const host = req.get('host')
// TODO: ABS does currently not support subfolders for installation
// If we want to support it we need to include a config for the serverurl
postLogoutRedirectUri = `${protocol}://${host}/login`
if (authMethod === 'openid') {
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
const host = req.get('host')
// TODO: ABS does currently not support subfolders for installation
// If we want to support it we need to include a config for the serverurl
postLogoutRedirectUri = `${protocol}://${host}/login`
}
// else for openid-mobile we keep postLogoutRedirectUri on null
// nice would be to redirect to the app here, but for example Authentik does not implement
// the post_logout_redirect_uri parameter at all and for other providers
// we would also need again to implement (and even before get to know somehow for 3rd party apps)
// the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect).
// Instead because its null (and this way the parameter will be omitted completly), the client/app can simply append something like
// &post_logout_redirect_uri=audiobookshelf://login to the received logout url by itself which is the simplest solution
// (The URL needs to be whitelisted in the config of the SSO/ID provider)
logoutUrl = client.endSessionUrl({
id_token_hint: req.cookies.openid_id_token,
post_logout_redirect_uri: postLogoutRedirectUri
})
}
// else for openid-mobile we keep postLogoutRedirectUri on null
// nice would be to redirect to the app here, but for example Authentik does not implement
// the post_logout_redirect_uri parameter at all and for other providers
// we would also need again to implement (and even before get to know somehow for 3rd party apps)
// the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect).
// Instead because its null (and this way the parameter will be omitted completly), the client/app can simply append something like
// &post_logout_redirect_uri=audiobookshelf://login to the received logout url by itself which is the simplest solution
// (The URL needs to be whitelisted in the config of the SSO/ID provider)
const logoutUrl = client.endSessionUrl({
id_token_hint: req.cookies.openid_id_token,
post_logout_redirect_uri: postLogoutRedirectUri
})
res.clearCookie('openid_id_token')
// Tell the user agent (browser) to redirect to the authentification provider's logout URL
res.send({ redirect_url: logoutUrl })
} else {
res.sendStatus(200)
}
// Tell the user agent (browser) to redirect to the authentification provider's logout URL
// (or redirect_url: null if we don't have one)
res.send({ redirect_url: logoutUrl })
}
})
})

View File

@ -689,6 +689,34 @@ class Database {
return this.libraryFilterData[libraryId].series.some(se => se.id === seriesId)
}
/**
* Get author id for library by name. Uses library filter data if available
*
* @param {string} libraryId
* @param {string} authorName
* @returns {Promise<string>} author id or null if not found
*/
async getAuthorIdByName(libraryId, authorName) {
if (!this.libraryFilterData[libraryId]) {
return (await this.authorModel.getOldByNameAndLibrary(authorName, libraryId))?.id || null
}
return this.libraryFilterData[libraryId].authors.find(au => au.name === authorName)?.id || null
}
/**
* Get series id for library by name. Uses library filter data if available
*
* @param {string} libraryId
* @param {string} seriesName
* @returns {Promise<string>} series id or null if not found
*/
async getSeriesIdByName(libraryId, seriesName) {
if (!this.libraryFilterData[libraryId]) {
return (await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId))?.id || null
}
return this.libraryFilterData[libraryId].series.find(se => se.name === seriesName)?.id || null
}
/**
* Reset numIssues for library
* @param {string} libraryId

View File

@ -70,14 +70,15 @@ class Logger {
}
/**
*
* @param {number} level
* @param {string[]} args
*
* @param {number} level
* @param {string[]} args
* @param {string} src
*/
async handleLog(level, args) {
async handleLog(level, args, src) {
const logObj = {
timestamp: this.timestamp,
source: this.source,
source: src,
message: args.join(' '),
levelName: this.getLogLevelString(level),
level
@ -104,31 +105,31 @@ class Logger {
trace(...args) {
if (this.logLevel > LogLevel.TRACE) return
console.trace(`[${this.timestamp}] TRACE:`, ...args)
this.handleLog(LogLevel.TRACE, args)
this.handleLog(LogLevel.TRACE, args, this.source)
}
debug(...args) {
if (this.logLevel > LogLevel.DEBUG) return
console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`)
this.handleLog(LogLevel.DEBUG, args)
this.handleLog(LogLevel.DEBUG, args, this.source)
}
info(...args) {
if (this.logLevel > LogLevel.INFO) return
console.info(`[${this.timestamp}] INFO:`, ...args)
this.handleLog(LogLevel.INFO, args)
this.handleLog(LogLevel.INFO, args, this.source)
}
warn(...args) {
if (this.logLevel > LogLevel.WARN) return
console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`)
this.handleLog(LogLevel.WARN, args)
this.handleLog(LogLevel.WARN, args, this.source)
}
error(...args) {
if (this.logLevel > LogLevel.ERROR) return
console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`)
this.handleLog(LogLevel.ERROR, args)
this.handleLog(LogLevel.ERROR, args, this.source)
}
/**
@ -139,12 +140,12 @@ class Logger {
*/
fatal(...args) {
console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`)
return this.handleLog(LogLevel.FATAL, args)
return this.handleLog(LogLevel.FATAL, args, this.source)
}
note(...args) {
console.log(`[${this.timestamp}] NOTE:`, ...args)
this.handleLog(LogLevel.NOTE, args)
this.handleLog(LogLevel.NOTE, args, this.source)
}
}
module.exports = new Logger()

View File

@ -41,13 +41,11 @@ const passport = require('passport')
const expressSession = require('express-session')
class Server {
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
this.Port = PORT
this.Host = HOST
global.Source = SOURCE
global.isWin = process.platform === 'win32'
global.Uid = isNaN(UID) ? undefined : Number(UID)
global.Gid = isNaN(GID) ? undefined : Number(GID)
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
global.RouterBasePath = ROUTER_BASE_PATH

View File

@ -660,6 +660,7 @@ class LibraryController {
for (const author of authors) {
const oldAuthor = author.getOldAuthor().toJSON()
oldAuthor.numBooks = author.books.length
oldAuthor.lastFirst = author.lastFirst
oldAuthors.push(oldAuthor)
}

View File

@ -283,6 +283,9 @@ class LibraryItemController {
return res.sendStatus(404)
}
if (req.query.ts)
res.set('Cache-Control', 'private, max-age=86400')
if (raw) { // any value
if (global.XAccel) {
const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath)

View File

@ -152,11 +152,12 @@ class BookFinder {
/**
*
* @param {string} title
* @param {string} author
* @param {string} author
* @param {string} isbn
* @param {string} providerSlug
* @returns {Promise<Object[]>}
*/
async getCustomProviderResults(title, author, providerSlug) {
async getCustomProviderResults(title, author, isbn, providerSlug) {
const books = await this.customProviderAdapter.search(title, author, providerSlug, 'book')
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
@ -333,7 +334,7 @@ class BookFinder {
// Custom providers are assumed to be correct
if (provider.startsWith('custom-')) {
return this.getCustomProviderResults(title, author, provider)
return this.getCustomProviderResults(title, author, isbn, provider)
}
if (!title)

View File

@ -11,8 +11,8 @@ const fileUtils = require('../utils/fileUtils')
class BinaryManager {
defaultRequiredBinaries = [
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1', '6'] },
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1', '6'] }
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1'] },
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1'] }
]
constructor(requiredBinaries = this.defaultRequiredBinaries) {
@ -24,7 +24,14 @@ class BinaryManager {
}
async init() {
// Optional skip binaries check
if (process.env.SKIP_BINARIES_CHECK === '1') {
Logger.info('[BinaryManager] Skipping check for binaries')
return
}
if (this.initialized) return
const missingBinaries = await this.findRequiredBinaries()
if (missingBinaries.length == 0) return
await this.removeOldBinaries(missingBinaries)
@ -135,7 +142,7 @@ class BinaryManager {
if (!binaries.length) return
Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`)
let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath
await ffbinaries.downloadBinaries(binaries, { destination, version: '6.1', force: true })
await ffbinaries.downloadBinaries(binaries, { destination, version: '5.1', force: true })
Logger.info(`[BinaryManager] Binaries installed to ${destination}`)
}

View File

@ -11,6 +11,7 @@ const oldLibrary = require('../objects/Library')
* @property {string} autoScanCronExpression
* @property {boolean} audiobooksOnly
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
* @property {boolean} onlyShowLaterBooksInContinueSeries Skip showing books that are earlier than the max sequence read
* @property {string[]} metadataPrecedence
*/

View File

@ -8,7 +8,8 @@ class LibrarySettings {
this.skipMatchingMediaWithIsbn = false
this.autoScanCronExpression = null
this.audiobooksOnly = false
this.hideSingleBookSeries = false // Do not show series that only have 1 book
this.hideSingleBookSeries = false // Do not show series that only have 1 book
this.onlyShowLaterBooksInContinueSeries = false // Skip showing books that are earlier than the max sequence read
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
this.podcastSearchRegion = 'us'
@ -25,6 +26,7 @@ class LibrarySettings {
this.autoScanCronExpression = settings.autoScanCronExpression || null
this.audiobooksOnly = !!settings.audiobooksOnly
this.hideSingleBookSeries = !!settings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!settings.onlyShowLaterBooksInContinueSeries
if (settings.metadataPrecedence) {
this.metadataPrecedence = [...settings.metadataPrecedence]
} else {
@ -43,6 +45,7 @@ class LibrarySettings {
autoScanCronExpression: this.autoScanCronExpression,
audiobooksOnly: this.audiobooksOnly,
hideSingleBookSeries: this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: this.onlyShowLaterBooksInContinueSeries,
metadataPrecedence: [...this.metadataPrecedence],
podcastSearchRegion: this.podcastSearchRegion
}

View File

@ -29,10 +29,9 @@ class Audible {
*/
cleanSeriesSequence(seriesName, sequence) {
if (!sequence) return ''
let updatedSequence = sequence.replace(/Book /, '').trim()
if (updatedSequence.includes(' ')) {
updatedSequence = updatedSequence.split(' ').shift().replace(/,$/, '')
}
// match any number with optional decimal (e.g, 1 or 1.5 or .5)
let numberFound = sequence.match(/\.\d+|\d+(?:\.\d+)?/)
let updatedSequence = numberFound ? numberFound[0] : sequence
if (sequence !== updatedSequence) {
Logger.debug(`[Audible] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`)
}

View File

@ -9,11 +9,12 @@ class CustomProviderAdapter {
*
* @param {string} title
* @param {string} author
* @param {string} isbn
* @param {string} providerSlug
* @param {string} mediaType
* @returns {Promise<Object[]>}
*/
async search(title, author, providerSlug, mediaType) {
async search(title, author, isbn, providerSlug, mediaType) {
const providerId = providerSlug.split('custom-')[1]
const provider = await Database.customMetadataProviderModel.findByPk(providerId)
@ -29,6 +30,9 @@ class CustomProviderAdapter {
if (author) {
queryObj.author = author
}
if (isbn) {
queryObj.isbn = isbn
}
const queryString = (new URLSearchParams(queryObj)).toString()
// Setup headers

View File

@ -186,11 +186,11 @@ class BookScanner {
// Check for authors added
for (const authorName of bookMetadata.authors) {
if (!media.authors.some(au => au.name === authorName)) {
const existingAuthor = Database.libraryFilterData[libraryItemData.libraryId].authors.find(au => au.name === authorName)
if (existingAuthor) {
const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
if (existingAuthorId) {
await Database.bookAuthorModel.create({
bookId: media.id,
authorId: existingAuthor.id
authorId: existingAuthorId
})
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added author "${authorName}"`)
authorsUpdated = true
@ -221,11 +221,11 @@ class BookScanner {
for (const seriesObj of bookMetadata.series) {
const existingBookSeries = media.series.find(se => se.name === seriesObj.name)
if (!existingBookSeries) {
const existingSeries = Database.libraryFilterData[libraryItemData.libraryId].series.find(se => se.name === seriesObj.name)
if (existingSeries) {
const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name)
if (existingSeriesId) {
await Database.bookSeriesModel.create({
bookId: media.id,
seriesId: existingSeries.id,
seriesId: existingSeriesId,
sequence: seriesObj.sequence
})
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added series "${seriesObj.name}"${seriesObj.sequence ? ` with sequence "${seriesObj.sequence}"` : ''}`)
@ -443,10 +443,10 @@ class BookScanner {
}
if (bookMetadata.authors.length) {
for (const authorName of bookMetadata.authors) {
const matchingAuthor = Database.libraryFilterData[libraryItemData.libraryId].authors.find(au => au.name === authorName)
if (matchingAuthor) {
const matchingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
if (matchingAuthorId) {
bookObject.bookAuthors.push({
authorId: matchingAuthor.id
authorId: matchingAuthorId
})
} else {
// New author
@ -463,10 +463,10 @@ class BookScanner {
if (bookMetadata.series.length) {
for (const seriesObj of bookMetadata.series) {
if (!seriesObj.name) continue
const matchingSeries = Database.libraryFilterData[libraryItemData.libraryId].series.find(se => se.name === seriesObj.name)
if (matchingSeries) {
const matchingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name)
if (matchingSeriesId) {
bookObject.bookSeries.push({
seriesId: matchingSeries.id,
seriesId: matchingSeriesId,
sequence: seriesObj.sequence
})
} else {

View File

@ -357,7 +357,10 @@ module.exports.removeFile = (path) => {
}
module.exports.encodeUriPath = (path) => {
const uri = new URL(path, "file://")
const uri = new URL('/', "file://")
// we assign the path here to assure that URL control characters like # are
// actually interpreted as part of the URL path
uri.pathname = path
return uri.pathname
}

View File

@ -75,7 +75,7 @@ module.exports = {
/**
* Get library items for most recently added shelf
* @param {oldLibrary} library
* @param {import('../../objects/Library')} library
* @param {oldUser} user
* @param {string[]} include
* @param {number} limit
@ -120,14 +120,14 @@ module.exports = {
/**
* Get library items for continue series shelf
* @param {string} library
* @param {import('../../objects/Library')} library
* @param {oldUser} user
* @param {string[]} include
* @param {number} limit
* @returns {object} { libraryItems:LibraryItem[], count:number }
*/
async getLibraryItemsContinueSeries(library, user, include, limit) {
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, user, include, limit, 0)
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library, user, include, limit, 0)
return {
libraryItems: libraryItems.map(li => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
@ -145,7 +145,7 @@ module.exports = {
/**
* Get library items or podcast episodes for the "Listen Again" and "Read Again" shelf
* @param {oldLibrary} library
* @param {import('../../objects/Library')} library
* @param {oldUser} user
* @param {string[]} include
* @param {number} limit

View File

@ -204,6 +204,10 @@ module.exports = {
mediaWhere['ebookFile'] = {
[Sequelize.Op.not]: null
}
} else if (value == 'no-ebook') {
mediaWhere['ebookFile'] = {
[Sequelize.Op.eq]: null
}
}
} else if (group === 'missing') {
if (['asin', 'isbn', 'subtitle', 'publishedYear', 'description', 'publisher', 'language', 'cover'].includes(value)) {
@ -421,6 +425,10 @@ module.exports = {
libraryItemWhere['libraryFiles'] = {
[Sequelize.Op.substring]: `"isSupplementary":true`
}
} else if (filterGroup === 'ebooks' && filterValue === 'no-supplementary') {
libraryItemWhere['libraryFiles'] = {
[Sequelize.Op.notLike]: Sequelize.literal(`\'%"isSupplementary":true%\'`),
}
} else if (filterGroup === 'missing' && filterValue === 'authors') {
authorInclude = {
model: Database.authorModel,
@ -625,14 +633,15 @@ module.exports = {
* 2. Has no books in progress
* 3. Has at least 1 unfinished book
* TODO: Reduce queries
* @param {string} libraryId
* @param {oldUser} user
* @param {import('../../objects/Library')} library
* @param {import('../../objects/user/User')} user
* @param {string[]} include
* @param {number} limit
* @param {number} offset
* @returns {object} { libraryItems:LibraryItem[], count:number }
* @returns {{ libraryItems:import('../../models/LibraryItem')[], count:number }}
*/
async getContinueSeriesLibraryItems(libraryId, user, include, limit, offset) {
async getContinueSeriesLibraryItems(library, user, include, limit, offset) {
const libraryId = library.id
const libraryItemIncludes = []
if (include.includes('rssfeed')) {
libraryItemIncludes.push({
@ -646,6 +655,13 @@ module.exports = {
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
bookWhere.push(...userPermissionBookWhere.bookWhere)
let includeAttributes = [
[Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'],
]
if (library.settings.onlyShowLaterBooksInContinueSeries) {
includeAttributes.push([Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence'])
}
const { rows: series, count } = await Database.seriesModel.findAndCountAll({
where: [
{
@ -667,9 +683,7 @@ module.exports = {
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM mediaProgresses mp, bookSeries bs WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id AND mp.isFinished = 0 AND mp.currentTime > 0)`), 0)
],
attributes: {
include: [
[Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress']
]
include: includeAttributes
},
replacements: {
userId: user.id,
@ -723,13 +737,26 @@ module.exports = {
const libraryItems = series.map(s => {
if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series
const libraryItem = s.bookSeries[0].book.libraryItem.toJSON()
const book = s.bookSeries[0].book.toJSON()
let bookIndex = 0
// if the library setting is toggled, only show later entries in series, otherwise skip
if (library.settings.onlyShowLaterBooksInContinueSeries) {
bookIndex = s.bookSeries.findIndex(function (b) {
return parseFloat(b.dataValues.sequence) > s.dataValues.maxSequence
})
if (bookIndex === -1) {
// no later books than maxSequence
return null
}
}
const libraryItem = s.bookSeries[bookIndex].book.libraryItem.toJSON()
const book = s.bookSeries[bookIndex].book.toJSON()
delete book.libraryItem
libraryItem.series = {
id: s.id,
name: s.name,
sequence: s.bookSeries[0].sequence
sequence: s.bookSeries[bookIndex].sequence
}
if (libraryItem.feeds?.length) {
libraryItem.rssFeed = libraryItem.feeds[0]

View File

@ -291,6 +291,7 @@ describe('isBinaryGood', () => {
const binaryPath = '/path/to/binary'
const execCommand = '"' + binaryPath + '"' + ' -version'
const goodVersions = ['5.1', '6']
beforeEach(() => {
binaryManager = new BinaryManager()
@ -306,7 +307,7 @@ describe('isBinaryGood', () => {
it('should return false if binaryPath is falsy', async () => {
fsPathExistsStub.resolves(true)
const result = await binaryManager.isBinaryGood(null)
const result = await binaryManager.isBinaryGood(null, goodVersions)
expect(result).to.be.false
expect(fsPathExistsStub.called).to.be.false
@ -316,7 +317,7 @@ describe('isBinaryGood', () => {
it('should return false if binaryPath does not exist', async () => {
fsPathExistsStub.resolves(false)
const result = await binaryManager.isBinaryGood(binaryPath)
const result = await binaryManager.isBinaryGood(binaryPath, goodVersions)
expect(result).to.be.false
expect(fsPathExistsStub.calledOnce).to.be.true
@ -328,7 +329,7 @@ describe('isBinaryGood', () => {
fsPathExistsStub.resolves(true)
execStub.rejects(new Error('Failed to execute command'))
const result = await binaryManager.isBinaryGood(binaryPath)
const result = await binaryManager.isBinaryGood(binaryPath, goodVersions)
expect(result).to.be.false
expect(fsPathExistsStub.calledOnce).to.be.true
@ -342,7 +343,7 @@ describe('isBinaryGood', () => {
fsPathExistsStub.resolves(true)
execStub.resolves({ stdout })
const result = await binaryManager.isBinaryGood(binaryPath)
const result = await binaryManager.isBinaryGood(binaryPath, goodVersions)
expect(result).to.be.false
expect(fsPathExistsStub.calledOnce).to.be.true
@ -356,7 +357,7 @@ describe('isBinaryGood', () => {
fsPathExistsStub.resolves(true)
execStub.resolves({ stdout })
const result = await binaryManager.isBinaryGood(binaryPath)
const result = await binaryManager.isBinaryGood(binaryPath, goodVersions)
expect(result).to.be.false
expect(fsPathExistsStub.calledOnce).to.be.true
@ -370,7 +371,7 @@ describe('isBinaryGood', () => {
fsPathExistsStub.resolves(true)
execStub.resolves({ stdout })
const result = await binaryManager.isBinaryGood(binaryPath)
const result = await binaryManager.isBinaryGood(binaryPath, goodVersions)
expect(result).to.be.true
expect(fsPathExistsStub.calledOnce).to.be.true

View File

@ -0,0 +1,48 @@
const Audible = require('../../../server/providers/Audible')
const { expect } = require('chai')
const sinon = require('sinon')
describe('Audible', () => {
let audible;
beforeEach(() => {
audible = new Audible();
});
describe('cleanSeriesSequence', () => {
it('should return an empty string if sequence is falsy', () => {
const result = audible.cleanSeriesSequence('Series Name', null)
expect(result).to.equal('')
})
it('should return the sequence as is if it does not contain a number', () => {
const result = audible.cleanSeriesSequence('Series Name', 'part a')
expect(result).to.equal('part a')
})
it('should return the sequence as is if contains just a number', () => {
const result = audible.cleanSeriesSequence('Series Name', '2')
expect(result).to.equal('2')
})
it('should return the sequence as is if contains just a number with decimals', () => {
const result = audible.cleanSeriesSequence('Series Name', '2.3')
expect(result).to.equal('2.3')
})
it('should extract and return the first number from the sequence', () => {
const result = audible.cleanSeriesSequence('Series Name', 'Book 1')
expect(result).to.equal('1')
})
it('should extract and return the number with decimals from the sequence', () => {
const result = audible.cleanSeriesSequence('Series Name', 'Book 1.5')
expect(result).to.equal('1.5')
})
it('should extract and return the number even if it has no leading zero', () => {
const result = audible.cleanSeriesSequence('Series Name', 'Book .5')
expect(result).to.equal('.5')
})
})
})