mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-10 15:18:17 +02:00
Merge branch 'master' into authorSort
This commit is contained in:
commit
e50b06183e
@ -50,9 +50,8 @@ echo "$controlfile" > dist/debian/DEBIAN/control;
|
|||||||
# Package debian
|
# Package debian
|
||||||
pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
|
pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
|
||||||
|
|
||||||
fakeroot dpkg-deb --build dist/debian
|
fakeroot dpkg-deb -Zxz --build dist/debian
|
||||||
|
|
||||||
mv dist/debian.deb "dist/$OUTPUT_FILE"
|
mv dist/debian.deb "dist/$OUTPUT_FILE"
|
||||||
chmod +x "dist/$OUTPUT_FILE"
|
|
||||||
|
|
||||||
echo "Finished! Filename: $OUTPUT_FILE"
|
echo "Finished! Filename: $OUTPUT_FILE"
|
||||||
|
@ -217,36 +217,6 @@ Bookshelf Label
|
|||||||
filter: blur(20px);
|
filter: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.episode-subtitle {
|
|
||||||
word-break: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
line-height: 16px;
|
|
||||||
/* fallback */
|
|
||||||
max-height: 32px;
|
|
||||||
/* fallback */
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
/* number of lines to show */
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.episode-subtitle-long {
|
|
||||||
word-break: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
line-height: 16px;
|
|
||||||
/* fallback */
|
|
||||||
max-height: 72px;
|
|
||||||
/* fallback */
|
|
||||||
-webkit-line-clamp: 6;
|
|
||||||
/* number of lines to show */
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
|
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
|
||||||
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
|
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
|
||||||
padding-top: 104px;
|
padding-top: 104px;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<div class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
<div class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
||||||
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||||
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<player-ui
|
<player-ui
|
||||||
@ -380,7 +380,7 @@ export default {
|
|||||||
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
|
||||||
if (!data.numSegments) return
|
if (!data.numSegments) return
|
||||||
var chunks = data.chunks
|
var chunks = data.chunks
|
||||||
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
console.log(`[MediaPlayerContainer] Stream Progress ${data.percent}`)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||||
} else {
|
} else {
|
||||||
@ -397,17 +397,17 @@ export default {
|
|||||||
this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate)
|
this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate)
|
||||||
},
|
},
|
||||||
streamOpen(session) {
|
streamOpen(session) {
|
||||||
console.log(`[StreamContainer] Stream session open`, session)
|
console.log(`[MediaPlayerContainer] Stream session open`, session)
|
||||||
},
|
},
|
||||||
streamClosed(streamId) {
|
streamClosed(streamId) {
|
||||||
// Stream was closed from the server
|
// Stream was closed from the server
|
||||||
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
||||||
console.warn('[StreamContainer] Closing stream due to request from server')
|
console.warn('[MediaPlayerContainer] Closing stream due to request from server')
|
||||||
this.playerHandler.closePlayer()
|
this.playerHandler.closePlayer()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamReady() {
|
streamReady() {
|
||||||
console.log(`[StreamContainer] Stream Ready`)
|
console.log(`[MediaPlayerContainer] Stream Ready`)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setStreamReady()
|
this.$refs.audioPlayer.setStreamReady()
|
||||||
} else {
|
} else {
|
||||||
@ -417,7 +417,7 @@ export default {
|
|||||||
streamError(streamId) {
|
streamError(streamId) {
|
||||||
// Stream had critical error from the server
|
// Stream had critical error from the server
|
||||||
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
||||||
console.warn('[StreamContainer] Closing stream due to stream error from server')
|
console.warn('[MediaPlayerContainer] Closing stream due to stream error from server')
|
||||||
this.playerHandler.closePlayer()
|
this.playerHandler.closePlayer()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -496,7 +496,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#streamContainer {
|
#mediaPlayerContainer {
|
||||||
box-shadow: 0px -6px 8px #1111113f;
|
box-shadow: 0px -6px 8px #1111113f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
|
<slot name="header-prefix"></slot>
|
||||||
<h1 class="text-xl">{{ headerText }}</h1>
|
<h1 class="text-xl">{{ headerText }}</h1>
|
||||||
|
|
||||||
<slot name="header-items"></slot>
|
<slot name="header-items"></slot>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sm:w-80 w-full relative">
|
<div class="">
|
||||||
|
<div class="w-full relative sm:w-80">
|
||||||
<form @submit.prevent="submitSearch">
|
<form @submit.prevent="submitSearch">
|
||||||
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||||
</form>
|
</form>
|
||||||
@ -7,7 +8,8 @@
|
|||||||
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
||||||
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
</div>
|
||||||
|
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li v-if="isTyping" class="py-2 px-2">
|
<li v-if="isTyping" class="py-2 px-2">
|
||||||
<p>{{ $strings.MessageThinking }}</p>
|
<p>{{ $strings.MessageThinking }}</p>
|
||||||
|
@ -368,9 +368,17 @@ export default {
|
|||||||
id: 'ebook',
|
id: 'ebook',
|
||||||
name: this.$strings.LabelHasEbook
|
name: this.$strings.LabelHasEbook
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'no-ebook',
|
||||||
|
name: this.$strings.LabelMissingEbook
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'supplementary',
|
id: 'supplementary',
|
||||||
name: this.$strings.LabelHasSupplementaryEbook
|
name: this.$strings.LabelHasSupplementaryEbook
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'no-supplementary',
|
||||||
|
name: this.$strings.LabelMissingSupplementaryEbook
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div class="cursor-pointer text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
<button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||||
</div>
|
</button>
|
||||||
<transition name="menux">
|
<transition name="menux">
|
||||||
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
||||||
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||||
@ -38,8 +38,8 @@ export default {
|
|||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("volume", val);
|
localStorage.setItem('volume', val)
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
console.error('Failed to store volume', err)
|
console.error('Failed to store volume', err)
|
||||||
}
|
}
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
@ -146,7 +146,7 @@ export default {
|
|||||||
if (this.value === 0) {
|
if (this.value === 0) {
|
||||||
this.isMute = true
|
this.isMute = true
|
||||||
}
|
}
|
||||||
const storageVolume = localStorage.getItem("volume")
|
const storageVolume = localStorage.getItem('volume')
|
||||||
if (storageVolume) {
|
if (storageVolume) {
|
||||||
this.volume = parseFloat(storageVolume)
|
this.volume = parseFloat(storageVolume)
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex pt-4 px-2">
|
<div class="flex pt-4 px-2">
|
||||||
<ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn>
|
||||||
|
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -136,7 +137,8 @@ export default {
|
|||||||
newUser: {},
|
newUser: {},
|
||||||
isNew: true,
|
isNew: true,
|
||||||
tags: [],
|
tags: [],
|
||||||
loadingTags: false
|
loadingTags: false,
|
||||||
|
unlinkingFromOpenID: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -180,7 +182,7 @@ export default {
|
|||||||
return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
|
return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
|
||||||
},
|
},
|
||||||
isEditingRoot() {
|
isEditingRoot() {
|
||||||
return this.account && this.account.type === 'root'
|
return this.account?.type === 'root'
|
||||||
},
|
},
|
||||||
libraries() {
|
libraries() {
|
||||||
return this.$store.state.libraries.libraries
|
return this.$store.state.libraries.libraries
|
||||||
@ -198,6 +200,9 @@ export default {
|
|||||||
},
|
},
|
||||||
tagsSelectionText() {
|
tagsSelectionText() {
|
||||||
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
|
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
|
||||||
|
},
|
||||||
|
hasOpenIDLink() {
|
||||||
|
return !!this.account?.hasOpenIDLink
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -205,6 +210,31 @@ export default {
|
|||||||
// Force close when navigating - used in UsersTable
|
// Force close when navigating - used in UsersTable
|
||||||
if (this.$refs.modal) this.$refs.modal.setHide()
|
if (this.$refs.modal) this.$refs.modal.setHide()
|
||||||
},
|
},
|
||||||
|
unlinkOpenID() {
|
||||||
|
const payload = {
|
||||||
|
message: 'Are you sure you want to unlink this user from OpenID?',
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.unlinkingFromOpenID = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/users/${this.account.id}/openid-unlink`)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('User unlinked from OpenID')
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to unlink user from OpenID', error)
|
||||||
|
this.$toast.error('Failed to unlink user from OpenID')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.unlinkingFromOpenID = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
accessAllTagsToggled(val) {
|
accessAllTagsToggled(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
if (this.newUser.itemTagsSelected?.length) {
|
if (this.newUser.itemTagsSelected?.length) {
|
||||||
|
105
client/components/modals/AddCustomMetadataProviderModal.vue
Normal file
105
client/components/modals/AddCustomMetadataProviderModal.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="text-3xl text-white truncate">Add custom metadata provider</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="px-4 w-full flex items-center text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
<div class="w-full p-8">
|
||||||
|
<div class="flex mb-2">
|
||||||
|
<div class="w-3/4 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newName" :label="$strings.LabelName" />
|
||||||
|
</div>
|
||||||
|
<div class="w-1/4 p-1">
|
||||||
|
<ui-text-input-with-label value="Book" readonly :label="$strings.LabelMediaType" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full mb-2 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newUrl" label="URL" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full mb-2 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" />
|
||||||
|
</div>
|
||||||
|
<div class="flex px-1 pt-4">
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" type="submit">{{ $strings.ButtonAdd }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
newName: '',
|
||||||
|
newUrl: '',
|
||||||
|
newAuthHeaderValue: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm() {
|
||||||
|
if (!this.newName || !this.newUrl) {
|
||||||
|
this.$toast.error('Must add name and url')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/custom-metadata-providers', {
|
||||||
|
name: this.newName,
|
||||||
|
url: this.newUrl,
|
||||||
|
mediaType: 'book', // Currently only supporting book mediaType
|
||||||
|
authHeaderValue: this.newAuthHeaderValue
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
this.$emit('added', data.provider)
|
||||||
|
this.$toast.success('New provider added')
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = error.response?.data || 'Unknown error'
|
||||||
|
console.error('Failed to add provider', error)
|
||||||
|
this.$toast.error('Failed to add provider: ' + errorMsg)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.processing = false
|
||||||
|
this.newName = ''
|
||||||
|
this.newUrl = ''
|
||||||
|
this.newAuthHeaderValue = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -49,8 +49,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="media.coverPath">
|
<div v-if="media.coverPath">
|
||||||
<p class="text-center text-gray-200">Current</p>
|
<p class="text-center text-gray-200">Current</p>
|
||||||
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary">
|
<a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, 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" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -328,6 +328,17 @@ export default {
|
|||||||
console.error('PersistProvider', error)
|
console.error('PersistProvider', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getDefaultBookProvider() {
|
||||||
|
let provider = localStorage.getItem('book-provider')
|
||||||
|
if (!provider) return 'google'
|
||||||
|
// Validate book provider
|
||||||
|
if (!this.$store.getters['scanners/checkBookProviderExists'](provider)) {
|
||||||
|
console.error('Stored book provider does not exist', provider)
|
||||||
|
localStorage.removeItem('book-provider')
|
||||||
|
return 'google'
|
||||||
|
}
|
||||||
|
return provider
|
||||||
|
},
|
||||||
getSearchQuery() {
|
getSearchQuery() {
|
||||||
if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}`
|
if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}`
|
||||||
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
|
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
|
||||||
@ -434,7 +445,9 @@ export default {
|
|||||||
this.searchTitle = this.libraryItem.media.metadata.title
|
this.searchTitle = this.libraryItem.media.metadata.title
|
||||||
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
||||||
if (this.isPodcast) this.provider = 'itunes'
|
if (this.isPodcast) this.provider = 'itunes'
|
||||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
else {
|
||||||
|
this.provider = this.getDefaultBookProvider()
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer using ASIN if set and using audible provider
|
// Prefer using ASIN if set and using audible provider
|
||||||
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
|
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
|
||||||
@ -533,24 +546,11 @@ export default {
|
|||||||
// Persist in local storage
|
// Persist in local storage
|
||||||
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
|
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
|
||||||
|
|
||||||
|
if (Object.keys(updatePayload).length) {
|
||||||
if (updatePayload.metadata.cover) {
|
if (updatePayload.metadata.cover) {
|
||||||
const coverPayload = {
|
updatePayload.url = updatePayload.metadata.cover
|
||||||
url: updatePayload.metadata.cover
|
|
||||||
}
|
|
||||||
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
|
||||||
console.error('Failed to update', error)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (success) {
|
|
||||||
this.$toast.success(this.$strings.ToastItemCoverUpdateSuccess)
|
|
||||||
} else {
|
|
||||||
this.$toast.error(this.$strings.ToastItemCoverUpdateFailed)
|
|
||||||
}
|
|
||||||
console.log('Updated cover')
|
|
||||||
delete updatePayload.metadata.cover
|
delete updatePayload.metadata.cover
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
|
||||||
const mediaUpdatePayload = updatePayload
|
const mediaUpdatePayload = updatePayload
|
||||||
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||||
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
|
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
|
||||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max new episodes to download per check
|
Max new episodes to download per check
|
||||||
@ -129,9 +129,12 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.$refs.maxEpisodesInput && this.$refs.maxEpisodesInput.isFocused) {
|
|
||||||
|
if (this.$refs.maxEpisodesInput?.isFocused) {
|
||||||
this.$refs.maxEpisodesInput.blur()
|
this.$refs.maxEpisodesInput.blur()
|
||||||
return
|
}
|
||||||
|
if (this.$refs.maxEpisodesToDownloadInput?.isFocused) {
|
||||||
|
this.$refs.maxEpisodesToDownloadInput.blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePayload = {
|
const updatePayload = {
|
||||||
@ -140,9 +143,11 @@ export default {
|
|||||||
if (this.enableAutoDownloadEpisodes) {
|
if (this.enableAutoDownloadEpisodes) {
|
||||||
updatePayload.autoDownloadSchedule = this.cronExpression
|
updatePayload.autoDownloadSchedule = this.cronExpression
|
||||||
}
|
}
|
||||||
|
this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep)
|
||||||
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
|
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
|
||||||
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
|
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
|
||||||
}
|
}
|
||||||
|
this.newMaxNewEpisodesToDownload = Number(this.newMaxNewEpisodesToDownload)
|
||||||
if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) {
|
if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) {
|
||||||
updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload
|
updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,9 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -69,7 +72,8 @@ export default {
|
|||||||
skipMatchingMediaWithAsin: false,
|
skipMatchingMediaWithAsin: false,
|
||||||
skipMatchingMediaWithIsbn: false,
|
skipMatchingMediaWithIsbn: false,
|
||||||
audiobooksOnly: false,
|
audiobooksOnly: false,
|
||||||
hideSingleBookSeries: false
|
hideSingleBookSeries: false,
|
||||||
|
podcastSearchRegion: 'us'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -85,6 +89,9 @@ export default {
|
|||||||
isBookLibrary() {
|
isBookLibrary() {
|
||||||
return this.mediaType === 'book'
|
return this.mediaType === 'book'
|
||||||
},
|
},
|
||||||
|
isPodcastLibrary() {
|
||||||
|
return this.mediaType === 'podcast'
|
||||||
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.providers
|
||||||
@ -99,7 +106,8 @@ export default {
|
|||||||
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
||||||
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
|
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
|
||||||
audiobooksOnly: !!this.audiobooksOnly,
|
audiobooksOnly: !!this.audiobooksOnly,
|
||||||
hideSingleBookSeries: !!this.hideSingleBookSeries
|
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
||||||
|
podcastSearchRegion: this.podcastSearchRegion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -113,6 +121,7 @@ export default {
|
|||||||
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
||||||
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
|
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
|
||||||
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
||||||
|
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<div class="break-words">{{ episode.title }}</div>
|
<div class="break-words">{{ episode.title }}</div>
|
||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
|
||||||
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
|
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
|
||||||
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
||||||
<p class="break-words mb-1">{{ episode.title }}</p>
|
<p class="break-words mb-1">{{ episode.title }}</p>
|
||||||
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
|
||||||
<p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
<p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -2,21 +2,21 @@
|
|||||||
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
|
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
<button :aria-label="$strings.ButtonPreviousChapter" class="flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
|
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<button :aria-label="$strings.ButtonJumpBackward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<button :aria-label="$strings.ButtonJumpForward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
<button :aria-label="$strings.ButtonNextChapter" class="flex items-center justify-center ml-4 md:ml-8" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
|
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
|
||||||
</div>
|
</button>
|
||||||
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
@ -57,7 +57,6 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
duration: {
|
duration: {
|
||||||
immediate: true,
|
|
||||||
handler() {
|
handler() {
|
||||||
this.setChapterTicks()
|
this.setChapterTicks()
|
||||||
}
|
}
|
||||||
@ -205,10 +204,14 @@ export default {
|
|||||||
},
|
},
|
||||||
windowResize() {
|
windowResize() {
|
||||||
this.setTrackWidth()
|
this.setTrackWidth()
|
||||||
|
this.setChapterTicks()
|
||||||
|
this.updatePlayedTrackWidth()
|
||||||
|
this.updateBufferTrack()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setTrackWidth()
|
this.setTrackWidth()
|
||||||
|
this.setChapterTicks()
|
||||||
window.addEventListener('resize', this.windowResize)
|
window.addEventListener('resize', this.windowResize)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
@ -9,37 +9,37 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer">
|
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer">
|
||||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
<button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
||||||
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
|
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
|
||||||
<div v-else class="flex items-center">
|
<div v-else class="flex items-center">
|
||||||
<span class="material-icons text-lg text-warning">snooze</span>
|
<span class="material-icons text-lg text-warning">snooze</span>
|
||||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
|
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
|
||||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
<button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
||||||
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||||
</div>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
||||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
<button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
</div>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
|
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
|
||||||
<button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
<button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
||||||
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
|
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
||||||
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
<button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
||||||
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
||||||
</div>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -7,9 +7,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p>
|
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
|
||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : 'See Year in Review' }}</ui-btn>
|
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide :
|
||||||
|
$strings.LabelYearReviewShow }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- your year in review -->
|
<!-- your year in review -->
|
||||||
@ -20,24 +21,27 @@
|
|||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
||||||
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">Previous</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{
|
||||||
|
$strings.ButtonShare }}
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p>
|
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}
|
||||||
|
</p>
|
||||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<!-- refresh button -->
|
<!-- refresh button -->
|
||||||
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
|
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
|
||||||
<span class="hidden sm:inline-block">Refresh</span>
|
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
|
||||||
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">Next</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -46,7 +50,7 @@
|
|||||||
<!-- your year in review short -->
|
<!-- your year in review short -->
|
||||||
<div class="w-full max-w-[800px] mx-auto my-4">
|
<div class="w-full max-w-[800px] mx-auto my-4">
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort"> Share </ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
|
||||||
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -56,24 +60,25 @@
|
|||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
||||||
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">Previous</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }}
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p>
|
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
|
||||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<!-- refresh button -->
|
<!-- refresh button -->
|
||||||
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
|
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
|
||||||
<span class="hidden sm:inline-block">Refresh</span>
|
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
|
||||||
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">Next</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
127
client/components/tables/CustomMetadataProviderTable.vue
Normal file
127
client/components/tables/CustomMetadataProviderTable.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-40">
|
||||||
|
<table v-if="providers.length" id="providers">
|
||||||
|
<tr>
|
||||||
|
<th>{{ $strings.LabelName }}</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Authorization Header Value</th>
|
||||||
|
<th class="w-12"></th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="provider in providers" :key="provider.id">
|
||||||
|
<td class="text-sm">{{ provider.name }}</td>
|
||||||
|
<td class="text-sm">{{ provider.url }}</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
<span v-if="provider.authHeaderValue" class="custom-provider-api-key">{{ provider.authHeaderValue }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)">
|
||||||
|
<button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div v-else-if="!processing" class="text-center py-8">
|
||||||
|
<p class="text-lg">No custom metadata providers</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
providers: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
processing: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeProvider(provider) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$emit('update:processing', true)
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/custom-metadata-providers/${provider.id}`)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Provider removed')
|
||||||
|
this.$emit('removed', provider.id)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove provider', error)
|
||||||
|
this.$toast.error('Failed to remove provider')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$emit('update:processing', false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#providers {
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #474747;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#providers td,
|
||||||
|
#providers th {
|
||||||
|
/* border: 1px solid #2e2e2e; */
|
||||||
|
padding: 8px 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#providers td.py-0 {
|
||||||
|
padding: 0px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#providers tr:nth-child(even) {
|
||||||
|
background-color: #373838;
|
||||||
|
}
|
||||||
|
|
||||||
|
#providers tr:nth-child(odd) {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#providers tr:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#providers th {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
background-color: #272727;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-provider-api-key {
|
||||||
|
padding: 1px;
|
||||||
|
background-color: #272727;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: transparent;
|
||||||
|
transition: color, background-color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-provider-api-key:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
@ -7,8 +7,8 @@
|
|||||||
<widgets-podcast-type-indicator :type="episodeType" />
|
<widgets-podcast-type-indicator :type="episodeType" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-10 flex items-center mt-1.5 mb-0.5">
|
<div class="h-10 flex items-center mt-1.5 mb-0.5 overflow-hidden">
|
||||||
<p class="text-sm text-gray-200 episode-subtitle" v-html="episodeSubtitle"></p>
|
<p class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-8 flex items-center">
|
<div class="h-8 flex items-center">
|
||||||
<div class="w-full inline-flex justify-between max-w-xl">
|
<div class="w-full inline-flex justify-between max-w-xl">
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</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" style="min-width: 40px; width: fit-content" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -110,15 +110,6 @@ export default {
|
|||||||
this.typingTimeout = setTimeout(() => {
|
this.typingTimeout = setTimeout(() => {
|
||||||
this.currentSearch = this.textInput
|
this.currentSearch = this.textInput
|
||||||
}, 100)
|
}, 100)
|
||||||
this.setInputWidth()
|
|
||||||
},
|
|
||||||
setInputWidth() {
|
|
||||||
setTimeout(() => {
|
|
||||||
var value = this.$refs.input.value
|
|
||||||
var len = value.length * 7 + 24
|
|
||||||
this.$refs.input.style.width = len + 'px'
|
|
||||||
this.recalcMenuPos()
|
|
||||||
}, 50)
|
|
||||||
},
|
},
|
||||||
recalcMenuPos() {
|
recalcMenuPos() {
|
||||||
if (!this.menu || !this.$refs.inputWrapper) return
|
if (!this.menu || !this.$refs.inputWrapper) return
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
|
<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>
|
<span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
|
||||||
</div>
|
</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" style="min-width: 40px; width: fit-content" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -127,15 +127,6 @@ export default {
|
|||||||
this.typingTimeout = setTimeout(() => {
|
this.typingTimeout = setTimeout(() => {
|
||||||
this.search()
|
this.search()
|
||||||
}, 250)
|
}, 250)
|
||||||
this.setInputWidth()
|
|
||||||
},
|
|
||||||
setInputWidth() {
|
|
||||||
setTimeout(() => {
|
|
||||||
var value = this.$refs.input.value
|
|
||||||
var len = value.length * 7 + 24
|
|
||||||
this.$refs.input.style.width = len + 'px'
|
|
||||||
this.recalcMenuPos()
|
|
||||||
}, 50)
|
|
||||||
},
|
},
|
||||||
recalcMenuPos() {
|
recalcMenuPos() {
|
||||||
if (!this.menu || !this.$refs.inputWrapper) return
|
if (!this.menu || !this.$refs.inputWrapper) return
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<Nuxt :key="currentLang" />
|
<Nuxt :key="currentLang" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-stream-container ref="streamContainer" />
|
<app-media-player-container ref="mediaPlayerContainer" />
|
||||||
|
|
||||||
<modals-item-edit-modal />
|
<modals-item-edit-modal />
|
||||||
<modals-collections-add-create-modal />
|
<modals-collections-add-create-modal />
|
||||||
@ -129,23 +129,23 @@ export default {
|
|||||||
this.$eventBus.$emit('socket_init')
|
this.$eventBus.$emit('socket_init')
|
||||||
},
|
},
|
||||||
streamOpen(stream) {
|
streamOpen(stream) {
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamOpen(stream)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
|
||||||
},
|
},
|
||||||
streamClosed(streamId) {
|
streamClosed(streamId) {
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamClosed(streamId)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamClosed(streamId)
|
||||||
},
|
},
|
||||||
streamProgress(data) {
|
streamProgress(data) {
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamProgress(data)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamProgress(data)
|
||||||
},
|
},
|
||||||
streamReady() {
|
streamReady() {
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamReady()
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamReady()
|
||||||
},
|
},
|
||||||
streamReset(payload) {
|
streamReset(payload) {
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamReset(payload)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamReset(payload)
|
||||||
},
|
},
|
||||||
streamError({ id, errorMessage }) {
|
streamError({ id, errorMessage }) {
|
||||||
this.$toast.error(`Stream Failed: ${errorMessage}`)
|
this.$toast.error(`Stream Failed: ${errorMessage}`)
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamError(id)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamError(id)
|
||||||
},
|
},
|
||||||
libraryAdded(library) {
|
libraryAdded(library) {
|
||||||
this.$store.commit('libraries/addUpdate', library)
|
this.$store.commit('libraries/addUpdate', library)
|
||||||
@ -247,7 +247,7 @@ export default {
|
|||||||
this.multiSessionCurrentSessionId = null
|
this.multiSessionCurrentSessionId = null
|
||||||
this.$toast.dismiss('multiple-sessions')
|
this.$toast.dismiss('multiple-sessions')
|
||||||
}
|
}
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.sessionClosedEvent(sessionId)
|
||||||
},
|
},
|
||||||
userMediaProgressUpdate(payload) {
|
userMediaProgressUpdate(payload) {
|
||||||
this.$store.commit('user/updateMediaProgress', payload)
|
this.$store.commit('user/updateMediaProgress', payload)
|
||||||
@ -328,6 +328,14 @@ export default {
|
|||||||
|
|
||||||
this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices)
|
this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices)
|
||||||
},
|
},
|
||||||
|
customMetadataProviderAdded(provider) {
|
||||||
|
if (!provider?.id) return
|
||||||
|
this.$store.commit('scanners/addCustomMetadataProvider', provider)
|
||||||
|
},
|
||||||
|
customMetadataProviderRemoved(provider) {
|
||||||
|
if (!provider?.id) return
|
||||||
|
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
||||||
|
},
|
||||||
initializeSocket() {
|
initializeSocket() {
|
||||||
this.socket = this.$nuxtSocket({
|
this.socket = this.$nuxtSocket({
|
||||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||||
@ -406,6 +414,10 @@ export default {
|
|||||||
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
||||||
|
|
||||||
this.socket.on('admin_message', this.adminMessageEvt)
|
this.socket.on('admin_message', this.adminMessageEvt)
|
||||||
|
|
||||||
|
// Custom metadata provider Listeners
|
||||||
|
this.socket.on('custom_metadata_provider_added', this.customMetadataProviderAdded)
|
||||||
|
this.socket.on('custom_metadata_provider_removed', this.customMetadataProviderRemoved)
|
||||||
},
|
},
|
||||||
showUpdateToast(versionData) {
|
showUpdateToast(versionData) {
|
||||||
var ignoreVersion = localStorage.getItem('ignoreVersion')
|
var ignoreVersion = localStorage.getItem('ignoreVersion')
|
||||||
|
@ -29,7 +29,8 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
script: [],
|
script: [],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }
|
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' },
|
||||||
|
{ rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -95,7 +96,7 @@ module.exports = {
|
|||||||
meta: {
|
meta: {
|
||||||
appleStatusBarStyle: 'black',
|
appleStatusBarStyle: 'black',
|
||||||
name: 'Audiobookshelf',
|
name: 'Audiobookshelf',
|
||||||
theme_color: '#373838',
|
theme_color: '#232323',
|
||||||
mobileAppIOS: true,
|
mobileAppIOS: true,
|
||||||
nativeUI: true
|
nativeUI: true
|
||||||
},
|
},
|
||||||
@ -103,7 +104,7 @@ module.exports = {
|
|||||||
name: 'Audiobookshelf',
|
name: 'Audiobookshelf',
|
||||||
short_name: 'Audiobookshelf',
|
short_name: 'Audiobookshelf',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#373838',
|
background_color: '#232323',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
||||||
|
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.7.2",
|
"version": "2.8.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.7.2",
|
"version": "2.8.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.7.2",
|
"version": "2.8.0",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -82,19 +82,33 @@ export default {
|
|||||||
this.$setLanguageCode(lang)
|
this.$setLanguageCode(lang)
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
var rootSocket = this.$root.socket || {}
|
// Disconnect from socket
|
||||||
const logoutPayload = {
|
if (this.$root.socket) {
|
||||||
socketId: rootSocket.id
|
console.log('Disconnecting from socket', this.$root.socket.id)
|
||||||
|
this.$root.socket.removeAllListeners()
|
||||||
|
this.$root.socket.disconnect()
|
||||||
}
|
}
|
||||||
this.$axios.$post('/logout', logoutPayload).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
if (localStorage.getItem('token')) {
|
if (localStorage.getItem('token')) {
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
}
|
}
|
||||||
this.$store.commit('libraries/setUserPlaylists', [])
|
this.$store.commit('libraries/setUserPlaylists', [])
|
||||||
this.$store.commit('libraries/setCollections', [])
|
this.$store.commit('libraries/setCollections', [])
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post('/logout')
|
||||||
|
.then((logoutPayload) => {
|
||||||
|
const redirect_url = logoutPayload.redirect_url
|
||||||
|
|
||||||
|
if (redirect_url) {
|
||||||
|
window.location.href = redirect_url
|
||||||
|
} else {
|
||||||
this.$router.push('/login')
|
this.$router.push('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.password = null
|
this.password = null
|
||||||
|
@ -142,7 +142,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
||||||
<div v-if="!chapterData" class="flex p-20">
|
<div v-if="!chapterData" class="flex p-20">
|
||||||
<ui-text-input-with-label v-model="asinInput" label="ASIN" />
|
<ui-text-input-with-label v-model.trim="asinInput" label="ASIN" />
|
||||||
<ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 mx-1" />
|
<ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 mx-1" />
|
||||||
<ui-btn small color="primary" class="mt-5" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn>
|
<ui-btn small color="primary" class="mt-5" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p>
|
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p>
|
||||||
<p class="text-white max-w-3xl text-sm leading-5 whitespace-pre-wrap">{{ author.description }}</p>
|
<p ref="description" id="author-description" class="text-white max-w-3xl text-base whitespace-pre-wrap" :class="{ 'show-full': showFullDescription }">{{ author.description }}</p>
|
||||||
|
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">
|
||||||
|
{{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -62,7 +65,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
isDescriptionClamped: false,
|
||||||
|
showFullDescription: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
@ -82,6 +88,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkDescriptionClamped() {
|
||||||
|
if (!this.$refs.description) return
|
||||||
|
this.isDescriptionClamped = this.$refs.description.scrollHeight > this.$refs.description.clientHeight
|
||||||
|
},
|
||||||
editAuthor() {
|
editAuthor() {
|
||||||
this.$store.commit('globals/showEditAuthorModal', this.author)
|
this.$store.commit('globals/showEditAuthorModal', this.author)
|
||||||
},
|
},
|
||||||
@ -93,6 +103,7 @@ export default {
|
|||||||
series: this.authorSeries,
|
series: this.authorSeries,
|
||||||
libraryItems: this.libraryItems
|
libraryItems: this.libraryItems
|
||||||
}
|
}
|
||||||
|
this.$nextTick(this.checkDescriptionClamped)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
authorRemoved(author) {
|
authorRemoved(author) {
|
||||||
@ -104,6 +115,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (!this.author) this.$router.replace('/')
|
if (!this.author) this.$router.replace('/')
|
||||||
|
this.checkDescriptionClamped()
|
||||||
|
|
||||||
this.$root.socket.on('author_updated', this.authorUpdated)
|
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||||
this.$root.socket.on('author_removed', this.authorRemoved)
|
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||||
@ -114,3 +126,18 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#author-description {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
max-height: 6.25rem;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
#author-description.show-full {
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
max-height: 999rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -20,44 +20,44 @@
|
|||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
<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-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>
|
||||||
<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" />
|
<ui-checkbox v-model="selectedBatchUsage.authors" />
|
||||||
<!-- Authors filter only contains authors in this library, uses filter data -->
|
<!-- 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>
|
||||||
<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-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>
|
||||||
<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-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>
|
||||||
<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-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>
|
||||||
<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-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>
|
||||||
<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-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>
|
||||||
<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-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>
|
||||||
<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-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>
|
||||||
<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" />
|
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<ui-checkbox
|
<ui-checkbox
|
||||||
@ -71,6 +71,20 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
<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: [],
|
narrators: [],
|
||||||
publisher: null,
|
publisher: null,
|
||||||
language: null,
|
language: null,
|
||||||
explicit: false
|
explicit: false,
|
||||||
|
abridged: false
|
||||||
},
|
},
|
||||||
selectedBatchUsage: {
|
selectedBatchUsage: {
|
||||||
subtitle: false,
|
subtitle: false,
|
||||||
@ -151,7 +166,8 @@ export default {
|
|||||||
narrators: false,
|
narrators: false,
|
||||||
publisher: false,
|
publisher: false,
|
||||||
language: false,
|
language: false,
|
||||||
explicit: false
|
explicit: false,
|
||||||
|
abridged: false
|
||||||
},
|
},
|
||||||
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
||||||
openMapOptions: false
|
openMapOptions: false
|
||||||
|
@ -206,7 +206,7 @@ export default {
|
|||||||
|
|
||||||
function isValidRedirectURI(uri) {
|
function isValidRedirectURI(uri) {
|
||||||
// Check for somestring://someother/string
|
// Check for somestring://someother/string
|
||||||
const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i')
|
const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i')
|
||||||
return pattern.test(uri)
|
return pattern.test(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<app-settings-content :header-text="$strings.HeaderCustomMetadataProviders">
|
||||||
|
<template #header-prefix>
|
||||||
|
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center mr-2">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<template #header-items>
|
||||||
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||||
|
<a href="https://www.audiobookshelf.org/guides/custom-metadata-providers" target="_blank" class="inline-flex">
|
||||||
|
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
|
||||||
|
</a>
|
||||||
|
</ui-tooltip>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<ui-btn color="primary" small @click="setShowAddModal">{{ $strings.ButtonAdd }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<tables-custom-metadata-provider-table :providers="providers" :processing.sync="processing" class="pt-2" @removed="providerRemoved" />
|
||||||
|
<modals-add-custom-metadata-provider-modal ref="addModal" v-model="showAddModal" @added="providerAdded" />
|
||||||
|
</app-settings-content>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAddModal: false,
|
||||||
|
processing: false,
|
||||||
|
providers: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
providerRemoved(providerId) {
|
||||||
|
this.providers = this.providers.filter((p) => p.id !== providerId)
|
||||||
|
},
|
||||||
|
providerAdded(provider) {
|
||||||
|
this.providers.push(provider)
|
||||||
|
},
|
||||||
|
setShowAddModal() {
|
||||||
|
this.showAddModal = true
|
||||||
|
},
|
||||||
|
loadProviders() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/custom-metadata-providers')
|
||||||
|
.then((res) => {
|
||||||
|
this.providers = res.providers
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to load custom metadata providers')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadProviders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
@ -13,6 +13,12 @@
|
|||||||
<span class="material-icons">arrow_forward</span>
|
<span class="material-icons">arrow_forward</span>
|
||||||
</div>
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
<nuxt-link to="/config/item-metadata-utils/custom-metadata-providers" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 my-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p>{{ $strings.HeaderCustomMetadataProviders }}</p>
|
||||||
|
<span class="material-icons">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 800px; min-height: 550px">
|
<div ref="container" id="log-container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="min-height: 550px">
|
||||||
<template v-for="(log, index) in logs">
|
<template v-for="(log, index) in logs">
|
||||||
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
|
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
|
||||||
<p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p>
|
<p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p>
|
||||||
@ -136,7 +136,15 @@ export default {
|
|||||||
this.loadedLogs = this.loadedLogs.slice(-5000)
|
this.loadedLogs = this.loadedLogs.slice(-5000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init(attempts = 0) {
|
async loadLoggerData() {
|
||||||
|
const loggerData = await this.$axios.$get('/api/logger-data').catch((error) => {
|
||||||
|
console.error('Failed to load logger data', error)
|
||||||
|
this.$toast.error('Failed to load logger data')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.loadedLogs = loggerData?.currentDailyLogs || []
|
||||||
|
},
|
||||||
|
async init(attempts = 0) {
|
||||||
if (!this.$root.socket) {
|
if (!this.$root.socket) {
|
||||||
if (attempts > 10) {
|
if (attempts > 10) {
|
||||||
return console.error('Failed to setup socket listeners')
|
return console.error('Failed to setup socket listeners')
|
||||||
@ -147,14 +155,11 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.loadLoggerData()
|
||||||
|
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
this.$root.socket.on('daily_logs', this.dailyLogsLoaded)
|
|
||||||
this.$root.socket.on('log', this.logEvtReceived)
|
this.$root.socket.on('log', this.logEvtReceived)
|
||||||
this.$root.socket.emit('set_log_listener', this.newServerSettings.logLevel)
|
this.$root.socket.emit('set_log_listener', this.newServerSettings.logLevel)
|
||||||
this.$root.socket.emit('fetch_daily_logs')
|
|
||||||
},
|
|
||||||
dailyLogsLoaded(lines) {
|
|
||||||
this.loadedLogs = lines
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
@ -166,13 +171,15 @@ export default {
|
|||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (!this.$root.socket) return
|
if (!this.$root.socket) return
|
||||||
this.$root.socket.emit('remove_log_listener')
|
this.$root.socket.emit('remove_log_listener')
|
||||||
this.$root.socket.off('daily_logs', this.dailyLogsLoaded)
|
|
||||||
this.$root.socket.off('log', this.logEvtReceived)
|
this.$root.socket.off('log', this.logEvtReceived)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
#log-container {
|
||||||
|
height: calc(100vh - 285px);
|
||||||
|
}
|
||||||
.logmessage {
|
.logmessage {
|
||||||
width: calc(100% - 208px);
|
width: calc(100% - 208px);
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
<div class="flex items-center my-2">
|
<div class="flex items-center my-2">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div class="hidden sm:inline-flex items-center">
|
<div class="hidden sm:inline-flex items-center">
|
||||||
<p class="text-sm">{{ $strings.LabelRowsPerPage }}</p>
|
<p class="text-sm whitespace-nowrap">{{ $strings.LabelRowsPerPage }}</p>
|
||||||
<ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" />
|
<ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" />
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center">
|
<div class="inline-flex items-center">
|
||||||
|
@ -125,7 +125,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 w-full">
|
<div class="my-4 w-full">
|
||||||
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
<p ref="description" id="item-description" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p>
|
||||||
|
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">
|
||||||
|
{{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="invalidAudioFiles.length" class="bg-error border-red-800 shadow-md p-4">
|
<div v-if="invalidAudioFiles.length" class="bg-error border-red-800 shadow-md p-4">
|
||||||
@ -182,7 +185,9 @@ export default {
|
|||||||
podcastFeedEpisodes: [],
|
podcastFeedEpisodes: [],
|
||||||
episodesDownloading: [],
|
episodesDownloading: [],
|
||||||
episodeDownloadsQueued: [],
|
episodeDownloadsQueued: [],
|
||||||
showBookmarksModal: false
|
showBookmarksModal: false,
|
||||||
|
isDescriptionClamped: false,
|
||||||
|
showFullDescription: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -596,10 +601,15 @@ export default {
|
|||||||
this.$store.commit('setBookshelfBookIds', [])
|
this.$store.commit('setBookshelfBookIds', [])
|
||||||
this.$store.commit('showEditModal', this.libraryItem)
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
},
|
},
|
||||||
|
checkDescriptionClamped() {
|
||||||
|
if (!this.$refs.description) return
|
||||||
|
this.isDescriptionClamped = this.$refs.description.scrollHeight > this.$refs.description.clientHeight
|
||||||
|
},
|
||||||
libraryItemUpdated(libraryItem) {
|
libraryItemUpdated(libraryItem) {
|
||||||
if (libraryItem.id === this.libraryItemId) {
|
if (libraryItem.id === this.libraryItemId) {
|
||||||
console.log('Item was updated', libraryItem)
|
console.log('Item was updated', libraryItem)
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
|
this.$nextTick(this.checkDescriptionClamped)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearProgressClick() {
|
clearProgressClick() {
|
||||||
@ -756,6 +766,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.checkDescriptionClamped()
|
||||||
|
|
||||||
this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || []
|
this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || []
|
||||||
this.episodesDownloading = this.libraryItem.episodesDownloading || []
|
this.episodesDownloading = this.libraryItem.episodesDownloading || []
|
||||||
|
|
||||||
@ -782,3 +794,18 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#item-description {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
max-height: 6.25rem;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
#item-description.show-full {
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
max-height: 999rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -8,11 +8,11 @@
|
|||||||
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<template v-for="(episode, index) in episodesMapped">
|
<template v-for="(episode, index) in episodesMapped">
|
||||||
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
|
<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">
|
<div class="flex-grow pl-4 max-w-2xl">
|
||||||
<!-- mobile -->
|
<!-- mobile -->
|
||||||
<div class="flex md:hidden mb-2">
|
<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-grow px-2">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex" @click.stop>
|
<div class="flex" @click.stop>
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 mb-4 episode-subtitle-long" v-html="episode.subtitle || episode.description" />
|
<p class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" />
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||||
|
@ -86,6 +86,9 @@ export default {
|
|||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
|
librarySettings() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrarySettings']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -151,7 +154,12 @@ export default {
|
|||||||
async submitSearch(term) {
|
async submitSearch(term) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.termSearched = ''
|
this.termSearched = ''
|
||||||
let results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}`).catch((error) => {
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
term,
|
||||||
|
country: this.librarySettings?.podcastSearchRegion || 'us'
|
||||||
|
})
|
||||||
|
let results = await this.$axios.$get(`/api/search/podcast?${searchParams.toString()}`).catch((error) => {
|
||||||
console.error('Search request failed', error)
|
console.error('Search request failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
<form @submit.prevent="submitServerSetup">
|
<form @submit.prevent="submitServerSetup">
|
||||||
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
|
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
|
||||||
<ui-text-input-with-label v-model="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model.trim="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||||
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model.trim="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||||
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model.trim="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||||
|
|
||||||
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
|
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
|
||||||
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
|
||||||
@ -34,10 +34,10 @@
|
|||||||
|
|
||||||
<form v-show="login_local" @submit.prevent="submitForm">
|
<form v-show="login_local" @submit.prevent="submitForm">
|
||||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
||||||
<ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" />
|
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" />
|
||||||
|
|
||||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
|
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
|
||||||
<ui-text-input v-model="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" />
|
||||||
<div class="w-full flex justify-end py-3">
|
<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>
|
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,13 +10,16 @@ const languageCodeMap = {
|
|||||||
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||||
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
||||||
'es': { label: 'Español', dateFnsLocale: 'es' },
|
'es': { label: 'Español', dateFnsLocale: 'es' },
|
||||||
|
'et': { label: 'Eesti', dateFnsLocale: 'et' },
|
||||||
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||||
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||||
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||||
'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' },
|
'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' },
|
||||||
|
'hu': { label: 'Magyar', dateFnsLocale: 'hu' },
|
||||||
'nl': { label: 'Nederlands', dateFnsLocale: 'nl' },
|
'nl': { label: 'Nederlands', dateFnsLocale: 'nl' },
|
||||||
'no': { label: 'Norsk', dateFnsLocale: 'no' },
|
'no': { label: 'Norsk', dateFnsLocale: 'no' },
|
||||||
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
||||||
|
'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' },
|
||||||
'ru': { label: 'Русский', dateFnsLocale: 'ru' },
|
'ru': { label: 'Русский', dateFnsLocale: 'ru' },
|
||||||
'sv': { label: 'Svenska', dateFnsLocale: 'sv' },
|
'sv': { label: 'Svenska', dateFnsLocale: 'sv' },
|
||||||
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
||||||
@ -28,6 +31,18 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||||
|
const podcastSearchRegionMap = {
|
||||||
|
'us': { label: 'United States' },
|
||||||
|
'cn': { label: '中国' }
|
||||||
|
}
|
||||||
|
Vue.prototype.$podcastSearchRegionOptions = Object.keys(podcastSearchRegionMap).map(code => {
|
||||||
|
return {
|
||||||
|
text: podcastSearchRegionMap[code].label,
|
||||||
|
value: code
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
Vue.prototype.$languageCodes = {
|
Vue.prototype.$languageCodes = {
|
||||||
default: defaultCode,
|
default: defaultCode,
|
||||||
current: defaultCode,
|
current: defaultCode,
|
||||||
@ -83,7 +98,7 @@ async function loadi18n(code) {
|
|||||||
|
|
||||||
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
||||||
|
|
||||||
this.$eventBus.$emit('change-lang', code)
|
this?.$eventBus?.$emit('change-lang', code)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,14 +156,14 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function xmlToJson(xml) {
|
function xmlToJson(xml) {
|
||||||
const json = {};
|
const json = {}
|
||||||
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
|
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
|
||||||
const key = res[1] || res[3];
|
const key = res[1] || res[3]
|
||||||
const value = res[2] && xmlToJson(res[2]);
|
const value = res[2] && xmlToJson(res[2])
|
||||||
json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null;
|
json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null
|
||||||
|
|
||||||
}
|
}
|
||||||
return json;
|
return json
|
||||||
}
|
}
|
||||||
Vue.prototype.$xmlToJson = xmlToJson
|
Vue.prototype.$xmlToJson = xmlToJson
|
||||||
|
|
||||||
|
BIN
client/static/ios_icon.png
Normal file
BIN
client/static/ios_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
@ -113,6 +113,7 @@ export const actions = {
|
|||||||
const library = data.library
|
const library = data.library
|
||||||
const filterData = data.filterdata
|
const filterData = data.filterdata
|
||||||
const issues = data.issues || 0
|
const issues = data.issues || 0
|
||||||
|
const customMetadataProviders = data.customMetadataProviders || []
|
||||||
const numUserPlaylists = data.numUserPlaylists
|
const numUserPlaylists = data.numUserPlaylists
|
||||||
|
|
||||||
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
||||||
@ -126,6 +127,8 @@ export const actions = {
|
|||||||
commit('setLibraryIssues', issues)
|
commit('setLibraryIssues', issues)
|
||||||
commit('setLibraryFilterData', filterData)
|
commit('setLibraryFilterData', filterData)
|
||||||
commit('setNumUserPlaylists', numUserPlaylists)
|
commit('setNumUserPlaylists', numUserPlaylists)
|
||||||
|
commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
|
||||||
|
|
||||||
commit('setCurrentLibrary', libraryId)
|
commit('setCurrentLibrary', libraryId)
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
@ -71,8 +71,56 @@ export const state = () => ({
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {}
|
export const getters = {
|
||||||
|
checkBookProviderExists: state => (providerValue) => {
|
||||||
|
return state.providers.some(p => p.value === providerValue)
|
||||||
|
},
|
||||||
|
checkPodcastProviderExists: state => (providerValue) => {
|
||||||
|
return state.podcastProviders.some(p => p.value === providerValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const actions = {}
|
export const actions = {}
|
||||||
|
|
||||||
export const mutations = {}
|
export const mutations = {
|
||||||
|
addCustomMetadataProvider(state, provider) {
|
||||||
|
if (provider.mediaType === 'book') {
|
||||||
|
if (state.providers.some(p => p.value === provider.slug)) return
|
||||||
|
state.providers.push({
|
||||||
|
text: provider.name,
|
||||||
|
value: provider.slug
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (state.podcastProviders.some(p => p.value === provider.slug)) return
|
||||||
|
state.podcastProviders.push({
|
||||||
|
text: provider.name,
|
||||||
|
value: provider.slug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeCustomMetadataProvider(state, provider) {
|
||||||
|
if (provider.mediaType === 'book') {
|
||||||
|
state.providers = state.providers.filter(p => p.value !== provider.slug)
|
||||||
|
} else {
|
||||||
|
state.podcastProviders = state.podcastProviders.filter(p => p.value !== provider.slug)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCustomMetadataProviders(state, providers) {
|
||||||
|
if (!providers?.length) return
|
||||||
|
|
||||||
|
const mediaType = providers[0].mediaType
|
||||||
|
if (mediaType === 'book') {
|
||||||
|
// clear previous values, and add new values to the end
|
||||||
|
state.providers = state.providers.filter((p) => !p.value.startsWith('custom-'))
|
||||||
|
state.providers = [
|
||||||
|
...state.providers,
|
||||||
|
...providers.map((p) => ({
|
||||||
|
text: p.name,
|
||||||
|
value: p.slug
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
// Podcast providers not supported yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Skrýt",
|
"ButtonHide": "Skrýt",
|
||||||
"ButtonHome": "Domů",
|
"ButtonHome": "Domů",
|
||||||
"ButtonIssues": "Problémy",
|
"ButtonIssues": "Problémy",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Nejnovější",
|
"ButtonLatest": "Nejnovější",
|
||||||
"ButtonLibrary": "Knihovna",
|
"ButtonLibrary": "Knihovna",
|
||||||
"ButtonLogout": "Odhlásit",
|
"ButtonLogout": "Odhlásit",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Spárovat všechny autory",
|
"ButtonMatchAllAuthors": "Spárovat všechny autory",
|
||||||
"ButtonMatchBooks": "Spárovat Knihy",
|
"ButtonMatchBooks": "Spárovat Knihy",
|
||||||
"ButtonNevermind": "Nevadí",
|
"ButtonNevermind": "Nevadí",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Otevřít kanál",
|
"ButtonOpenFeed": "Otevřít kanál",
|
||||||
"ButtonOpenManager": "Otevřít správce",
|
"ButtonOpenManager": "Otevřít správce",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Přehrát",
|
"ButtonPlay": "Přehrát",
|
||||||
"ButtonPlaying": "Hraje",
|
"ButtonPlaying": "Hraje",
|
||||||
"ButtonPlaylists": "Seznamy skladeb",
|
"ButtonPlaylists": "Seznamy skladeb",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
|
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
|
||||||
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
|
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
|
||||||
"ButtonPurgeMediaProgress": "Vyčistit průběh médií",
|
"ButtonPurgeMediaProgress": "Vyčistit průběh médií",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Odstranit z fronty",
|
"ButtonQueueRemoveItem": "Odstranit z fronty",
|
||||||
"ButtonQuickMatch": "Rychlé přiřazení",
|
"ButtonQuickMatch": "Rychlé přiřazení",
|
||||||
"ButtonRead": "Číst",
|
"ButtonRead": "Číst",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Odstranit",
|
"ButtonRemove": "Odstranit",
|
||||||
"ButtonRemoveAll": "Odstranit vše",
|
"ButtonRemoveAll": "Odstranit vše",
|
||||||
"ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny",
|
"ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Vybrat cestu ke složce",
|
"ButtonSelectFolderPath": "Vybrat cestu ke složce",
|
||||||
"ButtonSeries": "Série",
|
"ButtonSeries": "Série",
|
||||||
"ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop",
|
"ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Časy posunu",
|
"ButtonShiftTimes": "Časy posunu",
|
||||||
"ButtonShow": "Zobrazit",
|
"ButtonShow": "Zobrazit",
|
||||||
"ButtonStartM4BEncode": "Spustit kódování M4B",
|
"ButtonStartM4BEncode": "Spustit kódování M4B",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Položky kolekce",
|
"HeaderCollectionItems": "Položky kolekce",
|
||||||
"HeaderCover": "Obálka",
|
"HeaderCover": "Obálka",
|
||||||
"HeaderCurrentDownloads": "Aktuální stahování",
|
"HeaderCurrentDownloads": "Aktuální stahování",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Podrobnosti",
|
"HeaderDetails": "Podrobnosti",
|
||||||
"HeaderDownloadQueue": "Fronta stahování",
|
"HeaderDownloadQueue": "Fronta stahování",
|
||||||
"HeaderEbookFiles": "Soubory elektronických knih",
|
"HeaderEbookFiles": "Soubory elektronických knih",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Aktualizovat podrobnosti",
|
"HeaderUpdateDetails": "Aktualizovat podrobnosti",
|
||||||
"HeaderUpdateLibrary": "Aktualizovat knihovnu",
|
"HeaderUpdateLibrary": "Aktualizovat knihovnu",
|
||||||
"HeaderUsers": "Uživatelé",
|
"HeaderUsers": "Uživatelé",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Vaše statistiky",
|
"HeaderYourStats": "Vaše statistiky",
|
||||||
"LabelAbridged": "Zkráceno",
|
"LabelAbridged": "Zkráceno",
|
||||||
"LabelAccountType": "Typ účtu",
|
"LabelAccountType": "Typ účtu",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Metaznačky",
|
"LabelMetaTags": "Metaznačky",
|
||||||
"LabelMinute": "Minuta",
|
"LabelMinute": "Minuta",
|
||||||
"LabelMissing": "Chybějící",
|
"LabelMissing": "Chybějící",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Chybějící díly",
|
"LabelMissingParts": "Chybějící díly",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Více",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Může stahovat",
|
"LabelPermissionsDownload": "Může stahovat",
|
||||||
"LabelPermissionsUpdate": "Může aktualizovat",
|
"LabelPermissionsUpdate": "Může aktualizovat",
|
||||||
"LabelPermissionsUpload": "Může nahrávat",
|
"LabelPermissionsUpload": "Může nahrávat",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Cesta k fotografii/URL",
|
"LabelPhotoPathURL": "Cesta k fotografii/URL",
|
||||||
"LabelPlaylists": "Seznamy skladeb",
|
"LabelPlaylists": "Seznamy skladeb",
|
||||||
"LabelPlayMethod": "Metoda přehrávání",
|
"LabelPlayMethod": "Metoda přehrávání",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasty",
|
"LabelPodcasts": "Podcasty",
|
||||||
|
"LabelPodcastSearchRegion": "Oblast vyhledávání podcastu",
|
||||||
"LabelPodcastType": "Typ podcastu",
|
"LabelPodcastType": "Typ podcastu",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)",
|
"LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Série",
|
"LabelSeries": "Série",
|
||||||
"LabelSeriesName": "Název série",
|
"LabelSeriesName": "Název série",
|
||||||
"LabelSeriesProgress": "Průběh série",
|
"LabelSeriesProgress": "Průběh série",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Nastavit jako primární",
|
"LabelSetEbookAsPrimary": "Nastavit jako primární",
|
||||||
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
|
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
|
||||||
"LabelSettingsAudiobooksOnly": "Pouze audioknihy",
|
"LabelSettingsAudiobooksOnly": "Pouze audioknihy",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Zobrazit frontu přehrávače",
|
"LabelViewQueue": "Zobrazit frontu přehrávače",
|
||||||
"LabelVolume": "Hlasitost",
|
"LabelVolume": "Hlasitost",
|
||||||
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
|
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Doba trvání vaší audioknihy",
|
"LabelYourAudiobookDuration": "Doba trvání vaší audioknihy",
|
||||||
"LabelYourBookmarks": "Vaše záložky",
|
"LabelYourBookmarks": "Vaše záložky",
|
||||||
"LabelYourPlaylists": "Vaše seznamy přehrávání",
|
"LabelYourPlaylists": "Vaše seznamy přehrávání",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Skjul",
|
"ButtonHide": "Skjul",
|
||||||
"ButtonHome": "Hjem",
|
"ButtonHome": "Hjem",
|
||||||
"ButtonIssues": "Problemer",
|
"ButtonIssues": "Problemer",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Seneste",
|
"ButtonLatest": "Seneste",
|
||||||
"ButtonLibrary": "Bibliotek",
|
"ButtonLibrary": "Bibliotek",
|
||||||
"ButtonLogout": "Log ud",
|
"ButtonLogout": "Log ud",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Match alle forfattere",
|
"ButtonMatchAllAuthors": "Match alle forfattere",
|
||||||
"ButtonMatchBooks": "Match bøger",
|
"ButtonMatchBooks": "Match bøger",
|
||||||
"ButtonNevermind": "Glem det",
|
"ButtonNevermind": "Glem det",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "OK",
|
"ButtonOk": "OK",
|
||||||
"ButtonOpenFeed": "Åbn feed",
|
"ButtonOpenFeed": "Åbn feed",
|
||||||
"ButtonOpenManager": "Åbn manager",
|
"ButtonOpenManager": "Åbn manager",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Afspil",
|
"ButtonPlay": "Afspil",
|
||||||
"ButtonPlaying": "Afspiller",
|
"ButtonPlaying": "Afspiller",
|
||||||
"ButtonPlaylists": "Afspilningslister",
|
"ButtonPlaylists": "Afspilningslister",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Ryd al cache",
|
"ButtonPurgeAllCache": "Ryd al cache",
|
||||||
"ButtonPurgeItemsCache": "Ryd elementcache",
|
"ButtonPurgeItemsCache": "Ryd elementcache",
|
||||||
"ButtonPurgeMediaProgress": "Ryd Medieforløb",
|
"ButtonPurgeMediaProgress": "Ryd Medieforløb",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Fjern fra kø",
|
"ButtonQueueRemoveItem": "Fjern fra kø",
|
||||||
"ButtonQuickMatch": "Hurtig Match",
|
"ButtonQuickMatch": "Hurtig Match",
|
||||||
"ButtonRead": "Læs",
|
"ButtonRead": "Læs",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Fjern",
|
"ButtonRemove": "Fjern",
|
||||||
"ButtonRemoveAll": "Fjern Alle",
|
"ButtonRemoveAll": "Fjern Alle",
|
||||||
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
|
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Vælg Mappen Sti",
|
"ButtonSelectFolderPath": "Vælg Mappen Sti",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
|
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Skift Tider",
|
"ButtonShiftTimes": "Skift Tider",
|
||||||
"ButtonShow": "Vis",
|
"ButtonShow": "Vis",
|
||||||
"ButtonStartM4BEncode": "Start M4B Kode",
|
"ButtonStartM4BEncode": "Start M4B Kode",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Samlingselementer",
|
"HeaderCollectionItems": "Samlingselementer",
|
||||||
"HeaderCover": "Omslag",
|
"HeaderCover": "Omslag",
|
||||||
"HeaderCurrentDownloads": "Nuværende Downloads",
|
"HeaderCurrentDownloads": "Nuværende Downloads",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Detaljer",
|
"HeaderDetails": "Detaljer",
|
||||||
"HeaderDownloadQueue": "Download Kø",
|
"HeaderDownloadQueue": "Download Kø",
|
||||||
"HeaderEbookFiles": "E-bogsfiler",
|
"HeaderEbookFiles": "E-bogsfiler",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Opdater Detaljer",
|
"HeaderUpdateDetails": "Opdater Detaljer",
|
||||||
"HeaderUpdateLibrary": "Opdater Bibliotek",
|
"HeaderUpdateLibrary": "Opdater Bibliotek",
|
||||||
"HeaderUsers": "Brugere",
|
"HeaderUsers": "Brugere",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Dine Statistikker",
|
"HeaderYourStats": "Dine Statistikker",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Kontotype",
|
"LabelAccountType": "Kontotype",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta-tags",
|
"LabelMetaTags": "Meta-tags",
|
||||||
"LabelMinute": "Minut",
|
"LabelMinute": "Minut",
|
||||||
"LabelMissing": "Mangler",
|
"LabelMissing": "Mangler",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Manglende dele",
|
"LabelMissingParts": "Manglende dele",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Mere",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Kan downloade",
|
"LabelPermissionsDownload": "Kan downloade",
|
||||||
"LabelPermissionsUpdate": "Kan opdatere",
|
"LabelPermissionsUpdate": "Kan opdatere",
|
||||||
"LabelPermissionsUpload": "Kan uploade",
|
"LabelPermissionsUpload": "Kan uploade",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Foto sti/URL",
|
"LabelPhotoPathURL": "Foto sti/URL",
|
||||||
"LabelPlaylists": "Afspilningslister",
|
"LabelPlaylists": "Afspilningslister",
|
||||||
"LabelPlayMethod": "Afspilningsmetode",
|
"LabelPlayMethod": "Afspilningsmetode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast søgeområde",
|
||||||
"LabelPodcastType": "Podcast type",
|
"LabelPodcastType": "Podcast type",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
|
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Serienavn",
|
"LabelSeriesName": "Serienavn",
|
||||||
"LabelSeriesProgress": "Seriefremskridt",
|
"LabelSeriesProgress": "Seriefremskridt",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Indstil som primær",
|
"LabelSetEbookAsPrimary": "Indstil som primær",
|
||||||
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
|
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
|
||||||
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
|
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Se afspilningskø",
|
"LabelViewQueue": "Se afspilningskø",
|
||||||
"LabelVolume": "Volumen",
|
"LabelVolume": "Volumen",
|
||||||
"LabelWeekdaysToRun": "Ugedage til kørsel",
|
"LabelWeekdaysToRun": "Ugedage til kørsel",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Din lydbogsvarighed",
|
"LabelYourAudiobookDuration": "Din lydbogsvarighed",
|
||||||
"LabelYourBookmarks": "Dine bogmærker",
|
"LabelYourBookmarks": "Dine bogmærker",
|
||||||
"LabelYourPlaylists": "Dine spillelister",
|
"LabelYourPlaylists": "Dine spillelister",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Ausblenden",
|
"ButtonHide": "Ausblenden",
|
||||||
"ButtonHome": "Startseite",
|
"ButtonHome": "Startseite",
|
||||||
"ButtonIssues": "Probleme",
|
"ButtonIssues": "Probleme",
|
||||||
|
"ButtonJumpBackward": "Zurück springen",
|
||||||
|
"ButtonJumpForward": "Vorwärts springen",
|
||||||
"ButtonLatest": "Neuste",
|
"ButtonLatest": "Neuste",
|
||||||
"ButtonLibrary": "Bibliothek",
|
"ButtonLibrary": "Bibliothek",
|
||||||
"ButtonLogout": "Abmelden",
|
"ButtonLogout": "Abmelden",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||||
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||||
"ButtonNevermind": "Abbrechen",
|
"ButtonNevermind": "Abbrechen",
|
||||||
|
"ButtonNext": "Vor",
|
||||||
|
"ButtonNextChapter": "Nächstes Kapitel",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
"ButtonOpenManager": "Manager öffnen",
|
"ButtonOpenManager": "Manager öffnen",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Abspielen",
|
"ButtonPlay": "Abspielen",
|
||||||
"ButtonPlaying": "Spielt",
|
"ButtonPlaying": "Spielt",
|
||||||
"ButtonPlaylists": "Wiedergabelisten",
|
"ButtonPlaylists": "Wiedergabelisten",
|
||||||
|
"ButtonPrevious": "Zurück",
|
||||||
|
"ButtonPreviousChapter": "Vorheriges Kapitel",
|
||||||
"ButtonPurgeAllCache": "Cache leeren",
|
"ButtonPurgeAllCache": "Cache leeren",
|
||||||
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
|
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
|
||||||
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
|
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||||
"ButtonQuickMatch": "Schnellabgleich",
|
"ButtonQuickMatch": "Schnellabgleich",
|
||||||
"ButtonRead": "Lesen",
|
"ButtonRead": "Lesen",
|
||||||
|
"ButtonRefresh": "Neu Laden",
|
||||||
"ButtonRemove": "Löschen",
|
"ButtonRemove": "Löschen",
|
||||||
"ButtonRemoveAll": "Alles löschen",
|
"ButtonRemoveAll": "Alles löschen",
|
||||||
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
|
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Ordnerpfad auswählen",
|
"ButtonSelectFolderPath": "Ordnerpfad auswählen",
|
||||||
"ButtonSeries": "Serien",
|
"ButtonSeries": "Serien",
|
||||||
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
|
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
|
||||||
|
"ButtonShare": "Teilen",
|
||||||
"ButtonShiftTimes": "Zeitverschiebung",
|
"ButtonShiftTimes": "Zeitverschiebung",
|
||||||
"ButtonShow": "Anzeigen",
|
"ButtonShow": "Anzeigen",
|
||||||
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Sammlungseinträge",
|
"HeaderCollectionItems": "Sammlungseinträge",
|
||||||
"HeaderCover": "Titelbild",
|
"HeaderCover": "Titelbild",
|
||||||
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
||||||
|
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadata Anbieter",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Warteschlange",
|
"HeaderDownloadQueue": "Download Warteschlange",
|
||||||
"HeaderEbookFiles": "E-Book Dateien",
|
"HeaderEbookFiles": "E-Book Dateien",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Details aktualisieren",
|
"HeaderUpdateDetails": "Details aktualisieren",
|
||||||
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
||||||
"HeaderUsers": "Benutzer",
|
"HeaderUsers": "Benutzer",
|
||||||
|
"HeaderYearReview": "Jahr {0} in Übersicht",
|
||||||
"HeaderYourStats": "Eigene Statistiken",
|
"HeaderYourStats": "Eigene Statistiken",
|
||||||
"LabelAbridged": "Gekürzt",
|
"LabelAbridged": "Gekürzt",
|
||||||
"LabelAccountType": "Kontoart",
|
"LabelAccountType": "Kontoart",
|
||||||
@ -281,11 +292,11 @@
|
|||||||
"LabelFinished": "Beendet",
|
"LabelFinished": "Beendet",
|
||||||
"LabelFolder": "Ordner",
|
"LabelFolder": "Ordner",
|
||||||
"LabelFolders": "Verzeichnisse",
|
"LabelFolders": "Verzeichnisse",
|
||||||
"LabelFontBold": "Bold",
|
"LabelFontBold": "Fett",
|
||||||
"LabelFontFamily": "Schriftfamilie",
|
"LabelFontFamily": "Schriftfamilie",
|
||||||
"LabelFontItalic": "Italic",
|
"LabelFontItalic": "Kursiv",
|
||||||
"LabelFontScale": "Schriftgröße",
|
"LabelFontScale": "Schriftgröße",
|
||||||
"LabelFontStrikethrough": "Strikethrough",
|
"LabelFontStrikethrough": "Durchgestrichen",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Kategorie",
|
"LabelGenre": "Kategorie",
|
||||||
"LabelGenres": "Kategorien",
|
"LabelGenres": "Kategorien",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Fehlend",
|
"LabelMissing": "Fehlend",
|
||||||
|
"LabelMissingEbook": "E-Book fehlt",
|
||||||
"LabelMissingParts": "Fehlende Teile",
|
"LabelMissingParts": "Fehlende Teile",
|
||||||
|
"LabelMissingSupplementaryEbook": "Ergänzendes E-Book fehlt",
|
||||||
"LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App",
|
"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.",
|
"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",
|
"LabelMore": "Mehr",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Herunterladen",
|
"LabelPermissionsDownload": "Herunterladen",
|
||||||
"LabelPermissionsUpdate": "Aktualisieren",
|
"LabelPermissionsUpdate": "Aktualisieren",
|
||||||
"LabelPermissionsUpload": "Hochladen",
|
"LabelPermissionsUpload": "Hochladen",
|
||||||
|
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
|
||||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||||
"LabelPlaylists": "Wiedergabelisten",
|
"LabelPlaylists": "Wiedergabelisten",
|
||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast-Suchregion",
|
||||||
"LabelPodcastType": "Podcast Typ",
|
"LabelPodcastType": "Podcast Typ",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
||||||
@ -406,7 +421,7 @@
|
|||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRecommended": "Empfohlen",
|
"LabelRecommended": "Empfohlen",
|
||||||
"LabelRedo": "Redo",
|
"LabelRedo": "Wiederholen",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Serienfortschritt",
|
"LabelSeriesProgress": "Serienfortschritt",
|
||||||
|
"LabelServerYearReview": "Server Jahr in Übersicht ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
|
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
|
||||||
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
|
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
|
||||||
"LabelSettingsAudiobooksOnly": "Nur Hörbücher",
|
"LabelSettingsAudiobooksOnly": "Nur Hörbücher",
|
||||||
@ -495,10 +511,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
||||||
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
|
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
|
||||||
"LabelTasks": "Laufende Aufgaben",
|
"LabelTasks": "Laufende Aufgaben",
|
||||||
"LabelTextEditorBulletedList": "Bulleted list",
|
"LabelTextEditorBulletedList": "Aufzählungsliste",
|
||||||
"LabelTextEditorLink": "Link",
|
"LabelTextEditorLink": "Link",
|
||||||
"LabelTextEditorNumberedList": "Numbered list",
|
"LabelTextEditorNumberedList": "nummerierte Liste",
|
||||||
"LabelTextEditorUnlink": "Unlink",
|
"LabelTextEditorUnlink": "entkoppeln",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dunkel",
|
"LabelThemeDark": "Dunkel",
|
||||||
"LabelThemeLight": "Hell",
|
"LabelThemeLight": "Hell",
|
||||||
@ -524,7 +540,7 @@
|
|||||||
"LabelTracksSingleTrack": "Einzeldatei",
|
"LabelTracksSingleTrack": "Einzeldatei",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnabridged": "Ungekürzt",
|
"LabelUnabridged": "Ungekürzt",
|
||||||
"LabelUndo": "Undo",
|
"LabelUndo": "Rückgängig machen",
|
||||||
"LabelUnknown": "Unbekannt",
|
"LabelUnknown": "Unbekannt",
|
||||||
"LabelUpdateCover": "Titelbild aktualisieren",
|
"LabelUpdateCover": "Titelbild aktualisieren",
|
||||||
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
|
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Lautstärke",
|
"LabelVolume": "Lautstärke",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
|
"LabelYearReviewHide": "Verstecke Jahr in Übersicht",
|
||||||
|
"LabelYearReviewShow": "Zeige Jahr in Übersicht",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit deines Mediums",
|
"LabelYourAudiobookDuration": "Laufzeit deines Mediums",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Hide",
|
"ButtonHide": "Hide",
|
||||||
"ButtonHome": "Home",
|
"ButtonHome": "Home",
|
||||||
"ButtonIssues": "Issues",
|
"ButtonIssues": "Issues",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Latest",
|
"ButtonLatest": "Latest",
|
||||||
"ButtonLibrary": "Library",
|
"ButtonLibrary": "Library",
|
||||||
"ButtonLogout": "Logout",
|
"ButtonLogout": "Logout",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Match All Authors",
|
"ButtonMatchAllAuthors": "Match All Authors",
|
||||||
"ButtonMatchBooks": "Match Books",
|
"ButtonMatchBooks": "Match Books",
|
||||||
"ButtonNevermind": "Nevermind",
|
"ButtonNevermind": "Nevermind",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Open Feed",
|
"ButtonOpenFeed": "Open Feed",
|
||||||
"ButtonOpenManager": "Open Manager",
|
"ButtonOpenManager": "Open Manager",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Play",
|
"ButtonPlay": "Play",
|
||||||
"ButtonPlaying": "Playing",
|
"ButtonPlaying": "Playing",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Playlists",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Purge All Cache",
|
"ButtonPurgeAllCache": "Purge All Cache",
|
||||||
"ButtonPurgeItemsCache": "Purge Items Cache",
|
"ButtonPurgeItemsCache": "Purge Items Cache",
|
||||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Remove from queue",
|
"ButtonQueueRemoveItem": "Remove from queue",
|
||||||
"ButtonQuickMatch": "Quick Match",
|
"ButtonQuickMatch": "Quick Match",
|
||||||
"ButtonRead": "Read",
|
"ButtonRead": "Read",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Remove",
|
"ButtonRemove": "Remove",
|
||||||
"ButtonRemoveAll": "Remove All",
|
"ButtonRemoveAll": "Remove All",
|
||||||
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
|
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Select Folder Path",
|
"ButtonSelectFolderPath": "Select Folder Path",
|
||||||
"ButtonSeries": "Series",
|
"ButtonSeries": "Series",
|
||||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Shift Times",
|
"ButtonShiftTimes": "Shift Times",
|
||||||
"ButtonShow": "Show",
|
"ButtonShow": "Show",
|
||||||
"ButtonStartM4BEncode": "Start M4B Encode",
|
"ButtonStartM4BEncode": "Start M4B Encode",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Collection Items",
|
"HeaderCollectionItems": "Collection Items",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Update Details",
|
"HeaderUpdateDetails": "Update Details",
|
||||||
"HeaderUpdateLibrary": "Update Library",
|
"HeaderUpdateLibrary": "Update Library",
|
||||||
"HeaderUsers": "Users",
|
"HeaderUsers": "Users",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Your Stats",
|
"HeaderYourStats": "Your Stats",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Account Type",
|
"LabelAccountType": "Account Type",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Missing",
|
"LabelMissing": "Missing",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Missing Parts",
|
"LabelMissingParts": "Missing Parts",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "More",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Can Download",
|
"LabelPermissionsDownload": "Can Download",
|
||||||
"LabelPermissionsUpdate": "Can Update",
|
"LabelPermissionsUpdate": "Can Update",
|
||||||
"LabelPermissionsUpload": "Can Upload",
|
"LabelPermissionsUpload": "Can Upload",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Photo Path/URL",
|
"LabelPhotoPathURL": "Photo Path/URL",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Play Method",
|
"LabelPlayMethod": "Play Method",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast search region",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Series Name",
|
"LabelSeriesName": "Series Name",
|
||||||
"LabelSeriesProgress": "Series Progress",
|
"LabelSeriesProgress": "Series Progress",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Set as primary",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "View player queue",
|
"LabelViewQueue": "View player queue",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Weekdays to run",
|
"LabelWeekdaysToRun": "Weekdays to run",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Your audiobook duration",
|
"LabelYourAudiobookDuration": "Your audiobook duration",
|
||||||
"LabelYourBookmarks": "Your Bookmarks",
|
"LabelYourBookmarks": "Your Bookmarks",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Your Playlists",
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Agregar",
|
"ButtonAdd": "Agregar",
|
||||||
"ButtonAddChapters": "Agregar Capitulo",
|
"ButtonAddChapters": "Agregar Capitulo",
|
||||||
"ButtonAddDevice": "Add Device",
|
"ButtonAddDevice": "Agregar Dispositivo",
|
||||||
"ButtonAddLibrary": "Add Library",
|
"ButtonAddLibrary": "Crear Biblioteca",
|
||||||
"ButtonAddPodcasts": "Agregar Podcasts",
|
"ButtonAddPodcasts": "Agregar Podcasts",
|
||||||
"ButtonAddUser": "Add User",
|
"ButtonAddUser": "Crear Usuario",
|
||||||
"ButtonAddYourFirstLibrary": "Agrega tu Primera Biblioteca",
|
"ButtonAddYourFirstLibrary": "Crea tu Primera Biblioteca",
|
||||||
"ButtonApply": "Aplicar",
|
"ButtonApply": "Aplicar",
|
||||||
"ButtonApplyChapters": "Aplicar Capítulos",
|
"ButtonApplyChapters": "Aplicar Capítulos",
|
||||||
"ButtonAuthors": "Autores",
|
"ButtonAuthors": "Autores",
|
||||||
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Esconder",
|
"ButtonHide": "Esconder",
|
||||||
"ButtonHome": "Inicio",
|
"ButtonHome": "Inicio",
|
||||||
"ButtonIssues": "Problemas",
|
"ButtonIssues": "Problemas",
|
||||||
|
"ButtonJumpBackward": "Retroceder",
|
||||||
|
"ButtonJumpForward": "Adelantar",
|
||||||
"ButtonLatest": "Últimos",
|
"ButtonLatest": "Últimos",
|
||||||
"ButtonLibrary": "Biblioteca",
|
"ButtonLibrary": "Biblioteca",
|
||||||
"ButtonLogout": "Cerrar Sesión",
|
"ButtonLogout": "Cerrar Sesión",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
|
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
|
||||||
"ButtonMatchBooks": "Encontrar Libros",
|
"ButtonMatchBooks": "Encontrar Libros",
|
||||||
"ButtonNevermind": "Olvidar",
|
"ButtonNevermind": "Olvidar",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Siguiente Capítulo",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Abrir Fuente",
|
"ButtonOpenFeed": "Abrir Fuente",
|
||||||
"ButtonOpenManager": "Abrir Editor",
|
"ButtonOpenManager": "Abrir Editor",
|
||||||
|
"ButtonPause": "Pausar",
|
||||||
"ButtonPlay": "Reproducir",
|
"ButtonPlay": "Reproducir",
|
||||||
"ButtonPlaying": "Reproduciendo",
|
"ButtonPlaying": "Reproduciendo",
|
||||||
"ButtonPlaylists": "Listas de Reproducción",
|
"ButtonPlaylists": "Listas de Reproducción",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Capítulo Anterior",
|
||||||
"ButtonPurgeAllCache": "Purgar Todo el Cache",
|
"ButtonPurgeAllCache": "Purgar Todo el Cache",
|
||||||
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
|
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
|
||||||
"ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia",
|
"ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Remover de la Fila",
|
"ButtonQueueRemoveItem": "Remover de la Fila",
|
||||||
"ButtonQuickMatch": "Encontrar Rápido",
|
"ButtonQuickMatch": "Encontrar Rápido",
|
||||||
"ButtonRead": "Leer",
|
"ButtonRead": "Leer",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Remover",
|
"ButtonRemove": "Remover",
|
||||||
"ButtonRemoveAll": "Remover Todos",
|
"ButtonRemoveAll": "Remover Todos",
|
||||||
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta",
|
"ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta",
|
||||||
"ButtonSeries": "Series",
|
"ButtonSeries": "Series",
|
||||||
"ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas",
|
"ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Desplazar Tiempos",
|
"ButtonShiftTimes": "Desplazar Tiempos",
|
||||||
"ButtonShow": "Mostrar",
|
"ButtonShow": "Mostrar",
|
||||||
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
||||||
@ -87,15 +96,15 @@
|
|||||||
"ButtonUserEdit": "Editar Usuario {0}",
|
"ButtonUserEdit": "Editar Usuario {0}",
|
||||||
"ButtonViewAll": "Ver Todos",
|
"ButtonViewAll": "Ver Todos",
|
||||||
"ButtonYes": "Aceptar",
|
"ButtonYes": "Aceptar",
|
||||||
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
|
"ErrorUploadFetchMetadataAPI": "Error obteniendo metadatos",
|
||||||
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
|
"ErrorUploadFetchMetadataNoResults": "No se pudo obtener metadatos - Intenta actualizar el título y/o autor",
|
||||||
"ErrorUploadLacksTitle": "Must have a title",
|
"ErrorUploadLacksTitle": "Se debe tener título",
|
||||||
"HeaderAccount": "Cuenta",
|
"HeaderAccount": "Cuenta",
|
||||||
"HeaderAdvanced": "Avanzado",
|
"HeaderAdvanced": "Avanzado",
|
||||||
"HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise",
|
"HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise",
|
||||||
"HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro",
|
"HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro",
|
||||||
"HeaderAudioTracks": "Pistas de Audio",
|
"HeaderAudioTracks": "Pistas de Audio",
|
||||||
"HeaderAuthentication": "Authentication",
|
"HeaderAuthentication": "Autenticación",
|
||||||
"HeaderBackups": "Respaldos",
|
"HeaderBackups": "Respaldos",
|
||||||
"HeaderChangePassword": "Cambiar Contraseña",
|
"HeaderChangePassword": "Cambiar Contraseña",
|
||||||
"HeaderChapters": "Capítulos",
|
"HeaderChapters": "Capítulos",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Elementos en la Colección",
|
"HeaderCollectionItems": "Elementos en la Colección",
|
||||||
"HeaderCover": "Portada",
|
"HeaderCover": "Portada",
|
||||||
"HeaderCurrentDownloads": "Descargando Actualmente",
|
"HeaderCurrentDownloads": "Descargando Actualmente",
|
||||||
|
"HeaderCustomMetadataProviders": "Proveedores de metadatos personalizados",
|
||||||
"HeaderDetails": "Detalles",
|
"HeaderDetails": "Detalles",
|
||||||
"HeaderDownloadQueue": "Lista de Descarga",
|
"HeaderDownloadQueue": "Lista de Descarga",
|
||||||
"HeaderEbookFiles": "Archivos de Ebook",
|
"HeaderEbookFiles": "Archivos de Ebook",
|
||||||
@ -130,15 +140,15 @@
|
|||||||
"HeaderManageTags": "Administrar Etiquetas",
|
"HeaderManageTags": "Administrar Etiquetas",
|
||||||
"HeaderMapDetails": "Asignar Detalles",
|
"HeaderMapDetails": "Asignar Detalles",
|
||||||
"HeaderMatch": "Encontrar",
|
"HeaderMatch": "Encontrar",
|
||||||
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
"HeaderMetadataOrderOfPrecedence": "Orden de precedencia de metadatos",
|
||||||
"HeaderMetadataToEmbed": "Metadatos para Insertar",
|
"HeaderMetadataToEmbed": "Metadatos para Insertar",
|
||||||
"HeaderNewAccount": "Nueva Cuenta",
|
"HeaderNewAccount": "Nueva Cuenta",
|
||||||
"HeaderNewLibrary": "Nueva Biblioteca",
|
"HeaderNewLibrary": "Nueva Biblioteca",
|
||||||
"HeaderNotifications": "Notificaciones",
|
"HeaderNotifications": "Notificaciones",
|
||||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
|
"HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect",
|
||||||
"HeaderOpenRSSFeed": "Abrir fuente RSS",
|
"HeaderOpenRSSFeed": "Abrir fuente RSS",
|
||||||
"HeaderOtherFiles": "Otros Archivos",
|
"HeaderOtherFiles": "Otros Archivos",
|
||||||
"HeaderPasswordAuthentication": "Password Authentication",
|
"HeaderPasswordAuthentication": "Autenticación por contraseña",
|
||||||
"HeaderPermissions": "Permisos",
|
"HeaderPermissions": "Permisos",
|
||||||
"HeaderPlayerQueue": "Fila del Reproductor",
|
"HeaderPlayerQueue": "Fila del Reproductor",
|
||||||
"HeaderPlaylist": "Lista de Reproducción",
|
"HeaderPlaylist": "Lista de Reproducción",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Actualizar Detalles",
|
"HeaderUpdateDetails": "Actualizar Detalles",
|
||||||
"HeaderUpdateLibrary": "Actualizar Biblioteca",
|
"HeaderUpdateLibrary": "Actualizar Biblioteca",
|
||||||
"HeaderUsers": "Usuarios",
|
"HeaderUsers": "Usuarios",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Tus Estadísticas",
|
"HeaderYourStats": "Tus Estadísticas",
|
||||||
"LabelAbridged": "Abreviado",
|
"LabelAbridged": "Abreviado",
|
||||||
"LabelAccountType": "Tipo de Cuenta",
|
"LabelAccountType": "Tipo de Cuenta",
|
||||||
@ -187,11 +198,11 @@
|
|||||||
"LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección",
|
"LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección",
|
||||||
"LabelAddToPlaylist": "Añadido a la Lista de Reproducción",
|
"LabelAddToPlaylist": "Añadido a la Lista de Reproducción",
|
||||||
"LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción",
|
"LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción",
|
||||||
"LabelAdminUsersOnly": "Admin users only",
|
"LabelAdminUsersOnly": "Solamente usuarios administradores",
|
||||||
"LabelAll": "Todos",
|
"LabelAll": "Todos",
|
||||||
"LabelAllUsers": "Todos los Usuarios",
|
"LabelAllUsers": "Todos los Usuarios",
|
||||||
"LabelAllUsersExcludingGuests": "All users excluding guests",
|
"LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
|
||||||
"LabelAllUsersIncludingGuests": "All users including guests",
|
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
|
||||||
"LabelAlreadyInYourLibrary": "Ya en la Biblioteca",
|
"LabelAlreadyInYourLibrary": "Ya en la Biblioteca",
|
||||||
"LabelAppend": "Adjuntar",
|
"LabelAppend": "Adjuntar",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
@ -199,12 +210,12 @@
|
|||||||
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
|
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
|
||||||
"LabelAuthors": "Autores",
|
"LabelAuthors": "Autores",
|
||||||
"LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente",
|
"LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente",
|
||||||
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
|
"LabelAutoFetchMetadata": "Actualizar Metadatos Automáticamente",
|
||||||
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
|
"LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.",
|
||||||
"LabelAutoLaunch": "Auto Launch",
|
"LabelAutoLaunch": "Lanzamiento automático",
|
||||||
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
|
"LabelAutoLaunchDescription": "Redirigir al proveedor de autenticación automáticamente al navegar a la página de inicio de sesión (ruta de sobreescritura manual <code>/login?autoLaunch=0</code>)",
|
||||||
"LabelAutoRegister": "Auto Register",
|
"LabelAutoRegister": "Registro automático",
|
||||||
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
|
"LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión",
|
||||||
"LabelBackToUser": "Regresar a Usuario",
|
"LabelBackToUser": "Regresar a Usuario",
|
||||||
"LabelBackupLocation": "Ubicación del Respaldo",
|
"LabelBackupLocation": "Ubicación del Respaldo",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
|
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
|
||||||
@ -215,13 +226,13 @@
|
|||||||
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.",
|
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.",
|
||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Libros",
|
"LabelBooks": "Libros",
|
||||||
"LabelButtonText": "Button Text",
|
"LabelButtonText": "Texto del botón",
|
||||||
"LabelChangePassword": "Cambiar Contraseña",
|
"LabelChangePassword": "Cambiar Contraseña",
|
||||||
"LabelChannels": "Canales",
|
"LabelChannels": "Canales",
|
||||||
"LabelChapters": "Capítulos",
|
"LabelChapters": "Capítulos",
|
||||||
"LabelChaptersFound": "Capítulo Encontrado",
|
"LabelChaptersFound": "Capítulo Encontrado",
|
||||||
"LabelChapterTitle": "Titulo del Capítulo",
|
"LabelChapterTitle": "Titulo del Capítulo",
|
||||||
"LabelClickForMoreInfo": "Click for more info",
|
"LabelClickForMoreInfo": "Click para más información",
|
||||||
"LabelClosePlayer": "Cerrar Reproductor",
|
"LabelClosePlayer": "Cerrar Reproductor",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Colapsar Serie",
|
"LabelCollapseSeries": "Colapsar Serie",
|
||||||
@ -240,12 +251,12 @@
|
|||||||
"LabelCurrently": "En este momento:",
|
"LabelCurrently": "En este momento:",
|
||||||
"LabelCustomCronExpression": "Expresión de Cron Personalizada:",
|
"LabelCustomCronExpression": "Expresión de Cron Personalizada:",
|
||||||
"LabelDatetime": "Hora y Fecha",
|
"LabelDatetime": "Hora y Fecha",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
"LabelDeleteFromFileSystemCheckbox": "Eliminar archivos del sistema (desmarcar para eliminar sólo de la base de datos)",
|
||||||
"LabelDescription": "Descripción",
|
"LabelDescription": "Descripción",
|
||||||
"LabelDeselectAll": "Deseleccionar Todos",
|
"LabelDeselectAll": "Deseleccionar Todos",
|
||||||
"LabelDevice": "Dispositivo",
|
"LabelDevice": "Dispositivo",
|
||||||
"LabelDeviceInfo": "Información de Dispositivo",
|
"LabelDeviceInfo": "Información de Dispositivo",
|
||||||
"LabelDeviceIsAvailableTo": "Device is available to...",
|
"LabelDeviceIsAvailableTo": "El dispositivo está disponible para...",
|
||||||
"LabelDirectory": "Directorio",
|
"LabelDirectory": "Directorio",
|
||||||
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
|
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
|
||||||
"LabelDiscFromMetadata": "Disco a partir de Metadata",
|
"LabelDiscFromMetadata": "Disco a partir de Metadata",
|
||||||
@ -271,7 +282,7 @@
|
|||||||
"LabelExample": "Ejemplo",
|
"LabelExample": "Ejemplo",
|
||||||
"LabelExplicit": "Explicito",
|
"LabelExplicit": "Explicito",
|
||||||
"LabelFeedURL": "Fuente de URL",
|
"LabelFeedURL": "Fuente de URL",
|
||||||
"LabelFetchingMetadata": "Fetching Metadata",
|
"LabelFetchingMetadata": "Obteniendo metadatos",
|
||||||
"LabelFile": "Archivo",
|
"LabelFile": "Archivo",
|
||||||
"LabelFileBirthtime": "Archivo Creado en",
|
"LabelFileBirthtime": "Archivo Creado en",
|
||||||
"LabelFileModified": "Archivo modificado",
|
"LabelFileModified": "Archivo modificado",
|
||||||
@ -281,23 +292,23 @@
|
|||||||
"LabelFinished": "Terminado",
|
"LabelFinished": "Terminado",
|
||||||
"LabelFolder": "Carpeta",
|
"LabelFolder": "Carpeta",
|
||||||
"LabelFolders": "Carpetas",
|
"LabelFolders": "Carpetas",
|
||||||
"LabelFontBold": "Bold",
|
"LabelFontBold": "Negrilla",
|
||||||
"LabelFontFamily": "Familia tipográfica",
|
"LabelFontFamily": "Familia tipográfica",
|
||||||
"LabelFontItalic": "Italic",
|
"LabelFontItalic": "Itálica",
|
||||||
"LabelFontScale": "Tamaño de Fuente",
|
"LabelFontScale": "Tamaño de Fuente",
|
||||||
"LabelFontStrikethrough": "Strikethrough",
|
"LabelFontStrikethrough": "Tachado",
|
||||||
"LabelFormat": "Formato",
|
"LabelFormat": "Formato",
|
||||||
"LabelGenre": "Genero",
|
"LabelGenre": "Genero",
|
||||||
"LabelGenres": "Géneros",
|
"LabelGenres": "Géneros",
|
||||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||||
"LabelHasEbook": "Tiene Ebook",
|
"LabelHasEbook": "Tiene Ebook",
|
||||||
"LabelHasSupplementaryEbook": "Tiene Ebook Suplementario",
|
"LabelHasSupplementaryEbook": "Tiene Ebook Suplementario",
|
||||||
"LabelHighestPriority": "Highest priority",
|
"LabelHighestPriority": "Mayor prioridad",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hora",
|
"LabelHour": "Hora",
|
||||||
"LabelIcon": "Icono",
|
"LabelIcon": "Icono",
|
||||||
"LabelImageURLFromTheWeb": "Image URL from the web",
|
"LabelImageURLFromTheWeb": "URL de la imagen",
|
||||||
"LabelIncludeInTracklist": "Incluir en Tracklist",
|
"LabelIncludeInTracklist": "Incluir en la Tracklist",
|
||||||
"LabelIncomplete": "Incompleto",
|
"LabelIncomplete": "Incompleto",
|
||||||
"LabelInProgress": "En Proceso",
|
"LabelInProgress": "En Proceso",
|
||||||
"LabelInterval": "Intervalo",
|
"LabelInterval": "Intervalo",
|
||||||
@ -334,20 +345,22 @@
|
|||||||
"LabelLogLevelInfo": "Información",
|
"LabelLogLevelInfo": "Información",
|
||||||
"LabelLogLevelWarn": "Advertencia",
|
"LabelLogLevelWarn": "Advertencia",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha",
|
"LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha",
|
||||||
"LabelLowestPriority": "Lowest Priority",
|
"LabelLowestPriority": "Menor prioridad",
|
||||||
"LabelMatchExistingUsersBy": "Match existing users by",
|
"LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por",
|
||||||
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
|
"LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO",
|
||||||
"LabelMediaPlayer": "Reproductor de Medios",
|
"LabelMediaPlayer": "Reproductor de Medios",
|
||||||
"LabelMediaType": "Tipo de Multimedia",
|
"LabelMediaType": "Tipo de Multimedia",
|
||||||
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
|
"LabelMetadataOrderOfPrecedenceDescription": "Las fuentes de metadatos de mayor prioridad prevalecerán sobre las de menor prioridad",
|
||||||
"LabelMetadataProvider": "Proveedor de Metadata",
|
"LabelMetadataProvider": "Proveedor de Metadatos",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Metaetiqueta",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Metaetiquetas",
|
||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMissing": "Ausente",
|
"LabelMissing": "Ausente",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Partes Ausentes",
|
"LabelMissingParts": "Partes Ausentes",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"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.",
|
"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",
|
"LabelMore": "Más",
|
||||||
"LabelMoreInfo": "Más Información",
|
"LabelMoreInfo": "Más Información",
|
||||||
"LabelName": "Nombre",
|
"LabelName": "Nombre",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Puede Descargar",
|
"LabelPermissionsDownload": "Puede Descargar",
|
||||||
"LabelPermissionsUpdate": "Puede Actualizar",
|
"LabelPermissionsUpdate": "Puede Actualizar",
|
||||||
"LabelPermissionsUpload": "Puede Subir",
|
"LabelPermissionsUpload": "Puede Subir",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Ruta de Acceso/URL de Foto",
|
"LabelPhotoPathURL": "Ruta de Acceso/URL de Foto",
|
||||||
"LabelPlaylists": "Lista de Reproducción",
|
"LabelPlaylists": "Lista de Reproducción",
|
||||||
"LabelPlayMethod": "Método de Reproducción",
|
"LabelPlayMethod": "Método de Reproducción",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Región de búsqueda de podcasts",
|
||||||
"LabelPodcastType": "Tipo Podcast",
|
"LabelPodcastType": "Tipo Podcast",
|
||||||
"LabelPort": "Puerto",
|
"LabelPort": "Puerto",
|
||||||
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
||||||
@ -406,11 +421,11 @@
|
|||||||
"LabelRecentlyAdded": "Agregado Recientemente",
|
"LabelRecentlyAdded": "Agregado Recientemente",
|
||||||
"LabelRecentSeries": "Series Recientes",
|
"LabelRecentSeries": "Series Recientes",
|
||||||
"LabelRecommended": "Recomendados",
|
"LabelRecommended": "Recomendados",
|
||||||
"LabelRedo": "Redo",
|
"LabelRedo": "Rehacer",
|
||||||
"LabelRegion": "Región",
|
"LabelRegion": "Región",
|
||||||
"LabelReleaseDate": "Fecha de Estreno",
|
"LabelReleaseDate": "Fecha de Estreno",
|
||||||
"LabelRemoveCover": "Remover Portada",
|
"LabelRemoveCover": "Remover Portada",
|
||||||
"LabelRowsPerPage": "Rows per page",
|
"LabelRowsPerPage": "Filas por página",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
|
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
|
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
|
||||||
"LabelRSSFeedOpen": "Fuente RSS Abierta",
|
"LabelRSSFeedOpen": "Fuente RSS Abierta",
|
||||||
@ -423,12 +438,13 @@
|
|||||||
"LabelSeason": "Temporada",
|
"LabelSeason": "Temporada",
|
||||||
"LabelSelectAllEpisodes": "Seleccionar todos los episodios",
|
"LabelSelectAllEpisodes": "Seleccionar todos los episodios",
|
||||||
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
|
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
|
||||||
"LabelSelectUsers": "Select users",
|
"LabelSelectUsers": "Seleccionar usuarios",
|
||||||
"LabelSendEbookToDevice": "Enviar Ebook a...",
|
"LabelSendEbookToDevice": "Enviar Ebook a...",
|
||||||
"LabelSequence": "Secuencia",
|
"LabelSequence": "Secuencia",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Nombre de la Serie",
|
"LabelSeriesName": "Nombre de la Serie",
|
||||||
"LabelSeriesProgress": "Progreso de la Serie",
|
"LabelSeriesProgress": "Progreso de la Serie",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Establecer como primario",
|
"LabelSetEbookAsPrimary": "Establecer como primario",
|
||||||
"LabelSetEbookAsSupplementary": "Establecer como suplementario",
|
"LabelSetEbookAsSupplementary": "Establecer como suplementario",
|
||||||
"LabelSettingsAudiobooksOnly": "Sólo Audiolibros",
|
"LabelSettingsAudiobooksOnly": "Sólo Audiolibros",
|
||||||
@ -495,14 +511,14 @@
|
|||||||
"LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario",
|
"LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario",
|
||||||
"LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario",
|
"LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario",
|
||||||
"LabelTasks": "Tareas Corriendo",
|
"LabelTasks": "Tareas Corriendo",
|
||||||
"LabelTextEditorBulletedList": "Bulleted list",
|
"LabelTextEditorBulletedList": "Lista con viñetas",
|
||||||
"LabelTextEditorLink": "Link",
|
"LabelTextEditorLink": "Enlazar",
|
||||||
"LabelTextEditorNumberedList": "Numbered list",
|
"LabelTextEditorNumberedList": "Lista numerada",
|
||||||
"LabelTextEditorUnlink": "Unlink",
|
"LabelTextEditorUnlink": "Desenlazar",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Oscuro",
|
"LabelThemeDark": "Oscuro",
|
||||||
"LabelThemeLight": "Claro",
|
"LabelThemeLight": "Claro",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Tiempo Base",
|
||||||
"LabelTimeListened": "Tiempo Escuchando",
|
"LabelTimeListened": "Tiempo Escuchando",
|
||||||
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
||||||
"LabelTimeRemaining": "{0} restante",
|
"LabelTimeRemaining": "{0} restante",
|
||||||
@ -524,7 +540,7 @@
|
|||||||
"LabelTracksSingleTrack": "Una pista",
|
"LabelTracksSingleTrack": "Una pista",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnabridged": "No Abreviado",
|
"LabelUnabridged": "No Abreviado",
|
||||||
"LabelUndo": "Undo",
|
"LabelUndo": "Deshacer",
|
||||||
"LabelUnknown": "Desconocido",
|
"LabelUnknown": "Desconocido",
|
||||||
"LabelUpdateCover": "Actualizar Portada",
|
"LabelUpdateCover": "Actualizar Portada",
|
||||||
"LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.",
|
"LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.",
|
||||||
@ -533,7 +549,7 @@
|
|||||||
"LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados",
|
"LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados",
|
||||||
"LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
|
"LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
|
||||||
"LabelUploaderDropFiles": "Suelte los Archivos",
|
"LabelUploaderDropFiles": "Suelte los Archivos",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
|
"LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente",
|
||||||
"LabelUseChapterTrack": "Usar pista por capitulo",
|
"LabelUseChapterTrack": "Usar pista por capitulo",
|
||||||
"LabelUseFullTrack": "Usar pista completa",
|
"LabelUseFullTrack": "Usar pista completa",
|
||||||
"LabelUser": "Usuario",
|
"LabelUser": "Usuario",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Ver Fila del Reproductor",
|
"LabelViewQueue": "Ver Fila del Reproductor",
|
||||||
"LabelVolume": "Volumen",
|
"LabelVolume": "Volumen",
|
||||||
"LabelWeekdaysToRun": "Correr en Días de la Semana",
|
"LabelWeekdaysToRun": "Correr en Días de la Semana",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
|
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
|
||||||
"LabelYourBookmarks": "Tus Marcadores",
|
"LabelYourBookmarks": "Tus Marcadores",
|
||||||
"LabelYourPlaylists": "Tus Listas",
|
"LabelYourPlaylists": "Tus Listas",
|
||||||
@ -567,15 +585,15 @@
|
|||||||
"MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?",
|
"MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?",
|
||||||
"MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?",
|
"MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?",
|
||||||
"MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
||||||
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
|
"MessageConfirmDeleteLibraryItem": "Esto removerá la librería de la base de datos y archivos en tu sistema. ¿Estás seguro?",
|
||||||
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
|
"MessageConfirmDeleteLibraryItems": "Esto removerá {0} elemento(s) de la librería en base de datos y archivos en tu sistema. ¿Estás seguro?",
|
||||||
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
|
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
|
||||||
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
|
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
|
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
|
||||||
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
|
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
|
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
|
||||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
"MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?",
|
||||||
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
|
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
|
||||||
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
|
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
|
||||||
@ -590,7 +608,7 @@
|
|||||||
"MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
|
"MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.",
|
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.",
|
||||||
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
|
||||||
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
|
"MessageConfirmReScanLibraryItems": "¿Estás seguro de querer re escanear {0} elemento(s)?",
|
||||||
"MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Descargando Capitulo",
|
"MessageDownloadingEpisode": "Descargando Capitulo",
|
||||||
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
|
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
|
||||||
@ -661,7 +679,7 @@
|
|||||||
"MessageRestoreBackupConfirm": "¿Está seguro de que desea para restaurar del respaldo creado en",
|
"MessageRestoreBackupConfirm": "¿Está seguro de que desea para restaurar del respaldo creado en",
|
||||||
"MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.<br /><br />El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.",
|
"MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.<br /><br />El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.",
|
||||||
"MessageSearchResultsFor": "Resultados de la búsqueda de",
|
"MessageSearchResultsFor": "Resultados de la búsqueda de",
|
||||||
"MessageSelected": "{0} selected",
|
"MessageSelected": "{0} seleccionado(s)",
|
||||||
"MessageServerCouldNotBeReached": "No se pudo establecer la conexión con el servidor",
|
"MessageServerCouldNotBeReached": "No se pudo establecer la conexión con el servidor",
|
||||||
"MessageSetChaptersFromTracksDescription": "Establecer capítulos usando cada archivo de audio como un capítulo y el título del capítulo como el nombre del archivo de audio",
|
"MessageSetChaptersFromTracksDescription": "Establecer capítulos usando cada archivo de audio como un capítulo y el título del capítulo como el nombre del archivo de audio",
|
||||||
"MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?",
|
"MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?",
|
||||||
|
779
client/strings/et.json
Normal file
779
client/strings/et.json
Normal file
@ -0,0 +1,779 @@
|
|||||||
|
{
|
||||||
|
"ButtonAdd": "Lisa",
|
||||||
|
"ButtonAddChapters": "Lisa peatükid",
|
||||||
|
"ButtonAddDevice": "Lisa seade",
|
||||||
|
"ButtonAddLibrary": "Lisa raamatukogu",
|
||||||
|
"ButtonAddPodcasts": "Lisa podcastid",
|
||||||
|
"ButtonAddUser": "Lisa kasutaja",
|
||||||
|
"ButtonAddYourFirstLibrary": "Lisa oma esimene raamatukogu",
|
||||||
|
"ButtonApply": "Rakenda",
|
||||||
|
"ButtonApplyChapters": "Rakenda peatükid",
|
||||||
|
"ButtonAuthors": "Autorid",
|
||||||
|
"ButtonBrowseForFolder": "Sirvi kausta",
|
||||||
|
"ButtonCancel": "Tühista",
|
||||||
|
"ButtonCancelEncode": "Tühista kodeerimine",
|
||||||
|
"ButtonChangeRootPassword": "Muuda põhiparooli",
|
||||||
|
"ButtonCheckAndDownloadNewEpisodes": "Kontrolli ja laadi alla uued episoodid",
|
||||||
|
"ButtonChooseAFolder": "Vali kaust",
|
||||||
|
"ButtonChooseFiles": "Vali failid",
|
||||||
|
"ButtonClearFilter": "Tühista filter",
|
||||||
|
"ButtonCloseFeed": "Sulge voog",
|
||||||
|
"ButtonCollections": "Kogud",
|
||||||
|
"ButtonConfigureScanner": "Konfigureeri skanner",
|
||||||
|
"ButtonCreate": "Loo",
|
||||||
|
"ButtonCreateBackup": "Loo varundus",
|
||||||
|
"ButtonDelete": "Kustuta",
|
||||||
|
"ButtonDownloadQueue": "Järjekord",
|
||||||
|
"ButtonEdit": "Muuda",
|
||||||
|
"ButtonEditChapters": "Muuda peatükke",
|
||||||
|
"ButtonEditPodcast": "Muuda podcasti",
|
||||||
|
"ButtonForceReScan": "Sunnitud uuestiskaneerimine",
|
||||||
|
"ButtonFullPath": "Täielik asukoht",
|
||||||
|
"ButtonHide": "Peida",
|
||||||
|
"ButtonHome": "Avaleht",
|
||||||
|
"ButtonIssues": "Probleemid",
|
||||||
|
"ButtonJumpBackward": "Hüppa tagasi",
|
||||||
|
"ButtonJumpForward": "Hüppa edasi",
|
||||||
|
"ButtonLatest": "Uusim",
|
||||||
|
"ButtonLibrary": "Raamatukogu",
|
||||||
|
"ButtonLogout": "Logi välja",
|
||||||
|
"ButtonLookup": "Otsi",
|
||||||
|
"ButtonManageTracks": "Halda lugusid",
|
||||||
|
"ButtonMapChapterTitles": "Kaardista peatükkide pealkirjad",
|
||||||
|
"ButtonMatchAllAuthors": "Sobita kõik autorid",
|
||||||
|
"ButtonMatchBooks": "Sobita raamatud",
|
||||||
|
"ButtonNevermind": "Pole tähtis",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Järgmine peatükk",
|
||||||
|
"ButtonOk": "Ok",
|
||||||
|
"ButtonOpenFeed": "Ava voog",
|
||||||
|
"ButtonOpenManager": "Ava haldur",
|
||||||
|
"ButtonPause": "Peata",
|
||||||
|
"ButtonPlay": "Mängi",
|
||||||
|
"ButtonPlaying": "Mängib",
|
||||||
|
"ButtonPlaylists": "Esitusloendid",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Eelmine peatükk",
|
||||||
|
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
|
||||||
|
"ButtonPurgeItemsCache": "Tühjenda esemete vahemälu",
|
||||||
|
"ButtonPurgeMediaProgress": "Tühjenda meedia edenemine",
|
||||||
|
"ButtonQueueAddItem": "Lisa järjekorda",
|
||||||
|
"ButtonQueueRemoveItem": "Eemalda järjekorrast",
|
||||||
|
"ButtonQuickMatch": "Kiire sobitamine",
|
||||||
|
"ButtonRead": "Loe",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
|
"ButtonRemove": "Eemalda",
|
||||||
|
"ButtonRemoveAll": "Eemalda kõik",
|
||||||
|
"ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed",
|
||||||
|
"ButtonRemoveFromContinueListening": "Eemalda jätkake kuulamisest",
|
||||||
|
"ButtonRemoveFromContinueReading": "Eemalda jätkake lugemisest",
|
||||||
|
"ButtonRemoveSeriesFromContinueSeries": "Eemalda seeria jätkamisest",
|
||||||
|
"ButtonReScan": "Uuestiskaneeri",
|
||||||
|
"ButtonReset": "Lähtesta",
|
||||||
|
"ButtonResetToDefault": "Lähtesta vaikeseade",
|
||||||
|
"ButtonRestore": "Taasta",
|
||||||
|
"ButtonSave": "Salvesta",
|
||||||
|
"ButtonSaveAndClose": "Salvesta ja sulge",
|
||||||
|
"ButtonSaveTracklist": "Salvesta lugude loend",
|
||||||
|
"ButtonScan": "Skanneeri",
|
||||||
|
"ButtonScanLibrary": "Skanneeri raamatukogu",
|
||||||
|
"ButtonSearch": "Otsi",
|
||||||
|
"ButtonSelectFolderPath": "Vali kaustatee",
|
||||||
|
"ButtonSeries": "Sarjad",
|
||||||
|
"ButtonSetChaptersFromTracks": "Määra peatükid lugudest",
|
||||||
|
"ButtonShare": "Share",
|
||||||
|
"ButtonShiftTimes": "Nihke ajad",
|
||||||
|
"ButtonShow": "Näita",
|
||||||
|
"ButtonStartM4BEncode": "Alusta M4B kodeerimist",
|
||||||
|
"ButtonStartMetadataEmbed": "Alusta metaandmete lisamist",
|
||||||
|
"ButtonSubmit": "Esita",
|
||||||
|
"ButtonTest": "Test",
|
||||||
|
"ButtonUpload": "Lae üles",
|
||||||
|
"ButtonUploadBackup": "Lae üles varundus",
|
||||||
|
"ButtonUploadCover": "Lae üles ümbris",
|
||||||
|
"ButtonUploadOPMLFile": "Lae üles OPML-fail",
|
||||||
|
"ButtonUserDelete": "Kustuta kasutaja {0}",
|
||||||
|
"ButtonUserEdit": "Muuda kasutajat {0}",
|
||||||
|
"ButtonViewAll": "Vaata kõiki",
|
||||||
|
"ButtonYes": "Jah",
|
||||||
|
"ErrorUploadFetchMetadataAPI": "Viga metaandmete hankimisel",
|
||||||
|
"ErrorUploadFetchMetadataNoResults": "Ei saanud metaandmeid hankida - proovi tiitlit ja/või autorit uuendada",
|
||||||
|
"ErrorUploadLacksTitle": "Peab olema pealkiri",
|
||||||
|
"HeaderAccount": "Konto",
|
||||||
|
"HeaderAdvanced": "Täpsem",
|
||||||
|
"HeaderAppriseNotificationSettings": "Apprise teavitamise seaded",
|
||||||
|
"HeaderAudiobookTools": "Heliraamatu failihaldustööriistad",
|
||||||
|
"HeaderAudioTracks": "Helirajad",
|
||||||
|
"HeaderAuthentication": "Autentimine",
|
||||||
|
"HeaderBackups": "Varukoopiad",
|
||||||
|
"HeaderChangePassword": "Muuda parooli",
|
||||||
|
"HeaderChapters": "Peatükid",
|
||||||
|
"HeaderChooseAFolder": "Vali kaust",
|
||||||
|
"HeaderCollection": "Kogu",
|
||||||
|
"HeaderCollectionItems": "Kogu esemed",
|
||||||
|
"HeaderCover": "Ümbris",
|
||||||
|
"HeaderCurrentDownloads": "Praegused allalaadimised",
|
||||||
|
"HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad",
|
||||||
|
"HeaderDetails": "Detailid",
|
||||||
|
"HeaderDownloadQueue": "Allalaadimise järjekord",
|
||||||
|
"HeaderEbookFiles": "E-raamatute failid",
|
||||||
|
"HeaderEmail": "E-post",
|
||||||
|
"HeaderEmailSettings": "E-posti seaded",
|
||||||
|
"HeaderEpisodes": "Episoodid",
|
||||||
|
"HeaderEreaderDevices": "E-lugerite seadmed",
|
||||||
|
"HeaderEreaderSettings": "E-lugerite seadistused",
|
||||||
|
"HeaderFiles": "Failid",
|
||||||
|
"HeaderFindChapters": "Leia peatükid",
|
||||||
|
"HeaderIgnoredFiles": "Ignoreeritud failid",
|
||||||
|
"HeaderItemFiles": "Esemete failid",
|
||||||
|
"HeaderItemMetadataUtils": "Eseme metaandmete tööriistad",
|
||||||
|
"HeaderLastListeningSession": "Viimane kuulamissessioon",
|
||||||
|
"HeaderLatestEpisodes": "Viimased episoodid",
|
||||||
|
"HeaderLibraries": "Raamatukogud",
|
||||||
|
"HeaderLibraryFiles": "Raamatukogu failid",
|
||||||
|
"HeaderLibraryStats": "Raamatukogu statistika",
|
||||||
|
"HeaderListeningSessions": "Kuulamissessioonid",
|
||||||
|
"HeaderListeningStats": "Kuulamise statistika",
|
||||||
|
"HeaderLogin": "Logi sisse",
|
||||||
|
"HeaderLogs": "Logid",
|
||||||
|
"HeaderManageGenres": "Halda žanre",
|
||||||
|
"HeaderManageTags": "Halda silte",
|
||||||
|
"HeaderMapDetails": "Kaardi detailid",
|
||||||
|
"HeaderMatch": "Sobita",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Metaandmete eelnevusjärjestus",
|
||||||
|
"HeaderMetadataToEmbed": "Manusta metaandmed",
|
||||||
|
"HeaderNewAccount": "Uus konto",
|
||||||
|
"HeaderNewLibrary": "Uus raamatukogu",
|
||||||
|
"HeaderNotifications": "Teatised",
|
||||||
|
"HeaderOpenIDConnectAuthentication": "OpenID Connect autentimine",
|
||||||
|
"HeaderOpenRSSFeed": "Ava RSS-voog",
|
||||||
|
"HeaderOtherFiles": "Muud failid",
|
||||||
|
"HeaderPasswordAuthentication": "Parooli autentimine",
|
||||||
|
"HeaderPermissions": "Õigused",
|
||||||
|
"HeaderPlayerQueue": "Mängija järjekord",
|
||||||
|
"HeaderPlaylist": "Mänguloend",
|
||||||
|
"HeaderPlaylistItems": "Mänguloendi esemed",
|
||||||
|
"HeaderPodcastsToAdd": "Lisatavad podcastid",
|
||||||
|
"HeaderPreviewCover": "Eelvaate kaas",
|
||||||
|
"HeaderRemoveEpisode": "Eemalda episood",
|
||||||
|
"HeaderRemoveEpisodes": "Eemalda {0} episoodi",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS-i üksikasjad",
|
||||||
|
"HeaderRSSFeedIsOpen": "RSS-voog on avatud",
|
||||||
|
"HeaderRSSFeeds": "RSS-vooged",
|
||||||
|
"HeaderSavedMediaProgress": "Salvestatud meedia edenemine",
|
||||||
|
"HeaderSchedule": "Ajakava",
|
||||||
|
"HeaderScheduleLibraryScans": "Ajasta automaatsed raamatukogu skaneerimised",
|
||||||
|
"HeaderSession": "Sessioon",
|
||||||
|
"HeaderSetBackupSchedule": "Määra varunduse ajakava",
|
||||||
|
"HeaderSettings": "Seaded",
|
||||||
|
"HeaderSettingsDisplay": "Kuva",
|
||||||
|
"HeaderSettingsExperimental": "Katsetusfunktsioonid",
|
||||||
|
"HeaderSettingsGeneral": "Üldised",
|
||||||
|
"HeaderSettingsScanner": "Skanner",
|
||||||
|
"HeaderSleepTimer": "Uinaku taimer",
|
||||||
|
"HeaderStatsLargestItems": "Suurimad esemed",
|
||||||
|
"HeaderStatsLongestItems": "Kõige pikemad esemed (tunnid)",
|
||||||
|
"HeaderStatsMinutesListeningChart": "Kuulamise minutid (viimased 7 päeva)",
|
||||||
|
"HeaderStatsRecentSessions": "Hiljutised sessioonid",
|
||||||
|
"HeaderStatsTop10Authors": "Top 10 autorit",
|
||||||
|
"HeaderStatsTop5Genres": "Top 5 žanrit",
|
||||||
|
"HeaderTableOfContents": "Sisukord",
|
||||||
|
"HeaderTools": "Tööriistad",
|
||||||
|
"HeaderUpdateAccount": "Uuenda kontot",
|
||||||
|
"HeaderUpdateAuthor": "Uuenda autorit",
|
||||||
|
"HeaderUpdateDetails": "Uuenda detaile",
|
||||||
|
"HeaderUpdateLibrary": "Uuenda raamatukogu",
|
||||||
|
"HeaderUsers": "Kasutajad",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
|
"HeaderYourStats": "Sinu statistika",
|
||||||
|
"LabelAbridged": "Kärbitud",
|
||||||
|
"LabelAccountType": "Konto tüüp",
|
||||||
|
"LabelAccountTypeAdmin": "Administraator",
|
||||||
|
"LabelAccountTypeGuest": "Külaline",
|
||||||
|
"LabelAccountTypeUser": "Kasutaja",
|
||||||
|
"LabelActivity": "Tegevus",
|
||||||
|
"LabelAdded": "Lisatud",
|
||||||
|
"LabelAddedAt": "Lisatud",
|
||||||
|
"LabelAddToCollection": "Lisa kogusse",
|
||||||
|
"LabelAddToCollectionBatch": "Lisa {0} raamatut kogusse",
|
||||||
|
"LabelAddToPlaylist": "Lisa mänguloendisse",
|
||||||
|
"LabelAddToPlaylistBatch": "Lisa {0} eset mänguloendisse",
|
||||||
|
"LabelAdminUsersOnly": "Ainult administraatorid",
|
||||||
|
"LabelAll": "Kõik",
|
||||||
|
"LabelAllUsers": "Kõik kasutajad",
|
||||||
|
"LabelAllUsersExcludingGuests": "Kõik kasutajad, välja arvatud külalised",
|
||||||
|
"LabelAllUsersIncludingGuests": "Kõik kasutajad, kaasa arvatud külalised",
|
||||||
|
"LabelAlreadyInYourLibrary": "Juba teie raamatukogus",
|
||||||
|
"LabelAppend": "Lisa",
|
||||||
|
"LabelAuthor": "Autor",
|
||||||
|
"LabelAuthorFirstLast": "Autor (Eesnimi Perekonnanimi)",
|
||||||
|
"LabelAuthorLastFirst": "Autor (Perekonnanimi, Eesnimi)",
|
||||||
|
"LabelAuthors": "Autorid",
|
||||||
|
"LabelAutoDownloadEpisodes": "Automaatne episoodide allalaadimine",
|
||||||
|
"LabelAutoFetchMetadata": "Automaatne metaandmete hankimine",
|
||||||
|
"LabelAutoFetchMetadataHelp": "Toob tiitli, autori ja seeria metaandmed üleslaadimise hõlbustamiseks. Lisametaandmed võivad pärast üleslaadimist vajada vastavust.",
|
||||||
|
"LabelAutoLaunch": "Automaatne käivitamine",
|
||||||
|
"LabelAutoLaunchDescription": "Suunab automaatselt autentimist pakkuvale teenusele, kui navigeeritakse sisselogimislehele (käsitsi ülekirjutuse tee <code>/login?autoLaunch=0</code>)",
|
||||||
|
"LabelAutoRegister": "Automaatne registreerimine",
|
||||||
|
"LabelAutoRegisterDescription": "Loo uued kasutajad automaatselt sisselogimisel",
|
||||||
|
"LabelBackToUser": "Tagasi kasutajale",
|
||||||
|
"LabelBackupLocation": "Varukoopia asukoht",
|
||||||
|
"LabelBackupsEnableAutomaticBackups": "Luba automaatsed varukoopiad",
|
||||||
|
"LabelBackupsEnableAutomaticBackupsHelp": "Varukoopiad salvestatakse /metadata/backups kausta",
|
||||||
|
"LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des)",
|
||||||
|
"LabelBackupsMaxBackupSizeHelp": "Kaitsena valesti seadistamise vastu ebaõnnestuvad varukoopiad, kui need ületavad seadistatud suuruse.",
|
||||||
|
"LabelBackupsNumberToKeep": "Varukoopiate arv, mida hoida",
|
||||||
|
"LabelBackupsNumberToKeepHelp": "Ühel ajal eemaldatakse ainult 1 varukoopia, seega kui teil on juba rohkem varukoopiaid kui siin määratud, peaksite need käsitsi eemaldama.",
|
||||||
|
"LabelBitrate": "Bittkiirus",
|
||||||
|
"LabelBooks": "Raamatud",
|
||||||
|
"LabelButtonText": "Nupu tekst",
|
||||||
|
"LabelChangePassword": "Muuda parooli",
|
||||||
|
"LabelChannels": "Kanalid",
|
||||||
|
"LabelChapters": "Peatükid",
|
||||||
|
"LabelChaptersFound": "peatükid leitud",
|
||||||
|
"LabelChapterTitle": "Peatüki pealkiri",
|
||||||
|
"LabelClickForMoreInfo": "Klõpsa lisateabe saamiseks",
|
||||||
|
"LabelClosePlayer": "Sulge mängija",
|
||||||
|
"LabelCodec": "Kodek",
|
||||||
|
"LabelCollapseSeries": "Ahenda seeria",
|
||||||
|
"LabelCollection": "Kogu",
|
||||||
|
"LabelCollections": "Kogud",
|
||||||
|
"LabelComplete": "Valmis",
|
||||||
|
"LabelConfirmPassword": "Kinnita parool",
|
||||||
|
"LabelContinueListening": "Jätka kuulamist",
|
||||||
|
"LabelContinueReading": "Jätka lugemist",
|
||||||
|
"LabelContinueSeries": "Jätka seeriat",
|
||||||
|
"LabelCover": "Ümbris",
|
||||||
|
"LabelCoverImageURL": "Ümbrise pildi URL",
|
||||||
|
"LabelCreatedAt": "Loodud",
|
||||||
|
"LabelCronExpression": "Croni valem",
|
||||||
|
"LabelCurrent": "Praegune",
|
||||||
|
"LabelCurrently": "Praegu:",
|
||||||
|
"LabelCustomCronExpression": "Kohandatud Croni valem:",
|
||||||
|
"LabelDatetime": "Kuupäev ja kellaaeg",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Kustuta failisüsteemist (ärge märkige seda ära, et eemaldada ainult andmebaasist)",
|
||||||
|
"LabelDescription": "Kirjeldus",
|
||||||
|
"LabelDeselectAll": "Tühista kõigi valimine",
|
||||||
|
"LabelDevice": "Seade",
|
||||||
|
"LabelDeviceInfo": "Seadme info",
|
||||||
|
"LabelDeviceIsAvailableTo": "Seade on saadaval kasutajale...",
|
||||||
|
"LabelDirectory": "Kataloog",
|
||||||
|
"LabelDiscFromFilename": "Ketas failinimest",
|
||||||
|
"LabelDiscFromMetadata": "Ketas metaandmetest",
|
||||||
|
"LabelDiscover": "Avasta",
|
||||||
|
"LabelDownload": "Lae alla",
|
||||||
|
"LabelDownloadNEpisodes": "Lae alla {0} episoodi",
|
||||||
|
"LabelDuration": "Kestus",
|
||||||
|
"LabelDurationFound": "Leitud kestus:",
|
||||||
|
"LabelEbook": "E-raamat",
|
||||||
|
"LabelEbooks": "E-raamatud",
|
||||||
|
"LabelEdit": "Muuda",
|
||||||
|
"LabelEmail": "E-post",
|
||||||
|
"LabelEmailSettingsFromAddress": "Saatja aadress",
|
||||||
|
"LabelEmailSettingsSecure": "Turvaline",
|
||||||
|
"LabelEmailSettingsSecureHelp": "Kui see on tõene, kasutab ühendus serveriga ühenduse loomisel TLS-i. Kui see on väär, kasutatakse TLS-i, kui server toetab STARTTLS-i laiendust. Enamikul juhtudest seadke see väärtus tõeks, kui ühendate pordile 465. Pordi 587 või 25 korral hoidke seda väär. (nodemailer.com/smtp/#authentication)",
|
||||||
|
"LabelEmailSettingsTestAddress": "Testi aadress",
|
||||||
|
"LabelEmbeddedCover": "Manustatud kaas",
|
||||||
|
"LabelEnable": "Luba",
|
||||||
|
"LabelEnd": "Lõpp",
|
||||||
|
"LabelEpisode": "Episood",
|
||||||
|
"LabelEpisodeTitle": "Episoodi pealkiri",
|
||||||
|
"LabelEpisodeType": "Episoodi tüüp",
|
||||||
|
"LabelExample": "Näide",
|
||||||
|
"LabelExplicit": "Vulgaarne",
|
||||||
|
"LabelFeedURL": "Voogu URL",
|
||||||
|
"LabelFetchingMetadata": "Metaandmete hankimine",
|
||||||
|
"LabelFile": "Fail",
|
||||||
|
"LabelFileBirthtime": "Faili sünniaeg",
|
||||||
|
"LabelFileModified": "Faili muudetud",
|
||||||
|
"LabelFilename": "Failinimi",
|
||||||
|
"LabelFilterByUser": "Filtri alusel kasutaja järgi",
|
||||||
|
"LabelFindEpisodes": "Otsi episoodid",
|
||||||
|
"LabelFinished": "Lõpetatud",
|
||||||
|
"LabelFolder": "Kaust",
|
||||||
|
"LabelFolders": "Kataloogid",
|
||||||
|
"LabelFontBold": "Paks",
|
||||||
|
"LabelFontFamily": "Fondi pere",
|
||||||
|
"LabelFontItalic": "Kaldkiri",
|
||||||
|
"LabelFontScale": "Fondi suurus",
|
||||||
|
"LabelFontStrikethrough": "Üle joonitud",
|
||||||
|
"LabelFormat": "Vorming",
|
||||||
|
"LabelGenre": "Žanr",
|
||||||
|
"LabelGenres": "Žanrid",
|
||||||
|
"LabelHardDeleteFile": "Faili lõplik kustutamine",
|
||||||
|
"LabelHasEbook": "On e-raamat",
|
||||||
|
"LabelHasSupplementaryEbook": "On täiendav e-raamat",
|
||||||
|
"LabelHighestPriority": "Kõrgeim prioriteet",
|
||||||
|
"LabelHost": "Host",
|
||||||
|
"LabelHour": "Tund",
|
||||||
|
"LabelIcon": "Ikoon",
|
||||||
|
"LabelImageURLFromTheWeb": "Pildi URL veebist",
|
||||||
|
"LabelIncludeInTracklist": "Kaasa jälgimisloendis",
|
||||||
|
"LabelIncomplete": "Puudulik",
|
||||||
|
"LabelInProgress": "Pooleli",
|
||||||
|
"LabelInterval": "Intervall",
|
||||||
|
"LabelIntervalCustomDailyWeekly": "Kohandatud päevane/nädalane",
|
||||||
|
"LabelIntervalEvery12Hours": "Iga 12 tunni tagant",
|
||||||
|
"LabelIntervalEvery15Minutes": "Iga 15 minuti tagant",
|
||||||
|
"LabelIntervalEvery2Hours": "Iga 2 tunni tagant",
|
||||||
|
"LabelIntervalEvery30Minutes": "Iga 30 minuti tagant",
|
||||||
|
"LabelIntervalEvery6Hours": "Iga 6 tunni tagant",
|
||||||
|
"LabelIntervalEveryDay": "Iga päev",
|
||||||
|
"LabelIntervalEveryHour": "Iga tunni tagant",
|
||||||
|
"LabelInvalidParts": "Vigased osad",
|
||||||
|
"LabelInvert": "Pööra ümber",
|
||||||
|
"LabelItem": "Kirje",
|
||||||
|
"LabelLanguage": "Keel",
|
||||||
|
"LabelLanguageDefaultServer": "Vaikeserveri keel",
|
||||||
|
"LabelLastBookAdded": "Viimati lisatud raamat",
|
||||||
|
"LabelLastBookUpdated": "Viimati uuendatud raamat",
|
||||||
|
"LabelLastSeen": "Viimati nähtud",
|
||||||
|
"LabelLastTime": "Viimati aeg",
|
||||||
|
"LabelLastUpdate": "Viimane uuendus",
|
||||||
|
"LabelLayout": "Paigutus",
|
||||||
|
"LabelLayoutSinglePage": "Üks lehekülg",
|
||||||
|
"LabelLayoutSplitPage": "Jagatud lehekülg",
|
||||||
|
"LabelLess": "Vähem",
|
||||||
|
"LabelLibrariesAccessibleToUser": "Kasutajale ligipääsetavad raamatukogud",
|
||||||
|
"LabelLibrary": "Raamatukogu",
|
||||||
|
"LabelLibraryItem": "Raamatukogu kirje",
|
||||||
|
"LabelLibraryName": "Raamatukogu nimi",
|
||||||
|
"LabelLimit": "Piirang",
|
||||||
|
"LabelLineSpacing": "Joonevahe",
|
||||||
|
"LabelListenAgain": "Kuula uuesti",
|
||||||
|
"LabelLogLevelDebug": "Silumine",
|
||||||
|
"LabelLogLevelInfo": "Teave",
|
||||||
|
"LabelLogLevelWarn": "Hoiatus",
|
||||||
|
"LabelLookForNewEpisodesAfterDate": "Otsi uusi episoodid pärast seda kuupäeva",
|
||||||
|
"LabelLowestPriority": "Madalaim prioriteet",
|
||||||
|
"LabelMatchExistingUsersBy": "Sobita olemasolevad kasutajad",
|
||||||
|
"LabelMatchExistingUsersByDescription": "Kasutatakse olemasolevate kasutajate ühendamiseks. Ühendatud kasutajaid sobitatakse teie SSO pakkuja unikaalse ID järgi.",
|
||||||
|
"LabelMediaPlayer": "Meediapleier",
|
||||||
|
"LabelMediaType": "Meedia tüüp",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "Kõrgema prioriteediga metaandmete allikad võtavad üle madalama prioriteediga metaandmete allikad",
|
||||||
|
"LabelMetadataProvider": "Metaandmete pakkuja",
|
||||||
|
"LabelMetaTag": "Meta märge",
|
||||||
|
"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",
|
||||||
|
"LabelMoreInfo": "Rohkem infot",
|
||||||
|
"LabelName": "Nimi",
|
||||||
|
"LabelNarrator": "Jutustaja",
|
||||||
|
"LabelNarrators": "Jutustajad",
|
||||||
|
"LabelNew": "Uus",
|
||||||
|
"LabelNewestAuthors": "Uusimad autorid",
|
||||||
|
"LabelNewestEpisodes": "Uusimad episoodid",
|
||||||
|
"LabelNewPassword": "Uus parool",
|
||||||
|
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
|
||||||
|
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
|
||||||
|
"LabelNoEpisodesSelected": "Episoodid pole valitud",
|
||||||
|
"LabelNotes": "Märkused",
|
||||||
|
"LabelNotFinished": "Ei ole lõpetatud",
|
||||||
|
"LabelNotificationAppriseURL": "Apprise URL-id",
|
||||||
|
"LabelNotificationAvailableVariables": "Saadaolevad muutujad",
|
||||||
|
"LabelNotificationBodyTemplate": "Keha mall",
|
||||||
|
"LabelNotificationEvent": "Teavituse sündmus",
|
||||||
|
"LabelNotificationsMaxFailedAttempts": "Maksimaalsed ebaõnnestunud katsed",
|
||||||
|
"LabelNotificationsMaxFailedAttemptsHelp": "Teatised keelatakse, kui need ebaõnnestuvad nii palju kordi",
|
||||||
|
"LabelNotificationsMaxQueueSize": "Teavituste sündmuste maksimaalne järjekorra suurus",
|
||||||
|
"LabelNotificationsMaxQueueSizeHelp": "Sündmused on piiratud 1 sekundiga. Sündmusi ignoreeritakse, kui järjekord on maksimumsuuruses. See takistab teavituste rämpsposti.",
|
||||||
|
"LabelNotificationTitleTemplate": "Pealkirja mall",
|
||||||
|
"LabelNotStarted": "Pole alustatud",
|
||||||
|
"LabelNumberOfBooks": "Raamatute arv",
|
||||||
|
"LabelNumberOfEpisodes": "Episoodide arv",
|
||||||
|
"LabelOpenRSSFeed": "Ava RSS voog",
|
||||||
|
"LabelOverwrite": "Kirjuta üle",
|
||||||
|
"LabelPassword": "Parool",
|
||||||
|
"LabelPath": "Asukoht",
|
||||||
|
"LabelPermissionsAccessAllLibraries": "Saab ligi kõikidele raamatukogudele",
|
||||||
|
"LabelPermissionsAccessAllTags": "Saab ligi kõikidele siltidele",
|
||||||
|
"LabelPermissionsAccessExplicitContent": "Saab ligi vulgaarsele sisule",
|
||||||
|
"LabelPermissionsDelete": "Saab kustutada",
|
||||||
|
"LabelPermissionsDownload": "Saab alla laadida",
|
||||||
|
"LabelPermissionsUpdate": "Saab uuendada",
|
||||||
|
"LabelPermissionsUpload": "Saab üles laadida",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
|
"LabelPhotoPathURL": "Foto tee/URL",
|
||||||
|
"LabelPlaylists": "Mänguloendid",
|
||||||
|
"LabelPlayMethod": "Esitusmeetod",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
|
"LabelPodcasts": "Podcastid",
|
||||||
|
"LabelPodcastSearchRegion": "Podcasti otsingu piirkond",
|
||||||
|
"LabelPodcastType": "Podcasti tüüp",
|
||||||
|
"LabelPort": "Port",
|
||||||
|
"LabelPrefixesToIgnore": "Eiramiseks eesliited (tõstutundetu)",
|
||||||
|
"LabelPreventIndexing": "Vältige oma voogu indekseerimist iTunes'i ja Google podcasti kataloogides",
|
||||||
|
"LabelPrimaryEbook": "Esmane e-raamat",
|
||||||
|
"LabelProgress": "Edenemine",
|
||||||
|
"LabelProvider": "Pakkuja",
|
||||||
|
"LabelPubDate": "Avaldamise kuupäev",
|
||||||
|
"LabelPublisher": "Kirjastaja",
|
||||||
|
"LabelPublishYear": "Aasta avaldamine",
|
||||||
|
"LabelRead": "Lugenud",
|
||||||
|
"LabelReadAgain": "Loe uuesti",
|
||||||
|
"LabelReadEbookWithoutProgress": "Lugege e-raamatut ilma edenemist säilitamata",
|
||||||
|
"LabelRecentlyAdded": "Hiljuti lisatud",
|
||||||
|
"LabelRecentSeries": "Hiljutised seeriad",
|
||||||
|
"LabelRecommended": "Soovitatud",
|
||||||
|
"LabelRedo": "Tee uuesti",
|
||||||
|
"LabelRegion": "Piirkond",
|
||||||
|
"LabelReleaseDate": "Väljalaske kuupäev",
|
||||||
|
"LabelRemoveCover": "Eemalda ümbris",
|
||||||
|
"LabelRowsPerPage": "Rida lehe kohta",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Kohandatud omaniku e-post",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Kohandatud omaniku nimi",
|
||||||
|
"LabelRSSFeedOpen": "Ava RSS voog",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Vältige indekseerimist",
|
||||||
|
"LabelRSSFeedSlug": "RSS voog Slug",
|
||||||
|
"LabelRSSFeedURL": "RSS voog URL",
|
||||||
|
"LabelSearchTerm": "Otsingutermin",
|
||||||
|
"LabelSearchTitle": "Otsi pealkirja",
|
||||||
|
"LabelSearchTitleOrASIN": "Otsi pealkirja või ASIN-i",
|
||||||
|
"LabelSeason": "Hooaeg",
|
||||||
|
"LabelSelectAllEpisodes": "Vali kõik episoodid",
|
||||||
|
"LabelSelectEpisodesShowing": "Valige {0} näidatavat episoodi",
|
||||||
|
"LabelSelectUsers": "Valige kasutajad",
|
||||||
|
"LabelSendEbookToDevice": "Saada e-raamat seadmele...",
|
||||||
|
"LabelSequence": "Järjestus",
|
||||||
|
"LabelSeries": "Seeria",
|
||||||
|
"LabelSeriesName": "Seeria nimi",
|
||||||
|
"LabelSeriesProgress": "Seeria edenemine",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
|
"LabelSetEbookAsPrimary": "Määra peamiseks",
|
||||||
|
"LabelSetEbookAsSupplementary": "Määra täiendavaks",
|
||||||
|
"LabelSettingsAudiobooksOnly": "Ainult heliraamatud",
|
||||||
|
"LabelSettingsAudiobooksOnlyHelp": "Selle seadistuse lubamine eirab e-raamatute faile, välja arvatud juhul, kui need on heliraamatu kaustas, kus need seatakse täiendavate e-raamatutena",
|
||||||
|
"LabelSettingsBookshelfViewHelp": "Skeumorfne kujundus puidust riiulitega",
|
||||||
|
"LabelSettingsChromecastSupport": "Chromecasti tugi",
|
||||||
|
"LabelSettingsDateFormat": "Kuupäeva vorming",
|
||||||
|
"LabelSettingsDisableWatcher": "Keela vaatamine",
|
||||||
|
"LabelSettingsDisableWatcherForLibrary": "Keela kaustavaatamine raamatukogu jaoks",
|
||||||
|
"LabelSettingsDisableWatcherHelp": "Keelab automaatse lisamise/uuendamise, kui failimuudatusi tuvastatakse. *Nõuab serveri taaskäivitamist",
|
||||||
|
"LabelSettingsEnableWatcher": "Luba vaatamine",
|
||||||
|
"LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks",
|
||||||
|
"LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
|
||||||
|
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
|
||||||
|
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",
|
||||||
|
"LabelSettingsFindCovers": "Leia ümbrised",
|
||||||
|
"LabelSettingsFindCoversHelp": "Kui teie heliraamatul pole sisseehitatud ümbrist ega ümbrise pilti kaustas, proovib skanner leida ümbrist.<br>Märkus: see pikendab skaneerimisaega",
|
||||||
|
"LabelSettingsHideSingleBookSeries": "Peida üksikute raamatute seeriad",
|
||||||
|
"LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.",
|
||||||
|
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
|
||||||
|
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
|
||||||
|
"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",
|
||||||
|
"LabelSettingsPreferMatchedMetadataHelp": "Sobitatud andmed kirjutavad Kiir Sobitamise kasutamisel üle üksikasjad.",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithASIN": "Jätke ASIN-iga sobituvad raamatud vahele",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithISBN": "Jätke ISBN-iga sobituvad raamatud vahele",
|
||||||
|
"LabelSettingsSortingIgnorePrefixes": "Ignoreeri eesliiteid sortimisel",
|
||||||
|
"LabelSettingsSortingIgnorePrefixesHelp": "nt. eesliidet \"the\" kasutades raamatu pealkiri \"The Book Title\" sorteeritakse \"Book Title, The\"",
|
||||||
|
"LabelSettingsSquareBookCovers": "Kasutage ruudukujulisi raamatu kaasi",
|
||||||
|
"LabelSettingsSquareBookCoversHelp": "Eelistage ruudukujulisi kaasi tavaliste 1.6:1 raamatu ümbrise asemel",
|
||||||
|
"LabelSettingsStoreCoversWithItem": "Salvesta kaaned üksusega",
|
||||||
|
"LabelSettingsStoreCoversWithItemHelp": "Vaikimisi salvestatakse kaaned /metadata/items kausta. Selle seadistuse lubamine salvestab kaaned teie raamatukogu üksuse kausta. Hoitakse ainult ühte faili nimega \"kaas\"",
|
||||||
|
"LabelSettingsStoreMetadataWithItem": "Salvesta metaandmed üksusega",
|
||||||
|
"LabelSettingsStoreMetadataWithItemHelp": "Vaikimisi salvestatakse metaandmed /metadata/items kausta. Selle seadistuse lubamine salvestab metaandmed teie raamatukogu üksuse kaustadesse",
|
||||||
|
"LabelSettingsTimeFormat": "Kellaaja vorming",
|
||||||
|
"LabelShowAll": "Näita kõiki",
|
||||||
|
"LabelSize": "Suurus",
|
||||||
|
"LabelSleepTimer": "Uinaku taimer",
|
||||||
|
"LabelSlug": "Slug",
|
||||||
|
"LabelStart": "Alusta",
|
||||||
|
"LabelStarted": "Alustatud",
|
||||||
|
"LabelStartedAt": "Alustatud",
|
||||||
|
"LabelStartTime": "Alustamise aeg",
|
||||||
|
"LabelStatsAudioTracks": "Audiojäljed",
|
||||||
|
"LabelStatsAuthors": "Autorid",
|
||||||
|
"LabelStatsBestDay": "Parim päev",
|
||||||
|
"LabelStatsDailyAverage": "Päevane keskmine",
|
||||||
|
"LabelStatsDays": "Päevad",
|
||||||
|
"LabelStatsDaysListened": "Kuulatud päevad",
|
||||||
|
"LabelStatsHours": "Tunnid",
|
||||||
|
"LabelStatsInARow": "järjest",
|
||||||
|
"LabelStatsItemsFinished": "Lõpetatud üksused",
|
||||||
|
"LabelStatsItemsInLibrary": "Üksused raamatukogus",
|
||||||
|
"LabelStatsMinutes": "minutit",
|
||||||
|
"LabelStatsMinutesListening": "Kuulamise minutid",
|
||||||
|
"LabelStatsOverallDays": "Kokku päevad",
|
||||||
|
"LabelStatsOverallHours": "Kokku tunnid",
|
||||||
|
"LabelStatsWeekListening": "Nädala kuulamine",
|
||||||
|
"LabelSubtitle": "Alapealkiri",
|
||||||
|
"LabelSupportedFileTypes": "Toetatud failitüübid",
|
||||||
|
"LabelTag": "Silt",
|
||||||
|
"LabelTags": "Sildid",
|
||||||
|
"LabelTagsAccessibleToUser": "Kasutajale kättesaadavad sildid",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Kasutajale mittekättesaadavad sildid",
|
||||||
|
"LabelTasks": "Käimasolevad ülesanded",
|
||||||
|
"LabelTextEditorBulletedList": "Punktloend",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numberloend",
|
||||||
|
"LabelTextEditorUnlink": "Eemalda link",
|
||||||
|
"LabelTheme": "Teema",
|
||||||
|
"LabelThemeDark": "Tume",
|
||||||
|
"LabelThemeLight": "Hele",
|
||||||
|
"LabelTimeBase": "Aja alus",
|
||||||
|
"LabelTimeListened": "Kuulatud aeg",
|
||||||
|
"LabelTimeListenedToday": "Täna kuulatud aeg",
|
||||||
|
"LabelTimeRemaining": "{0} jäänud",
|
||||||
|
"LabelTimeToShift": "Nihutamiseks sekundites kuluv aeg",
|
||||||
|
"LabelTitle": "Pealkiri",
|
||||||
|
"LabelToolsEmbedMetadata": "Manusta metaandmed",
|
||||||
|
"LabelToolsEmbedMetadataDescription": "Manusta metaandmed helifailidesse, sealhulgas kaanepilt ja peatükid.",
|
||||||
|
"LabelToolsMakeM4b": "Loo M4B heliraamatu fail",
|
||||||
|
"LabelToolsMakeM4bDescription": "Loo .M4B heliraamatu fail, kuhu on manustatud metaandmed, kaanepilt ja peatükid.",
|
||||||
|
"LabelToolsSplitM4b": "Jaga M4B MP3-deks",
|
||||||
|
"LabelToolsSplitM4bDescription": "Loo MP3-d M4B-st peatükkide kaupa, kus on manustatud metaandmed, kaanepilt ja peatükid.",
|
||||||
|
"LabelTotalDuration": "Kogukestus",
|
||||||
|
"LabelTotalTimeListened": "Kogu kuulatud aeg",
|
||||||
|
"LabelTrackFromFilename": "Jälg nimest",
|
||||||
|
"LabelTrackFromMetadata": "Jälg metaandmetest",
|
||||||
|
"LabelTracks": "Jäljed",
|
||||||
|
"LabelTracksMultiTrack": "Mitmejälg",
|
||||||
|
"LabelTracksNone": "Ühtegi jälgimist",
|
||||||
|
"LabelTracksSingleTrack": "Üksikjälg",
|
||||||
|
"LabelType": "Tüüp",
|
||||||
|
"LabelUnabridged": "Täismahus",
|
||||||
|
"LabelUndo": "Võta tagasi",
|
||||||
|
"LabelUnknown": "Tundmatu",
|
||||||
|
"LabelUpdateCover": "Uuenda kaant",
|
||||||
|
"LabelUpdateCoverHelp": "Luba üle kirjutamine olemasolevate kaante jaoks valitud raamatutele, kui leitakse sobivus",
|
||||||
|
"LabelUpdatedAt": "Uuendatud",
|
||||||
|
"LabelUpdateDetails": "Uuenda üksikasju",
|
||||||
|
"LabelUpdateDetailsHelp": "Luba üle kirjutamine olemasolevate üksikasjade jaoks valitud raamatutele, kui leitakse sobivus",
|
||||||
|
"LabelUploaderDragAndDrop": "Lohista ja aseta faile või kaustu",
|
||||||
|
"LabelUploaderDropFiles": "Aseta failid",
|
||||||
|
"LabelUploaderItemFetchMetadataHelp": "Hangi automaatselt pealkiri, autor ja seeria",
|
||||||
|
"LabelUseChapterTrack": "Kasuta peatüki jälge",
|
||||||
|
"LabelUseFullTrack": "Kasuta täielikku jälge",
|
||||||
|
"LabelUser": "Kasutaja",
|
||||||
|
"LabelUsername": "Kasutajanimi",
|
||||||
|
"LabelValue": "Väärtus",
|
||||||
|
"LabelVersion": "Versioon",
|
||||||
|
"LabelViewBookmarks": "Vaata järjehoidjaid",
|
||||||
|
"LabelViewChapters": "Vaata peatükke",
|
||||||
|
"LabelViewQueue": "Vaata esitusjärjekorda",
|
||||||
|
"LabelVolume": "Heli tugevus",
|
||||||
|
"LabelWeekdaysToRun": "Päevad nädalas käivitamiseks",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
|
"LabelYourAudiobookDuration": "Teie heliraamatu kestus",
|
||||||
|
"LabelYourBookmarks": "Teie järjehoidjad",
|
||||||
|
"LabelYourPlaylists": "Teie esitusloendid",
|
||||||
|
"LabelYourProgress": "Teie edenemine",
|
||||||
|
"MessageAddToPlayerQueue": "Lisa esitusjärjekorda",
|
||||||
|
"MessageAppriseDescription": "Selle funktsiooni kasutamiseks peate käivitama <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> eksemplari või API, mis töötleb samu päringuid. <br />Apprise API URL peaks olema täielik URL-rada teatise saatmiseks, näiteks kui teie API eksemplar töötab aadressil <code>http://192.168.1.1:8337</code>, siis peaksite sisestama <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
|
"MessageBackupsDescription": "Varukoopiad hõlmavad kasutajaid, kasutajate edenemist, raamatukogu üksikasju, serveri seadeid ja kaustades <code>/metadata/items</code> ja <code>/metadata/authors</code> salvestatud pilte. Varukoopiad ei hõlma ühtegi teie raamatukogu kaustades olevat faili.",
|
||||||
|
"MessageBatchQuickMatchDescription": "Kiire sobitamine üritab lisada valitud üksustele puuduvad kaaned ja metaandmed. Luba allpool olevad valikud, et lubada Kiire sobitamine'il üle kirjutada olemasolevaid kaasi ja/või metaandmeid.",
|
||||||
|
"MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud",
|
||||||
|
"MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi",
|
||||||
|
"MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud",
|
||||||
|
"MessageBookshelfNoSeries": "Teil pole ühtegi seeriat",
|
||||||
|
"MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu",
|
||||||
|
"MessageChapterErrorFirstNotZero": "Esimene peatükk peab algama 0-st",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Vigane algusaeg peab olema väiksem kui heliraamatu kestus",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Vigane algusaeg peab olema suurem või võrdne eelneva peatüki algusajaga",
|
||||||
|
"MessageChapterStartIsAfter": "Peatüki algus on pärast teie heliraamatu lõppu",
|
||||||
|
"MessageCheckingCron": "Croni kontrollimine...",
|
||||||
|
"MessageConfirmCloseFeed": "Olete kindel, et soovite selle voo sulgeda?",
|
||||||
|
"MessageConfirmDeleteBackup": "Olete kindel, et soovite varukoopia kustutada {0} kohta?",
|
||||||
|
"MessageConfirmDeleteFile": "See kustutab faili teie failisüsteemist. Olete kindel?",
|
||||||
|
"MessageConfirmDeleteLibrary": "Olete kindel, et soovite raamatukogu \"{0}\" lõplikult kustutada?",
|
||||||
|
"MessageConfirmDeleteLibraryItem": "See kustutab raamatukogu üksuse andmebaasist ja failisüsteemist. Olete kindel?",
|
||||||
|
"MessageConfirmDeleteLibraryItems": "See kustutab {0} raamatukogu üksust andmebaasist ja failisüsteemist. Olete kindel?",
|
||||||
|
"MessageConfirmDeleteSession": "Olete kindel, et soovite selle seansi kustutada?",
|
||||||
|
"MessageConfirmForceReScan": "Olete kindel, et soovite jõuga uuesti skannida?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Olete kindel, et soovite kõik episoodid lõpetatuks märkida?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Olete kindel, et soovite kõik episoodid mitte lõpetatuks märkida?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Olete kindel, et soovite selle seeria kõik raamatud lõpetatuks märkida?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Olete kindel, et soovite selle seeria kõik raamatud mitte lõpetatuks märkida?",
|
||||||
|
"MessageConfirmQuickEmbed": "Hoiatus! Quick Embed ei tee varukoopiaid teie helifailidest. Veenduge, et teil oleks varukoopia oma helifailidest. <br><br>Kas soovite jätkata?",
|
||||||
|
"MessageConfirmRemoveAllChapters": "Olete kindel, et soovite eemaldada kõik peatükid?",
|
||||||
|
"MessageConfirmRemoveAuthor": "Olete kindel, et soovite autori \"{0}\" eemaldada?",
|
||||||
|
"MessageConfirmRemoveCollection": "Olete kindel, et soovite kogumi \"{0}\" eemaldada?",
|
||||||
|
"MessageConfirmRemoveEpisode": "Olete kindel, et soovite episoodi \"{0}\" eemaldada?",
|
||||||
|
"MessageConfirmRemoveEpisodes": "Olete kindel, et soovite eemaldada {0} episoodi?",
|
||||||
|
"MessageConfirmRemoveListeningSessions": "Olete kindel, et soovite eemaldada {0} kuulamise sessiooni?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Olete kindel, et soovite jutustaja \"{0}\" eemaldada?",
|
||||||
|
"MessageConfirmRemovePlaylist": "Olete kindel, et soovite eemaldada oma esitusloendi \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Olete kindel, et soovite žanri \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Märkus: See žanr on juba olemas, nii et need ühendatakse.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Hoiatus! Sarnane žanr erineva puhvriga on juba olemas \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Olete kindel, et soovite silti \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Märkus: See silt on juba olemas, nii et need ühendatakse.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Hoiatus! Sarnane silt erineva puhvriga on juba olemas \"{0}\".",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Olete kindel, et soovite uuesti skannida {0} üksust?",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Olete kindel, et soovite saata {0} e-raamatu \"{1}\" seadmesse \"{2}\"?",
|
||||||
|
"MessageDownloadingEpisode": "Episoodi allalaadimine",
|
||||||
|
"MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda",
|
||||||
|
"MessageEmbedFinished": "Manustamine lõpetatud!",
|
||||||
|
"MessageEpisodesQueuedForDownload": "{0} Episood(i) on allalaadimiseks järjekorras",
|
||||||
|
"MessageFeedURLWillBe": "Toite URL saab olema {0}",
|
||||||
|
"MessageFetching": "Hangitakse...",
|
||||||
|
"MessageForceReScanDescription": "skaneerib kõik failid uuesti nagu värsket skannimist. Heli faili ID3 silte, OPF faile ja tekstifaile skaneeritakse uuesti.",
|
||||||
|
"MessageImportantNotice": "Oluline märkus!",
|
||||||
|
"MessageInsertChapterBelow": "Sisesta peatükk allapoole",
|
||||||
|
"MessageItemsSelected": "{0} Valitud üksust",
|
||||||
|
"MessageItemsUpdated": "{0} Üksust on uuendatud",
|
||||||
|
"MessageJoinUsOn": "Liitu meiega",
|
||||||
|
"MessageListeningSessionsInTheLastYear": "Kuulamissessioone viimase aasta jooksul: {0}",
|
||||||
|
"MessageLoading": "Laadimine...",
|
||||||
|
"MessageLoadingFolders": "Kaustade laadimine...",
|
||||||
|
"MessageM4BFailed": "M4B ebaõnnestus!",
|
||||||
|
"MessageM4BFinished": "M4B lõpetatud!",
|
||||||
|
"MessageMapChapterTitles": "Kaarda peatükkide pealkirjad olemasolevatele heliraamatu peatükkidele, ajatempe ei muudeta",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Märgi kõik episoodid lõpetatuks",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Märgi kõik episoodid mitte lõpetatuks",
|
||||||
|
"MessageMarkAsFinished": "Märgi lõpetatuks",
|
||||||
|
"MessageMarkAsNotFinished": "Märgi mitte lõpetatuks",
|
||||||
|
"MessageMatchBooksDescription": "üritab raamatuid raamatukogus sobitada otsingupakkujast leitud raamatuga ning täita tühjad üksikasjad ja kaas. Ei üle kirjuta üksikasju.",
|
||||||
|
"MessageNoAudioTracks": "Ühtegi helijälge pole",
|
||||||
|
"MessageNoAuthors": "Ühtegi autori pole",
|
||||||
|
"MessageNoBackups": "Ühtegi varukoopia pole",
|
||||||
|
"MessageNoBookmarks": "Ühtegi järjehoidjat pole",
|
||||||
|
"MessageNoChapters": "Ühtegi peatükki pole",
|
||||||
|
"MessageNoCollections": "Ühtegi kogumit pole",
|
||||||
|
"MessageNoCoversFound": "Ühtegi kaant pole leitud",
|
||||||
|
"MessageNoDescription": "Kirjeldust pole",
|
||||||
|
"MessageNoDownloadsInProgress": "Praegu allalaadimisi pole",
|
||||||
|
"MessageNoDownloadsQueued": "Pole järjekorras allalaadimisi",
|
||||||
|
"MessageNoEpisodeMatchesFound": "Ühtegi episoodi vastet pole leitud",
|
||||||
|
"MessageNoEpisodes": "Ühtegi episoodi pole",
|
||||||
|
"MessageNoFoldersAvailable": "Ühtegi kausta pole saadaval",
|
||||||
|
"MessageNoGenres": "Ühtegi žanrit pole",
|
||||||
|
"MessageNoIssues": "Ühtegi probleemi pole",
|
||||||
|
"MessageNoItems": "Ühtegi üksust pole",
|
||||||
|
"MessageNoItemsFound": "Ühtegi üksust pole leitud",
|
||||||
|
"MessageNoListeningSessions": "Ühtegi kuulamissessiooni pole",
|
||||||
|
"MessageNoLogs": "Ühtegi logi pole",
|
||||||
|
"MessageNoMediaProgress": "Ühtegi meediaprogressi pole",
|
||||||
|
"MessageNoNotifications": "Ühtegi teavitust pole",
|
||||||
|
"MessageNoPodcastsFound": "Ühtegi podcasti pole leitud",
|
||||||
|
"MessageNoResults": "Ühtegi tulemust pole",
|
||||||
|
"MessageNoSearchResultsFor": "Otsingutulemusi pole märksõna kohta: \"{0}\"",
|
||||||
|
"MessageNoSeries": "Ühtegi seeriat pole",
|
||||||
|
"MessageNoTags": "Ühtegi silti pole",
|
||||||
|
"MessageNoTasksRunning": "Ühtegi käimasolevat ülesannet pole",
|
||||||
|
"MessageNotYetImplemented": "Pole veel ellu viidud",
|
||||||
|
"MessageNoUpdateNecessary": "Ühtegi värskendust pole vaja",
|
||||||
|
"MessageNoUpdatesWereNecessary": "Ühtegi värskendust polnud vaja",
|
||||||
|
"MessageNoUserPlaylists": "Teil pole ühtegi esitusloendit",
|
||||||
|
"MessageOr": "või",
|
||||||
|
"MessagePauseChapter": "Peata peatüki esitamine",
|
||||||
|
"MessagePlayChapter": "Kuula peatüki algust",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Loo esitusloend kogumist",
|
||||||
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcastil pole sobitamiseks RSS-voogu",
|
||||||
|
"MessageQuickMatchDescription": "täidab tühjad üksikasjad ja kaaned raamatukogus esimese otsingutulemusega rakendusest '{0}'. Ei üle kirjuta üksikasju, välja arvatud juhul, kui serveri sätetes on lubatud 'Eelista sobitatud metaandmeid'.",
|
||||||
|
"MessageRemoveChapter": "Eemalda peatükk",
|
||||||
|
"MessageRemoveEpisodes": "Eemalda {0} episood(i)",
|
||||||
|
"MessageRemoveFromPlayerQueue": "Eemalda esitusjärjekorrast",
|
||||||
|
"MessageRemoveUserWarning": "Olete kindel, et soovite kasutaja \"{0}\" lõplikult kustutada?",
|
||||||
|
"MessageReportBugsAndContribute": "Raporteeri vigu, palu funktsioone ja aita kaasa",
|
||||||
|
"MessageResetChaptersConfirm": "Olete kindel, et soovite peatükkide lähtestada ja tehtud muudatused tagasi võtta?",
|
||||||
|
"MessageRestoreBackupConfirm": "Olete kindel, et soovite taastada varukoopia, mis loodi",
|
||||||
|
"MessageRestoreBackupWarning": "Varukoopia taastamine kirjutab üle kogu /config ja /metadata/items & /metadata/authors kaustas oleva andmebaasi. <br /><br />Varukoopiad ei muuda teie raamatukogukaustades olevaid faile. Kui olete lubanud serveri sätetel salvestada kaane kunsti ja metaandmed teie raamatukogu kaustadesse, siis neid ei varundata ega kirjutata üle.<br /><br />Kõik teie serveri kasutavad kliendid värskendatakse automaatselt.",
|
||||||
|
"MessageSearchResultsFor": "Otsingutulemused märksõnale",
|
||||||
|
"MessageSelected": "{0} valitud",
|
||||||
|
"MessageServerCouldNotBeReached": "Serveriga ei saanud ühendust luua",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Määrake peatükid, kasutades iga helifaili peatükina ja peatüki pealkirjana helifaili nime",
|
||||||
|
"MessageStartPlaybackAtTime": "Alustage \"{0}\" esitamist kell {1}?",
|
||||||
|
"MessageThinking": "Mõtlen...",
|
||||||
|
"MessageUploaderItemFailed": "Üleslaadimine ebaõnnestus",
|
||||||
|
"MessageUploaderItemSuccess": "Edukalt üles laaditud!",
|
||||||
|
"MessageUploading": "Üles laadimine...",
|
||||||
|
"MessageValidCronExpression": "Kehtiv cron-väljend",
|
||||||
|
"MessageWatcherIsDisabledGlobally": "Vaatleja on ülemaailmselt keelatud serveri sätetes",
|
||||||
|
"MessageXLibraryIsEmpty": "{0} raamatukogu on tühi!",
|
||||||
|
"MessageYourAudiobookDurationIsLonger": "Teie heliraamatu kestus on pikem kui leitud kestus",
|
||||||
|
"MessageYourAudiobookDurationIsShorter": "Teie heliraamatu kestus on lühem kui leitud kestus",
|
||||||
|
"NoteChangeRootPassword": "Root kasutajal võib olla ainus kasutaja, kellel võib olla tühi parool",
|
||||||
|
"NoteChapterEditorTimes": "Märkus: Esimese peatüki algusaeg peab jääma 0:00 ja viimase peatüki algusaeg ei tohi ületada selle heliraamatu kestust.",
|
||||||
|
"NoteFolderPicker": "Märkus: juba kaardistatud kaustu ei kuvata",
|
||||||
|
"NoteRSSFeedPodcastAppsHttps": "Hoiatus: Enamik podcasti rakendusi nõuab, et RSS-voogu URL kasutaks HTTPS-i",
|
||||||
|
"NoteRSSFeedPodcastAppsPubDate": "Hoiatus: Üks või mitu teie episoodi ei sisalda publikatsioonikuupäeva. Mõned podcasti rakendused nõuavad seda.",
|
||||||
|
"NoteUploaderFoldersWithMediaFiles": "Kaustu, kus on meediat, käsitletakse eraldi raamatukogu üksustena.",
|
||||||
|
"NoteUploaderOnlyAudioFiles": "Kui laadite üles ainult helifaile, käsitletakse iga helifaili eraldi heliraamatuna.",
|
||||||
|
"NoteUploaderUnsupportedFiles": "Toetamata failid jäetakse tähelepanuta. Kausta valimisel või lohistamisel jäetakse tähelepanuta muud failid, mis pole üksuse kaustas.",
|
||||||
|
"PlaceholderNewCollection": "Uue kogumi nimi",
|
||||||
|
"PlaceholderNewFolderPath": "Uus kausta tee",
|
||||||
|
"PlaceholderNewPlaylist": "Uue esitusloendi nimi",
|
||||||
|
"PlaceholderSearch": "Otsi...",
|
||||||
|
"PlaceholderSearchEpisode": "Otsi episoodi...",
|
||||||
|
"ToastAccountUpdateFailed": "Konto värskendamine ebaõnnestus",
|
||||||
|
"ToastAccountUpdateSuccess": "Konto on värskendatud",
|
||||||
|
"ToastAuthorImageRemoveFailed": "Pildi eemaldamine ebaõnnestus",
|
||||||
|
"ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud",
|
||||||
|
"ToastAuthorUpdateFailed": "Autori värskendamine ebaõnnestus",
|
||||||
|
"ToastAuthorUpdateMerged": "Autor liidetud",
|
||||||
|
"ToastAuthorUpdateSuccess": "Autor värskendatud",
|
||||||
|
"ToastAuthorUpdateSuccessNoImageFound": "Autor värskendatud (pilti ei leitud)",
|
||||||
|
"ToastBackupCreateFailed": "Varukoopia loomine ebaõnnestus",
|
||||||
|
"ToastBackupCreateSuccess": "Varukoopia loodud",
|
||||||
|
"ToastBackupDeleteFailed": "Varukoopia kustutamine ebaõnnestus",
|
||||||
|
"ToastBackupDeleteSuccess": "Varukoopia kustutatud",
|
||||||
|
"ToastBackupRestoreFailed": "Varukoopia taastamine ebaõnnestus",
|
||||||
|
"ToastBackupUploadFailed": "Varukoopia üles laadimine ebaõnnestus",
|
||||||
|
"ToastBackupUploadSuccess": "Varukoopia üles laaditud",
|
||||||
|
"ToastBatchUpdateFailed": "Partii värskendamine ebaõnnestus",
|
||||||
|
"ToastBatchUpdateSuccess": "Partii värskendamine õnnestus",
|
||||||
|
"ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus",
|
||||||
|
"ToastBookmarkCreateSuccess": "Järjehoidja lisatud",
|
||||||
|
"ToastBookmarkRemoveFailed": "Järjehoidja eemaldamine ebaõnnestus",
|
||||||
|
"ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud",
|
||||||
|
"ToastBookmarkUpdateFailed": "Järjehoidja värskendamine ebaõnnestus",
|
||||||
|
"ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud",
|
||||||
|
"ToastChaptersHaveErrors": "Peatükkidel on vigu",
|
||||||
|
"ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri",
|
||||||
|
"ToastCollectionItemsRemoveFailed": "Üksuse(te) eemaldamine kogumist ebaõnnestus",
|
||||||
|
"ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist",
|
||||||
|
"ToastCollectionRemoveFailed": "Kogumi eemaldamine ebaõnnestus",
|
||||||
|
"ToastCollectionRemoveSuccess": "Kogum eemaldatud",
|
||||||
|
"ToastCollectionUpdateFailed": "Kogumi värskendamine ebaõnnestus",
|
||||||
|
"ToastCollectionUpdateSuccess": "Kogum värskendatud",
|
||||||
|
"ToastItemCoverUpdateFailed": "Üksuse kaane värskendamine ebaõnnestus",
|
||||||
|
"ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud",
|
||||||
|
"ToastItemDetailsUpdateFailed": "Üksuse üksikasjade värskendamine ebaõnnestus",
|
||||||
|
"ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud",
|
||||||
|
"ToastItemDetailsUpdateUnneeded": "Üksuse üksikasjade värskendamine pole vajalik",
|
||||||
|
"ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus",
|
||||||
|
"ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud",
|
||||||
|
"ToastItemMarkedAsNotFinishedFailed": "Märgistamine kui mitte lõpetatud ebaõnnestus",
|
||||||
|
"ToastItemMarkedAsNotFinishedSuccess": "Üksus märgitud kui mitte lõpetatud",
|
||||||
|
"ToastLibraryCreateFailed": "Raamatukogu loomine ebaõnnestus",
|
||||||
|
"ToastLibraryCreateSuccess": "Raamatukogu \"{0}\" loodud",
|
||||||
|
"ToastLibraryDeleteFailed": "Raamatukogu kustutamine ebaõnnestus",
|
||||||
|
"ToastLibraryDeleteSuccess": "Raamatukogu kustutatud",
|
||||||
|
"ToastLibraryScanFailedToStart": "Skanneerimine ei käivitunud",
|
||||||
|
"ToastLibraryScanStarted": "Raamatukogu skaneerimine alustatud",
|
||||||
|
"ToastLibraryUpdateFailed": "Raamatukogu värskendamine ebaõnnestus",
|
||||||
|
"ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud",
|
||||||
|
"ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus",
|
||||||
|
"ToastPlaylistCreateSuccess": "Esitusloend loodud",
|
||||||
|
"ToastPlaylistRemoveFailed": "Esitusloendi eemaldamine ebaõnnestus",
|
||||||
|
"ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud",
|
||||||
|
"ToastPlaylistUpdateFailed": "Esitusloendi värskendamine ebaõnnestus",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Esitusloend värskendatud",
|
||||||
|
"ToastPodcastCreateFailed": "Podcasti loomine ebaõnnestus",
|
||||||
|
"ToastPodcastCreateSuccess": "Podcast loodud edukalt",
|
||||||
|
"ToastRemoveItemFromCollectionFailed": "Üksuse eemaldamine kogumist ebaõnnestus",
|
||||||
|
"ToastRemoveItemFromCollectionSuccess": "Üksus eemaldatud kogumist",
|
||||||
|
"ToastRSSFeedCloseFailed": "RSS-voogu sulgemine ebaõnnestus",
|
||||||
|
"ToastRSSFeedCloseSuccess": "RSS-voog suletud",
|
||||||
|
"ToastSendEbookToDeviceFailed": "E-raamatu saatmine seadmesse ebaõnnestus",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "E-raamat saadetud seadmesse \"{0}\"",
|
||||||
|
"ToastSeriesUpdateFailed": "Sarja värskendamine ebaõnnestus",
|
||||||
|
"ToastSeriesUpdateSuccess": "Sarja värskendamine õnnestus",
|
||||||
|
"ToastSessionDeleteFailed": "Seansi kustutamine ebaõnnestus",
|
||||||
|
"ToastSessionDeleteSuccess": "Sessioon kustutatud",
|
||||||
|
"ToastSocketConnected": "Pesa ühendatud",
|
||||||
|
"ToastSocketDisconnected": "Pesa ühendus katkenud",
|
||||||
|
"ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus",
|
||||||
|
"ToastUserDeleteFailed": "Kasutaja kustutamine ebaõnnestus",
|
||||||
|
"ToastUserDeleteSuccess": "Kasutaja kustutatud"
|
||||||
|
}
|
@ -7,7 +7,7 @@
|
|||||||
"ButtonAddUser": "Ajouter un utilisateur",
|
"ButtonAddUser": "Ajouter un utilisateur",
|
||||||
"ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque",
|
"ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque",
|
||||||
"ButtonApply": "Appliquer",
|
"ButtonApply": "Appliquer",
|
||||||
"ButtonApplyChapters": "Appliquer les chapitres",
|
"ButtonApplyChapters": "Appliquer aux chapitres",
|
||||||
"ButtonAuthors": "Auteurs",
|
"ButtonAuthors": "Auteurs",
|
||||||
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
|
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
|
||||||
"ButtonCancel": "Annuler",
|
"ButtonCancel": "Annuler",
|
||||||
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Cacher",
|
"ButtonHide": "Cacher",
|
||||||
"ButtonHome": "Accueil",
|
"ButtonHome": "Accueil",
|
||||||
"ButtonIssues": "Parutions",
|
"ButtonIssues": "Parutions",
|
||||||
|
"ButtonJumpBackward": "Retour",
|
||||||
|
"ButtonJumpForward": "Avancer",
|
||||||
"ButtonLatest": "Dernière version",
|
"ButtonLatest": "Dernière version",
|
||||||
"ButtonLibrary": "Bibliothèque",
|
"ButtonLibrary": "Bibliothèque",
|
||||||
"ButtonLogout": "Me déconnecter",
|
"ButtonLogout": "Me déconnecter",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Chercher tous les auteurs",
|
"ButtonMatchAllAuthors": "Chercher tous les auteurs",
|
||||||
"ButtonMatchBooks": "Chercher les livres",
|
"ButtonMatchBooks": "Chercher les livres",
|
||||||
"ButtonNevermind": "Non merci",
|
"ButtonNevermind": "Non merci",
|
||||||
|
"ButtonNext": "Suivant",
|
||||||
|
"ButtonNextChapter": "Chapitre suivant",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Ouvrir le flux",
|
"ButtonOpenFeed": "Ouvrir le flux",
|
||||||
"ButtonOpenManager": "Ouvrir le gestionnaire",
|
"ButtonOpenManager": "Ouvrir le gestionnaire",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Écouter",
|
"ButtonPlay": "Écouter",
|
||||||
"ButtonPlaying": "En lecture",
|
"ButtonPlaying": "En lecture",
|
||||||
"ButtonPlaylists": "Listes de lecture",
|
"ButtonPlaylists": "Listes de lecture",
|
||||||
|
"ButtonPrevious": "Précédent",
|
||||||
|
"ButtonPreviousChapter": "Chapitre précédent",
|
||||||
"ButtonPurgeAllCache": "Purger le cache",
|
"ButtonPurgeAllCache": "Purger le cache",
|
||||||
"ButtonPurgeItemsCache": "Purger le cache des articles",
|
"ButtonPurgeItemsCache": "Purger le cache des articles",
|
||||||
"ButtonPurgeMediaProgress": "Purger la progression des médias",
|
"ButtonPurgeMediaProgress": "Purger la progression des médias",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
|
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
|
||||||
"ButtonQuickMatch": "Recherche rapide",
|
"ButtonQuickMatch": "Recherche rapide",
|
||||||
"ButtonRead": "Lire",
|
"ButtonRead": "Lire",
|
||||||
|
"ButtonRefresh": "Rafraîchir",
|
||||||
"ButtonRemove": "Supprimer",
|
"ButtonRemove": "Supprimer",
|
||||||
"ButtonRemoveAll": "Supprimer tout",
|
"ButtonRemoveAll": "Supprimer tout",
|
||||||
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Sélectionner le chemin du dossier",
|
"ButtonSelectFolderPath": "Sélectionner le chemin du dossier",
|
||||||
"ButtonSeries": "Séries",
|
"ButtonSeries": "Séries",
|
||||||
"ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes",
|
"ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes",
|
||||||
|
"ButtonShare": "Partager",
|
||||||
"ButtonShiftTimes": "Décaler l’horodatage du livre",
|
"ButtonShiftTimes": "Décaler l’horodatage du livre",
|
||||||
"ButtonShow": "Afficher",
|
"ButtonShow": "Afficher",
|
||||||
"ButtonStartM4BEncode": "Démarrer l’encodage M4B",
|
"ButtonStartM4BEncode": "Démarrer l’encodage M4B",
|
||||||
@ -83,7 +92,7 @@
|
|||||||
"ButtonUploadBackup": "Téléverser une sauvegarde",
|
"ButtonUploadBackup": "Téléverser une sauvegarde",
|
||||||
"ButtonUploadCover": "Téléverser une couverture",
|
"ButtonUploadCover": "Téléverser une couverture",
|
||||||
"ButtonUploadOPMLFile": "Téléverser un fichier OPML",
|
"ButtonUploadOPMLFile": "Téléverser un fichier OPML",
|
||||||
"ButtonUserDelete": "Effacer l’utilisateur {0}",
|
"ButtonUserDelete": "Supprimer l’utilisateur {0}",
|
||||||
"ButtonUserEdit": "Modifier l’utilisateur {0}",
|
"ButtonUserEdit": "Modifier l’utilisateur {0}",
|
||||||
"ButtonViewAll": "Afficher tout",
|
"ButtonViewAll": "Afficher tout",
|
||||||
"ButtonYes": "Oui",
|
"ButtonYes": "Oui",
|
||||||
@ -92,8 +101,8 @@
|
|||||||
"ErrorUploadLacksTitle": "Doit avoir un titre",
|
"ErrorUploadLacksTitle": "Doit avoir un titre",
|
||||||
"HeaderAccount": "Compte",
|
"HeaderAccount": "Compte",
|
||||||
"HeaderAdvanced": "Avancé",
|
"HeaderAdvanced": "Avancé",
|
||||||
"HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise",
|
"HeaderAppriseNotificationSettings": "Configuration des notifications Apprise",
|
||||||
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
"HeaderAudiobookTools": "Outils de gestion de fichiers de livres audio",
|
||||||
"HeaderAudioTracks": "Pistes audio",
|
"HeaderAudioTracks": "Pistes audio",
|
||||||
"HeaderAuthentication": "Authentication",
|
"HeaderAuthentication": "Authentication",
|
||||||
"HeaderBackups": "Sauvegardes",
|
"HeaderBackups": "Sauvegardes",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Entrées de la collection",
|
"HeaderCollectionItems": "Entrées de la collection",
|
||||||
"HeaderCover": "Couverture",
|
"HeaderCover": "Couverture",
|
||||||
"HeaderCurrentDownloads": "Téléchargements en cours",
|
"HeaderCurrentDownloads": "Téléchargements en cours",
|
||||||
|
"HeaderCustomMetadataProviders": "Fournisseurs de métadonnées personnalisés",
|
||||||
"HeaderDetails": "Détails",
|
"HeaderDetails": "Détails",
|
||||||
"HeaderDownloadQueue": "File d’attente de téléchargements",
|
"HeaderDownloadQueue": "File d’attente de téléchargements",
|
||||||
"HeaderEbookFiles": "Fichier des livres numériques",
|
"HeaderEbookFiles": "Fichier des livres numériques",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Mettre à jour les détails",
|
"HeaderUpdateDetails": "Mettre à jour les détails",
|
||||||
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
|
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
|
||||||
"HeaderUsers": "Utilisateurs",
|
"HeaderUsers": "Utilisateurs",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Vos statistiques",
|
"HeaderYourStats": "Vos statistiques",
|
||||||
"LabelAbridged": "Version courte",
|
"LabelAbridged": "Version courte",
|
||||||
"LabelAccountType": "Type de compte",
|
"LabelAccountType": "Type de compte",
|
||||||
@ -281,11 +292,11 @@
|
|||||||
"LabelFinished": "Terminé le",
|
"LabelFinished": "Terminé le",
|
||||||
"LabelFolder": "Dossier",
|
"LabelFolder": "Dossier",
|
||||||
"LabelFolders": "Dossiers",
|
"LabelFolders": "Dossiers",
|
||||||
"LabelFontBold": "Bold",
|
"LabelFontBold": "Gras",
|
||||||
"LabelFontFamily": "Polices de caractères",
|
"LabelFontFamily": "Polices de caractères",
|
||||||
"LabelFontItalic": "Italic",
|
"LabelFontItalic": "Italique",
|
||||||
"LabelFontScale": "Taille de la police de caractère",
|
"LabelFontScale": "Taille de la police de caractère",
|
||||||
"LabelFontStrikethrough": "Strikethrough",
|
"LabelFontStrikethrough": "Barrer",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Balises de métadonnée",
|
"LabelMetaTags": "Balises de métadonnée",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Manquant",
|
"LabelMissing": "Manquant",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Parties manquantes",
|
"LabelMissingParts": "Parties manquantes",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
|
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
|
||||||
"LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI 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. L’utilisation d’un astérisque (<code>*</code>) comme seule entrée autorise n’importe quel URI.",
|
"LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI 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. L’utilisation d’un astérisque (<code>*</code>) comme seule entrée autorise n’importe quel URI.",
|
||||||
"LabelMore": "Plus",
|
"LabelMore": "Plus",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Peut télécharger",
|
"LabelPermissionsDownload": "Peut télécharger",
|
||||||
"LabelPermissionsUpdate": "Peut mettre à jour",
|
"LabelPermissionsUpdate": "Peut mettre à jour",
|
||||||
"LabelPermissionsUpload": "Peut téléverser",
|
"LabelPermissionsUpload": "Peut téléverser",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Chemin / URL des photos",
|
"LabelPhotoPathURL": "Chemin / URL des photos",
|
||||||
"LabelPlaylists": "Listes de lecture",
|
"LabelPlaylists": "Listes de lecture",
|
||||||
"LabelPlayMethod": "Méthode d’écoute",
|
"LabelPlayMethod": "Méthode d’écoute",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Région de recherche de podcasts",
|
||||||
"LabelPodcastType": "Type de Podcast",
|
"LabelPodcastType": "Type de Podcast",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||||
@ -406,7 +421,7 @@
|
|||||||
"LabelRecentlyAdded": "Derniers ajouts",
|
"LabelRecentlyAdded": "Derniers ajouts",
|
||||||
"LabelRecentSeries": "Séries récentes",
|
"LabelRecentSeries": "Séries récentes",
|
||||||
"LabelRecommended": "Recommandé",
|
"LabelRecommended": "Recommandé",
|
||||||
"LabelRedo": "Redo",
|
"LabelRedo": "Refaire",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de parution",
|
"LabelReleaseDate": "Date de parution",
|
||||||
"LabelRemoveCover": "Supprimer la couverture",
|
"LabelRemoveCover": "Supprimer la couverture",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Séries",
|
"LabelSeries": "Séries",
|
||||||
"LabelSeriesName": "Nom de la série",
|
"LabelSeriesName": "Nom de la série",
|
||||||
"LabelSeriesProgress": "Progression de séries",
|
"LabelSeriesProgress": "Progression de séries",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Définir comme principale",
|
"LabelSetEbookAsPrimary": "Définir comme principale",
|
||||||
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
||||||
"LabelSettingsAudiobooksOnly": "Livres audios seulement",
|
"LabelSettingsAudiobooksOnly": "Livres audios seulement",
|
||||||
@ -495,10 +511,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
||||||
"LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à l’utilisateur",
|
"LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à l’utilisateur",
|
||||||
"LabelTasks": "Tâches en cours",
|
"LabelTasks": "Tâches en cours",
|
||||||
"LabelTextEditorBulletedList": "Bulleted list",
|
"LabelTextEditorBulletedList": "Liste à puces",
|
||||||
"LabelTextEditorLink": "Link",
|
"LabelTextEditorLink": "Lien",
|
||||||
"LabelTextEditorNumberedList": "Numbered list",
|
"LabelTextEditorNumberedList": "Liste numérotée",
|
||||||
"LabelTextEditorUnlink": "Unlink",
|
"LabelTextEditorUnlink": "Dissocier",
|
||||||
"LabelTheme": "Thème",
|
"LabelTheme": "Thème",
|
||||||
"LabelThemeDark": "Sombre",
|
"LabelThemeDark": "Sombre",
|
||||||
"LabelThemeLight": "Clair",
|
"LabelThemeLight": "Clair",
|
||||||
@ -524,7 +540,7 @@
|
|||||||
"LabelTracksSingleTrack": "Piste simple",
|
"LabelTracksSingleTrack": "Piste simple",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Version intégrale",
|
"LabelUnabridged": "Version intégrale",
|
||||||
"LabelUndo": "Undo",
|
"LabelUndo": "Annuler",
|
||||||
"LabelUnknown": "Inconnu",
|
"LabelUnknown": "Inconnu",
|
||||||
"LabelUpdateCover": "Mettre à jour la couverture",
|
"LabelUpdateCover": "Mettre à jour la couverture",
|
||||||
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu’une correspondance est trouvée",
|
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu’une correspondance est trouvée",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Afficher la liste de lecture",
|
"LabelViewQueue": "Afficher la liste de lecture",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
||||||
|
"LabelYearReviewHide": "Masquer le bilan de l’année",
|
||||||
|
"LabelYearReviewShow": "Afficher le bilan de l’année",
|
||||||
"LabelYourAudiobookDuration": "Durée de vos livres audios",
|
"LabelYourAudiobookDuration": "Durée de vos livres audios",
|
||||||
"LabelYourBookmarks": "Vos signets",
|
"LabelYourBookmarks": "Vos signets",
|
||||||
"LabelYourPlaylists": "Vos listes de lecture",
|
"LabelYourPlaylists": "Vos listes de lecture",
|
||||||
@ -676,7 +694,7 @@
|
|||||||
"MessageYourAudiobookDurationIsShorter": "La durée de votre livre audio est plus courte que la durée trouvée",
|
"MessageYourAudiobookDurationIsShorter": "La durée de votre livre audio est plus courte que la durée trouvée",
|
||||||
"NoteChangeRootPassword": "seul l’utilisateur « root » peut utiliser un mot de passe vide",
|
"NoteChangeRootPassword": "seul l’utilisateur « root » peut utiliser un mot de passe vide",
|
||||||
"NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.",
|
"NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.",
|
||||||
"NoteFolderPicker": "Information : Les dossiers déjà surveillés ne sont pas affichés",
|
"NoteFolderPicker": "Information : les dossiers déjà surveillés ne sont pas affichés",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
|
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
|
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "છુપાવો",
|
"ButtonHide": "છુપાવો",
|
||||||
"ButtonHome": "ઘર",
|
"ButtonHome": "ઘર",
|
||||||
"ButtonIssues": "સમસ્યાઓ",
|
"ButtonIssues": "સમસ્યાઓ",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "નવીનતમ",
|
"ButtonLatest": "નવીનતમ",
|
||||||
"ButtonLibrary": "પુસ્તકાલય",
|
"ButtonLibrary": "પુસ્તકાલય",
|
||||||
"ButtonLogout": "લૉગ આઉટ",
|
"ButtonLogout": "લૉગ આઉટ",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
|
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
|
||||||
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
|
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
|
||||||
"ButtonNevermind": "કંઈ વાંધો નહીં",
|
"ButtonNevermind": "કંઈ વાંધો નહીં",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "ઓકે",
|
"ButtonOk": "ઓકે",
|
||||||
"ButtonOpenFeed": "ફીડ ખોલો",
|
"ButtonOpenFeed": "ફીડ ખોલો",
|
||||||
"ButtonOpenManager": "મેનેજર ખોલો",
|
"ButtonOpenManager": "મેનેજર ખોલો",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "ચલાવો",
|
"ButtonPlay": "ચલાવો",
|
||||||
"ButtonPlaying": "ચલાવી રહ્યું છે",
|
"ButtonPlaying": "ચલાવી રહ્યું છે",
|
||||||
"ButtonPlaylists": "પ્લેલિસ્ટ",
|
"ButtonPlaylists": "પ્લેલિસ્ટ",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
|
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
|
||||||
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
|
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
|
||||||
"ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો",
|
"ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "કતારથી કાઢી નાખો",
|
"ButtonQueueRemoveItem": "કતારથી કાઢી નાખો",
|
||||||
"ButtonQuickMatch": "ઝડપી મેળ ખવડાવો",
|
"ButtonQuickMatch": "ઝડપી મેળ ખવડાવો",
|
||||||
"ButtonRead": "વાંચો",
|
"ButtonRead": "વાંચો",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "કાઢી નાખો",
|
"ButtonRemove": "કાઢી નાખો",
|
||||||
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
||||||
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો",
|
"ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો",
|
||||||
"ButtonSeries": "સિરીઝ",
|
"ButtonSeries": "સિરીઝ",
|
||||||
"ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો",
|
"ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "સમય શિફ્ટ કરો",
|
"ButtonShiftTimes": "સમય શિફ્ટ કરો",
|
||||||
"ButtonShow": "બતાવો",
|
"ButtonShow": "બતાવો",
|
||||||
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
|
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
|
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
|
||||||
"HeaderCover": "આવરણ",
|
"HeaderCover": "આવરણ",
|
||||||
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
|
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "વિગતો",
|
"HeaderDetails": "વિગતો",
|
||||||
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
|
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
|
||||||
"HeaderEbookFiles": "ઇબુક ફાઇલો",
|
"HeaderEbookFiles": "ઇબુક ફાઇલો",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Update Details",
|
"HeaderUpdateDetails": "Update Details",
|
||||||
"HeaderUpdateLibrary": "Update Library",
|
"HeaderUpdateLibrary": "Update Library",
|
||||||
"HeaderUsers": "Users",
|
"HeaderUsers": "Users",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Your Stats",
|
"HeaderYourStats": "Your Stats",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Account Type",
|
"LabelAccountType": "Account Type",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Missing",
|
"LabelMissing": "Missing",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Missing Parts",
|
"LabelMissingParts": "Missing Parts",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "More",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Can Download",
|
"LabelPermissionsDownload": "Can Download",
|
||||||
"LabelPermissionsUpdate": "Can Update",
|
"LabelPermissionsUpdate": "Can Update",
|
||||||
"LabelPermissionsUpload": "Can Upload",
|
"LabelPermissionsUpload": "Can Upload",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Photo Path/URL",
|
"LabelPhotoPathURL": "Photo Path/URL",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Play Method",
|
"LabelPlayMethod": "Play Method",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Series Name",
|
"LabelSeriesName": "Series Name",
|
||||||
"LabelSeriesProgress": "Series Progress",
|
"LabelSeriesProgress": "Series Progress",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Set as primary",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "View player queue",
|
"LabelViewQueue": "View player queue",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Weekdays to run",
|
"LabelWeekdaysToRun": "Weekdays to run",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Your audiobook duration",
|
"LabelYourAudiobookDuration": "Your audiobook duration",
|
||||||
"LabelYourBookmarks": "Your Bookmarks",
|
"LabelYourBookmarks": "Your Bookmarks",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Your Playlists",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "छुपाएं",
|
"ButtonHide": "छुपाएं",
|
||||||
"ButtonHome": "घर",
|
"ButtonHome": "घर",
|
||||||
"ButtonIssues": "समस्याएं",
|
"ButtonIssues": "समस्याएं",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "नवीनतम",
|
"ButtonLatest": "नवीनतम",
|
||||||
"ButtonLibrary": "पुस्तकालय",
|
"ButtonLibrary": "पुस्तकालय",
|
||||||
"ButtonLogout": "लॉग आउट",
|
"ButtonLogout": "लॉग आउट",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें",
|
"ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें",
|
||||||
"ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें",
|
"ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें",
|
||||||
"ButtonNevermind": "कोई बात नहीं",
|
"ButtonNevermind": "कोई बात नहीं",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "ठीक है",
|
"ButtonOk": "ठीक है",
|
||||||
"ButtonOpenFeed": "फ़ीड खोलें",
|
"ButtonOpenFeed": "फ़ीड खोलें",
|
||||||
"ButtonOpenManager": "मैनेजर खोलें",
|
"ButtonOpenManager": "मैनेजर खोलें",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "चलाएँ",
|
"ButtonPlay": "चलाएँ",
|
||||||
"ButtonPlaying": "चल रही है",
|
"ButtonPlaying": "चल रही है",
|
||||||
"ButtonPlaylists": "प्लेलिस्ट्स",
|
"ButtonPlaylists": "प्लेलिस्ट्स",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "सभी Cache मिटाएं",
|
"ButtonPurgeAllCache": "सभी Cache मिटाएं",
|
||||||
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
|
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
|
||||||
"ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे",
|
"ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "कतार से हटाएं",
|
"ButtonQueueRemoveItem": "कतार से हटाएं",
|
||||||
"ButtonQuickMatch": "जल्दी से समानता की तलाश करें",
|
"ButtonQuickMatch": "जल्दी से समानता की तलाश करें",
|
||||||
"ButtonRead": "पढ़ लिया",
|
"ButtonRead": "पढ़ लिया",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "हटाएं",
|
"ButtonRemove": "हटाएं",
|
||||||
"ButtonRemoveAll": "सभी हटाएं",
|
"ButtonRemoveAll": "सभी हटाएं",
|
||||||
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "फ़ोल्डर का पथ चुनें",
|
"ButtonSelectFolderPath": "फ़ोल्डर का पथ चुनें",
|
||||||
"ButtonSeries": "सीरीज",
|
"ButtonSeries": "सीरीज",
|
||||||
"ButtonSetChaptersFromTracks": "ट्रैक्स से अध्याय बनाएं",
|
"ButtonSetChaptersFromTracks": "ट्रैक्स से अध्याय बनाएं",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "समय खिसकाए",
|
"ButtonShiftTimes": "समय खिसकाए",
|
||||||
"ButtonShow": "दिखाएं",
|
"ButtonShow": "दिखाएं",
|
||||||
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
|
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Collection Items",
|
"HeaderCollectionItems": "Collection Items",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Update Details",
|
"HeaderUpdateDetails": "Update Details",
|
||||||
"HeaderUpdateLibrary": "Update Library",
|
"HeaderUpdateLibrary": "Update Library",
|
||||||
"HeaderUsers": "Users",
|
"HeaderUsers": "Users",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Your Stats",
|
"HeaderYourStats": "Your Stats",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Account Type",
|
"LabelAccountType": "Account Type",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Missing",
|
"LabelMissing": "Missing",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Missing Parts",
|
"LabelMissingParts": "Missing Parts",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "More",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Can Download",
|
"LabelPermissionsDownload": "Can Download",
|
||||||
"LabelPermissionsUpdate": "Can Update",
|
"LabelPermissionsUpdate": "Can Update",
|
||||||
"LabelPermissionsUpload": "Can Upload",
|
"LabelPermissionsUpload": "Can Upload",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Photo Path/URL",
|
"LabelPhotoPathURL": "Photo Path/URL",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Play Method",
|
"LabelPlayMethod": "Play Method",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Series Name",
|
"LabelSeriesName": "Series Name",
|
||||||
"LabelSeriesProgress": "Series Progress",
|
"LabelSeriesProgress": "Series Progress",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Set as primary",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "View player queue",
|
"LabelViewQueue": "View player queue",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Weekdays to run",
|
"LabelWeekdaysToRun": "Weekdays to run",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Your audiobook duration",
|
"LabelYourAudiobookDuration": "Your audiobook duration",
|
||||||
"LabelYourBookmarks": "Your Bookmarks",
|
"LabelYourBookmarks": "Your Bookmarks",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Your Playlists",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Sakrij",
|
"ButtonHide": "Sakrij",
|
||||||
"ButtonHome": "Početna stranica",
|
"ButtonHome": "Početna stranica",
|
||||||
"ButtonIssues": "Problemi",
|
"ButtonIssues": "Problemi",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Najnovije",
|
"ButtonLatest": "Najnovije",
|
||||||
"ButtonLibrary": "Biblioteka",
|
"ButtonLibrary": "Biblioteka",
|
||||||
"ButtonLogout": "Odjavi se",
|
"ButtonLogout": "Odjavi se",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Matchaj sve autore",
|
"ButtonMatchAllAuthors": "Matchaj sve autore",
|
||||||
"ButtonMatchBooks": "Matchaj knjige",
|
"ButtonMatchBooks": "Matchaj knjige",
|
||||||
"ButtonNevermind": "Nije bitno",
|
"ButtonNevermind": "Nije bitno",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Otvori feed",
|
"ButtonOpenFeed": "Otvori feed",
|
||||||
"ButtonOpenManager": "Otvori menadžera",
|
"ButtonOpenManager": "Otvori menadžera",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Pokreni",
|
"ButtonPlay": "Pokreni",
|
||||||
"ButtonPlaying": "Playing",
|
"ButtonPlaying": "Playing",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Playlists",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Isprazni sav cache",
|
"ButtonPurgeAllCache": "Isprazni sav cache",
|
||||||
"ButtonPurgeItemsCache": "Isprazni Items Cache",
|
"ButtonPurgeItemsCache": "Isprazni Items Cache",
|
||||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Remove from queue",
|
"ButtonQueueRemoveItem": "Remove from queue",
|
||||||
"ButtonQuickMatch": "Brzi match",
|
"ButtonQuickMatch": "Brzi match",
|
||||||
"ButtonRead": "Pročitaj",
|
"ButtonRead": "Pročitaj",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Ukloni",
|
"ButtonRemove": "Ukloni",
|
||||||
"ButtonRemoveAll": "Ukloni sve",
|
"ButtonRemoveAll": "Ukloni sve",
|
||||||
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
|
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Odaberi putanju do folder",
|
"ButtonSelectFolderPath": "Odaberi putanju do folder",
|
||||||
"ButtonSeries": "Serije",
|
"ButtonSeries": "Serije",
|
||||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Pomakni vremena",
|
"ButtonShiftTimes": "Pomakni vremena",
|
||||||
"ButtonShow": "Prikaži",
|
"ButtonShow": "Prikaži",
|
||||||
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
|
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Stvari u kolekciji",
|
"HeaderCollectionItems": "Stvari u kolekciji",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Detalji",
|
"HeaderDetails": "Detalji",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Aktualiziraj detalje",
|
"HeaderUpdateDetails": "Aktualiziraj detalje",
|
||||||
"HeaderUpdateLibrary": "Aktualiziraj biblioteku",
|
"HeaderUpdateLibrary": "Aktualiziraj biblioteku",
|
||||||
"HeaderUsers": "Korinici",
|
"HeaderUsers": "Korinici",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Tvoja statistika",
|
"HeaderYourStats": "Tvoja statistika",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Vrsta korisničkog računa",
|
"LabelAccountType": "Vrsta korisničkog računa",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minuta",
|
"LabelMinute": "Minuta",
|
||||||
"LabelMissing": "Nedostaje",
|
"LabelMissing": "Nedostaje",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Nedostajali dijelovi",
|
"LabelMissingParts": "Nedostajali dijelovi",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Više",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Smije preuzimati",
|
"LabelPermissionsDownload": "Smije preuzimati",
|
||||||
"LabelPermissionsUpdate": "Smije aktualizirati",
|
"LabelPermissionsUpdate": "Smije aktualizirati",
|
||||||
"LabelPermissionsUpload": "Smije uploadati",
|
"LabelPermissionsUpload": "Smije uploadati",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Slika putanja/URL",
|
"LabelPhotoPathURL": "Slika putanja/URL",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Vrsta reprodukcije",
|
"LabelPlayMethod": "Vrsta reprodukcije",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Područje pretrage podcasta",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
|
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serije",
|
"LabelSeries": "Serije",
|
||||||
"LabelSeriesName": "Ime serije",
|
"LabelSeriesName": "Ime serije",
|
||||||
"LabelSeriesProgress": "Series Progress",
|
"LabelSeriesProgress": "Series Progress",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Set as primary",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "View player queue",
|
"LabelViewQueue": "View player queue",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Radnih dana da radi",
|
"LabelWeekdaysToRun": "Radnih dana da radi",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Tvoje trajanje audiobooka",
|
"LabelYourAudiobookDuration": "Tvoje trajanje audiobooka",
|
||||||
"LabelYourBookmarks": "Tvoje knjižne oznake",
|
"LabelYourBookmarks": "Tvoje knjižne oznake",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Your Playlists",
|
||||||
|
779
client/strings/hu.json
Normal file
779
client/strings/hu.json
Normal file
@ -0,0 +1,779 @@
|
|||||||
|
{
|
||||||
|
"ButtonAdd": "Hozzáadás",
|
||||||
|
"ButtonAddChapters": "Fejezetek hozzáadása",
|
||||||
|
"ButtonAddDevice": "Eszköz hozzáadása",
|
||||||
|
"ButtonAddLibrary": "Könyvtár hozzáadása",
|
||||||
|
"ButtonAddPodcasts": "Podcastok hozzáadása",
|
||||||
|
"ButtonAddUser": "Felhasználó hozzáadása",
|
||||||
|
"ButtonAddYourFirstLibrary": "Az első könyvtár hozzáadása",
|
||||||
|
"ButtonApply": "Alkalmaz",
|
||||||
|
"ButtonApplyChapters": "Fejezetek alkalmazása",
|
||||||
|
"ButtonAuthors": "Szerzők",
|
||||||
|
"ButtonBrowseForFolder": "Mappa keresése",
|
||||||
|
"ButtonCancel": "Mégse",
|
||||||
|
"ButtonCancelEncode": "Kódolás megszakítása",
|
||||||
|
"ButtonChangeRootPassword": "Gyökérjelszó megváltoztatása",
|
||||||
|
"ButtonCheckAndDownloadNewEpisodes": "Új epizódok ellenőrzése és letöltése",
|
||||||
|
"ButtonChooseAFolder": "Válassz egy mappát",
|
||||||
|
"ButtonChooseFiles": "Fájlok kiválasztása",
|
||||||
|
"ButtonClearFilter": "Szűrő törlése",
|
||||||
|
"ButtonCloseFeed": "Hírcsatorna bezárása",
|
||||||
|
"ButtonCollections": "Gyűjtemény",
|
||||||
|
"ButtonConfigureScanner": "Szkenner konfigurálása",
|
||||||
|
"ButtonCreate": "Létrehozás",
|
||||||
|
"ButtonCreateBackup": "Biztonsági másolat készítése",
|
||||||
|
"ButtonDelete": "Törlés",
|
||||||
|
"ButtonDownloadQueue": "Sor",
|
||||||
|
"ButtonEdit": "Szerkesztés",
|
||||||
|
"ButtonEditChapters": "Fejezetek szerkesztése",
|
||||||
|
"ButtonEditPodcast": "Podcast szerkesztése",
|
||||||
|
"ButtonForceReScan": "Újraszkennelés kényszerítése",
|
||||||
|
"ButtonFullPath": "Teljes útvonal",
|
||||||
|
"ButtonHide": "Elrejtés",
|
||||||
|
"ButtonHome": "Kezdőlap",
|
||||||
|
"ButtonIssues": "Problémák",
|
||||||
|
"ButtonJumpBackward": "Ugrás vissza",
|
||||||
|
"ButtonJumpForward": "Ugrás előre",
|
||||||
|
"ButtonLatest": "Legújabb",
|
||||||
|
"ButtonLibrary": "Könyvtár",
|
||||||
|
"ButtonLogout": "Kijelentkezés",
|
||||||
|
"ButtonLookup": "Keresés",
|
||||||
|
"ButtonManageTracks": "Sávok kezelése",
|
||||||
|
"ButtonMapChapterTitles": "Fejezetcímek hozzárendelése",
|
||||||
|
"ButtonMatchAllAuthors": "Minden szerző egyeztetése",
|
||||||
|
"ButtonMatchBooks": "Könyvek egyeztetése",
|
||||||
|
"ButtonNevermind": "Mindegy",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Következő fejezet",
|
||||||
|
"ButtonOk": "Oké",
|
||||||
|
"ButtonOpenFeed": "Hírcsatorna megnyitása",
|
||||||
|
"ButtonOpenManager": "Kezelő megnyitása",
|
||||||
|
"ButtonPause": "Szünet",
|
||||||
|
"ButtonPlay": "Lejátszás",
|
||||||
|
"ButtonPlaying": "Lejátszás folyamatban",
|
||||||
|
"ButtonPlaylists": "Lejátszási listák",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Előző fejezet",
|
||||||
|
"ButtonPurgeAllCache": "Összes gyorsítótár törlése",
|
||||||
|
"ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése",
|
||||||
|
"ButtonPurgeMediaProgress": "Médialejátszás állapotának törlése",
|
||||||
|
"ButtonQueueAddItem": "Hozzáadás a sorhoz",
|
||||||
|
"ButtonQueueRemoveItem": "Eltávolítás a sorból",
|
||||||
|
"ButtonQuickMatch": "Gyors egyeztetés",
|
||||||
|
"ButtonRead": "Olvasás",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
|
"ButtonRemove": "Eltávolítás",
|
||||||
|
"ButtonRemoveAll": "Összes eltávolítása",
|
||||||
|
"ButtonRemoveAllLibraryItems": "Összes könyvtárelem eltávolítása",
|
||||||
|
"ButtonRemoveFromContinueListening": "Eltávolítás a Folytatás hallgatásából",
|
||||||
|
"ButtonRemoveFromContinueReading": "Eltávolítás a Folytatás olvasásából",
|
||||||
|
"ButtonRemoveSeriesFromContinueSeries": "Sorozat eltávolítása a Folytatás sorozatokból",
|
||||||
|
"ButtonReScan": "Újraszkennelés",
|
||||||
|
"ButtonReset": "Visszaállítás",
|
||||||
|
"ButtonResetToDefault": "Alapértelmezésre állítás",
|
||||||
|
"ButtonRestore": "Visszaállítás",
|
||||||
|
"ButtonSave": "Mentés",
|
||||||
|
"ButtonSaveAndClose": "Mentés és bezárás",
|
||||||
|
"ButtonSaveTracklist": "Sávlista mentése",
|
||||||
|
"ButtonScan": "Szkennelés",
|
||||||
|
"ButtonScanLibrary": "Könyvtár szkennelése",
|
||||||
|
"ButtonSearch": "Keresés",
|
||||||
|
"ButtonSelectFolderPath": "Mappa útvonalának kiválasztása",
|
||||||
|
"ButtonSeries": "Sorozatok",
|
||||||
|
"ButtonSetChaptersFromTracks": "Fejezetek beállítása sávokból",
|
||||||
|
"ButtonShare": "Share",
|
||||||
|
"ButtonShiftTimes": "Idők eltolása",
|
||||||
|
"ButtonShow": "Megjelenítés",
|
||||||
|
"ButtonStartM4BEncode": "M4B kódolás indítása",
|
||||||
|
"ButtonStartMetadataEmbed": "Metaadatok beágyazásának indítása",
|
||||||
|
"ButtonSubmit": "Beküldés",
|
||||||
|
"ButtonTest": "Teszt",
|
||||||
|
"ButtonUpload": "Feltöltés",
|
||||||
|
"ButtonUploadBackup": "Biztonsági másolat feltöltése",
|
||||||
|
"ButtonUploadCover": "Borító feltöltése",
|
||||||
|
"ButtonUploadOPMLFile": "OPML fájl feltöltése",
|
||||||
|
"ButtonUserDelete": "Felhasználó törlése {0}",
|
||||||
|
"ButtonUserEdit": "Felhasználó szerkesztése {0}",
|
||||||
|
"ButtonViewAll": "Összes megtekintése",
|
||||||
|
"ButtonYes": "Igen",
|
||||||
|
"ErrorUploadFetchMetadataAPI": "Hiba a metaadatok lekérésekor",
|
||||||
|
"ErrorUploadFetchMetadataNoResults": "Nem sikerült a metaadatok lekérése - próbálja meg frissíteni a címet és/vagy a szerzőt",
|
||||||
|
"ErrorUploadLacksTitle": "Cím szükséges",
|
||||||
|
"HeaderAccount": "Fiók",
|
||||||
|
"HeaderAdvanced": "Haladó",
|
||||||
|
"HeaderAppriseNotificationSettings": "Apprise értesítési beállítások",
|
||||||
|
"HeaderAudiobookTools": "Hangoskönyv fájlkezelő eszközök",
|
||||||
|
"HeaderAudioTracks": "Audiósávok",
|
||||||
|
"HeaderAuthentication": "Hitelesítés",
|
||||||
|
"HeaderBackups": "Biztonsági másolatok",
|
||||||
|
"HeaderChangePassword": "Jelszó megváltoztatása",
|
||||||
|
"HeaderChapters": "Fejezetek",
|
||||||
|
"HeaderChooseAFolder": "Válasszon egy mappát",
|
||||||
|
"HeaderCollection": "Gyűjtemény",
|
||||||
|
"HeaderCollectionItems": "Gyűjtemény elemek",
|
||||||
|
"HeaderCover": "Borító",
|
||||||
|
"HeaderCurrentDownloads": "Jelenlegi letöltések",
|
||||||
|
"HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók",
|
||||||
|
"HeaderDetails": "Részletek",
|
||||||
|
"HeaderDownloadQueue": "Letöltési sor",
|
||||||
|
"HeaderEbookFiles": "E-könyv fájlok",
|
||||||
|
"HeaderEmail": "E-mail",
|
||||||
|
"HeaderEmailSettings": "E-mail beállítások",
|
||||||
|
"HeaderEpisodes": "Epizódok",
|
||||||
|
"HeaderEreaderDevices": "E-olvasó eszközök",
|
||||||
|
"HeaderEreaderSettings": "E-olvasó beállítások",
|
||||||
|
"HeaderFiles": "Fájlok",
|
||||||
|
"HeaderFindChapters": "Fejezetek keresése",
|
||||||
|
"HeaderIgnoredFiles": "Figyelmen kívül hagyott fájlok",
|
||||||
|
"HeaderItemFiles": "Elemfájlok",
|
||||||
|
"HeaderItemMetadataUtils": "Elem metaadat eszközök",
|
||||||
|
"HeaderLastListeningSession": "Utolsó hallgatási munkamenet",
|
||||||
|
"HeaderLatestEpisodes": "Legújabb epizódok",
|
||||||
|
"HeaderLibraries": "Könyvtárak",
|
||||||
|
"HeaderLibraryFiles": "Könyvtárfájlok",
|
||||||
|
"HeaderLibraryStats": "Könyvtár statisztikák",
|
||||||
|
"HeaderListeningSessions": "Hallgatási munkamenetek",
|
||||||
|
"HeaderListeningStats": "Hallgatási statisztikák",
|
||||||
|
"HeaderLogin": "Bejelentkezés",
|
||||||
|
"HeaderLogs": "Naplók",
|
||||||
|
"HeaderManageGenres": "Műfajok kezelése",
|
||||||
|
"HeaderManageTags": "Címkék kezelése",
|
||||||
|
"HeaderMapDetails": "Részletek hozzárendelése",
|
||||||
|
"HeaderMatch": "Egyeztetés",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Metaadatok előnyben részesítési sorrendje",
|
||||||
|
"HeaderMetadataToEmbed": "Beágyazandó metaadatok",
|
||||||
|
"HeaderNewAccount": "Új fiók",
|
||||||
|
"HeaderNewLibrary": "Új könyvtár",
|
||||||
|
"HeaderNotifications": "Értesítések",
|
||||||
|
"HeaderOpenIDConnectAuthentication": "OpenID Connect hitelesítés",
|
||||||
|
"HeaderOpenRSSFeed": "RSS hírcsatorna megnyitása",
|
||||||
|
"HeaderOtherFiles": "Egyéb fájlok",
|
||||||
|
"HeaderPasswordAuthentication": "Jelszó hitelesítés",
|
||||||
|
"HeaderPermissions": "Engedélyek",
|
||||||
|
"HeaderPlayerQueue": "Lejátszó sor",
|
||||||
|
"HeaderPlaylist": "Lejátszási lista",
|
||||||
|
"HeaderPlaylistItems": "Lejátszási lista elemek",
|
||||||
|
"HeaderPodcastsToAdd": "Hozzáadandó podcastok",
|
||||||
|
"HeaderPreviewCover": "Borító előnézete",
|
||||||
|
"HeaderRemoveEpisode": "Epizód eltávolítása",
|
||||||
|
"HeaderRemoveEpisodes": "{0} epizód eltávolítása",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS részletek",
|
||||||
|
"HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva",
|
||||||
|
"HeaderRSSFeeds": "RSS hírcsatornák",
|
||||||
|
"HeaderSavedMediaProgress": "Mentett médialejátszási állapot",
|
||||||
|
"HeaderSchedule": "Ütemezés",
|
||||||
|
"HeaderScheduleLibraryScans": "Könyvtárak automatikus szkennelésének ütemezése",
|
||||||
|
"HeaderSession": "Munkamenet",
|
||||||
|
"HeaderSetBackupSchedule": "Biztonsági másolatok ütemezésének beállítása",
|
||||||
|
"HeaderSettings": "Beállítások",
|
||||||
|
"HeaderSettingsDisplay": "Kijelző",
|
||||||
|
"HeaderSettingsExperimental": "Kísérleti funkciók",
|
||||||
|
"HeaderSettingsGeneral": "Általános",
|
||||||
|
"HeaderSettingsScanner": "Szkenner",
|
||||||
|
"HeaderSleepTimer": "Alvásidőzítő",
|
||||||
|
"HeaderStatsLargestItems": "Legnagyobb elemek",
|
||||||
|
"HeaderStatsLongestItems": "Leghosszabb elemek (órákban)",
|
||||||
|
"HeaderStatsMinutesListeningChart": "Hallgatási percek (az utolsó 7 napban)",
|
||||||
|
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
|
||||||
|
"HeaderStatsTop10Authors": "Top 10 szerzők",
|
||||||
|
"HeaderStatsTop5Genres": "Top 5 műfajok",
|
||||||
|
"HeaderTableOfContents": "Tartalomjegyzék",
|
||||||
|
"HeaderTools": "Eszközök",
|
||||||
|
"HeaderUpdateAccount": "Fiók frissítése",
|
||||||
|
"HeaderUpdateAuthor": "Szerző frissítése",
|
||||||
|
"HeaderUpdateDetails": "Részletek frissítése",
|
||||||
|
"HeaderUpdateLibrary": "Könyvtár frissítése",
|
||||||
|
"HeaderUsers": "Felhasználók",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
|
"HeaderYourStats": "Saját statisztikák",
|
||||||
|
"LabelAbridged": "Tömörített",
|
||||||
|
"LabelAccountType": "Fióktípus",
|
||||||
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
|
"LabelAccountTypeGuest": "Vendég",
|
||||||
|
"LabelAccountTypeUser": "Felhasználó",
|
||||||
|
"LabelActivity": "Tevékenység",
|
||||||
|
"LabelAdded": "Hozzáadva",
|
||||||
|
"LabelAddedAt": "Hozzáadás ideje",
|
||||||
|
"LabelAddToCollection": "Hozzáadás a gyűjteményhez",
|
||||||
|
"LabelAddToCollectionBatch": "{0} könyv hozzáadása a gyűjteményhez",
|
||||||
|
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
|
||||||
|
"LabelAddToPlaylistBatch": "{0} elem hozzáadása a lejátszási listához",
|
||||||
|
"LabelAdminUsersOnly": "Csak admin felhasználók",
|
||||||
|
"LabelAll": "Minden",
|
||||||
|
"LabelAllUsers": "Minden felhasználó",
|
||||||
|
"LabelAllUsersExcludingGuests": "Minden felhasználó, vendégek kivételével",
|
||||||
|
"LabelAllUsersIncludingGuests": "Minden felhasználó, beleértve a vendégeket is",
|
||||||
|
"LabelAlreadyInYourLibrary": "Már a könyvtárában van",
|
||||||
|
"LabelAppend": "Hozzáfűzés",
|
||||||
|
"LabelAuthor": "Szerző",
|
||||||
|
"LabelAuthorFirstLast": "Szerző (Keresztnév Vezetéknév)",
|
||||||
|
"LabelAuthorLastFirst": "Szerző (Vezetéknév, Keresztnév)",
|
||||||
|
"LabelAuthors": "Szerzők",
|
||||||
|
"LabelAutoDownloadEpisodes": "Epizódok automatikus letöltése",
|
||||||
|
"LabelAutoFetchMetadata": "Metaadatok automatikus lekérése",
|
||||||
|
"LabelAutoFetchMetadataHelp": "Cím, szerző és sorozat metaadatok automatikus lekérése a feltöltés megkönnyítése érdekében. További metaadatok egyeztetése szükséges lehet a feltöltés után.",
|
||||||
|
"LabelAutoLaunch": "Automatikus indítás",
|
||||||
|
"LabelAutoLaunchDescription": "Automatikus átirányítás az hitelesítő szolgáltatóhoz a bejelentkezési oldalra navigáláskor (kézi felülbírálás útvonala <code>/login?autoLaunch=0</code>)",
|
||||||
|
"LabelAutoRegister": "Automatikus regisztráció",
|
||||||
|
"LabelAutoRegisterDescription": "Új felhasználók automatikus létrehozása bejelentkezés után",
|
||||||
|
"LabelBackToUser": "Vissza a felhasználóhoz",
|
||||||
|
"LabelBackupLocation": "Biztonsági másolat helye",
|
||||||
|
"LabelBackupsEnableAutomaticBackups": "Automatikus biztonsági másolatok engedélyezése",
|
||||||
|
"LabelBackupsEnableAutomaticBackupsHelp": "Biztonsági másolatok mentése a /metadata/backups mappába",
|
||||||
|
"LabelBackupsMaxBackupSize": "Maximális biztonsági másolat méret (GB-ban)",
|
||||||
|
"LabelBackupsMaxBackupSizeHelp": "A rossz konfiguráció elleni védelem érdekében a biztonsági másolatok meghiúsulnak, ha meghaladják a beállított méretet.",
|
||||||
|
"LabelBackupsNumberToKeep": "Megtartandó biztonsági másolatok száma",
|
||||||
|
"LabelBackupsNumberToKeepHelp": "Egyszerre csak 1 biztonsági másolat kerül eltávolításra, tehát ha már több biztonsági másolat van, mint ez a szám, akkor manuálisan kell eltávolítani őket.",
|
||||||
|
"LabelBitrate": "Bitráta",
|
||||||
|
"LabelBooks": "Könyvek",
|
||||||
|
"LabelButtonText": "Gomb szövege",
|
||||||
|
"LabelChangePassword": "Jelszó megváltoztatása",
|
||||||
|
"LabelChannels": "Csatornák",
|
||||||
|
"LabelChapters": "Fejezetek",
|
||||||
|
"LabelChaptersFound": "fejezet található",
|
||||||
|
"LabelChapterTitle": "Fejezet címe",
|
||||||
|
"LabelClickForMoreInfo": "További információkért kattintson",
|
||||||
|
"LabelClosePlayer": "Lejátszó bezárása",
|
||||||
|
"LabelCodec": "Kodek",
|
||||||
|
"LabelCollapseSeries": "Sorozat összecsukása",
|
||||||
|
"LabelCollection": "Gyűjtemény",
|
||||||
|
"LabelCollections": "Gyűjtemények",
|
||||||
|
"LabelComplete": "Teljes",
|
||||||
|
"LabelConfirmPassword": "Jelszó megerősítése",
|
||||||
|
"LabelContinueListening": "Hallgatás folytatása",
|
||||||
|
"LabelContinueReading": "Olvasás folytatása",
|
||||||
|
"LabelContinueSeries": "Sorozat folytatása",
|
||||||
|
"LabelCover": "Borító",
|
||||||
|
"LabelCoverImageURL": "Borítókép URL",
|
||||||
|
"LabelCreatedAt": "Létrehozás ideje",
|
||||||
|
"LabelCronExpression": "Cron kifejezés",
|
||||||
|
"LabelCurrent": "Jelenlegi",
|
||||||
|
"LabelCurrently": "Jelenleg:",
|
||||||
|
"LabelCustomCronExpression": "Egyéni Cron kifejezés:",
|
||||||
|
"LabelDatetime": "Dátumidő",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Törlés a fájlrendszerről (ne jelölje be, ha csak az adatbázisból szeretné eltávolítani)",
|
||||||
|
"LabelDescription": "Leírás",
|
||||||
|
"LabelDeselectAll": "Minden kijelölés megszüntetése",
|
||||||
|
"LabelDevice": "Eszköz",
|
||||||
|
"LabelDeviceInfo": "Eszköz információ",
|
||||||
|
"LabelDeviceIsAvailableTo": "Eszköz elérhető a következő számára...",
|
||||||
|
"LabelDirectory": "Könyvtár",
|
||||||
|
"LabelDiscFromFilename": "Lemez a fájlnévből",
|
||||||
|
"LabelDiscFromMetadata": "Lemez a metaadatokból",
|
||||||
|
"LabelDiscover": "Felfedezés",
|
||||||
|
"LabelDownload": "Letöltés",
|
||||||
|
"LabelDownloadNEpisodes": "{0} epizód letöltése",
|
||||||
|
"LabelDuration": "Időtartam",
|
||||||
|
"LabelDurationFound": "Megtalált időtartam:",
|
||||||
|
"LabelEbook": "E-könyv",
|
||||||
|
"LabelEbooks": "E-könyvek",
|
||||||
|
"LabelEdit": "Szerkesztés",
|
||||||
|
"LabelEmail": "E-mail",
|
||||||
|
"LabelEmailSettingsFromAddress": "Feladó címe",
|
||||||
|
"LabelEmailSettingsSecure": "Biztonságos",
|
||||||
|
"LabelEmailSettingsSecureHelp": "Ha igaz, a kapcsolat TLS-t használ a szerverhez való csatlakozáskor. Ha hamis, akkor TLS-t használ, ha a szerver támogatja a STARTTLS kiterjesztést. A legtöbb esetben állítsa ezt az értéket igazra, ha a 465-ös portra csatlakozik. A 587-es vagy 25-ös port esetében tartsa hamis értéken. (a nodemailer.com/smtp/#authentication oldalról)",
|
||||||
|
"LabelEmailSettingsTestAddress": "Teszt cím",
|
||||||
|
"LabelEmbeddedCover": "Beágyazott borító",
|
||||||
|
"LabelEnable": "Engedélyezés",
|
||||||
|
"LabelEnd": "Vége",
|
||||||
|
"LabelEpisode": "Epizód",
|
||||||
|
"LabelEpisodeTitle": "Epizód címe",
|
||||||
|
"LabelEpisodeType": "Epizód típusa",
|
||||||
|
"LabelExample": "Példa",
|
||||||
|
"LabelExplicit": "Explicit",
|
||||||
|
"LabelFeedURL": "Hírcsatorna URL",
|
||||||
|
"LabelFetchingMetadata": "Metaadatok lekérése",
|
||||||
|
"LabelFile": "Fájl",
|
||||||
|
"LabelFileBirthtime": "Fájl létrehozásának ideje",
|
||||||
|
"LabelFileModified": "Fájl módosításának ideje",
|
||||||
|
"LabelFilename": "Fájlnév",
|
||||||
|
"LabelFilterByUser": "Szűrés felhasználó szerint",
|
||||||
|
"LabelFindEpisodes": "Epizódok keresése",
|
||||||
|
"LabelFinished": "Befejezett",
|
||||||
|
"LabelFolder": "Mappa",
|
||||||
|
"LabelFolders": "Mappák",
|
||||||
|
"LabelFontBold": "Félkövér",
|
||||||
|
"LabelFontFamily": "Betűtípus család",
|
||||||
|
"LabelFontItalic": "Dőlt",
|
||||||
|
"LabelFontScale": "Betűméret skála",
|
||||||
|
"LabelFontStrikethrough": "Áthúzott",
|
||||||
|
"LabelFormat": "Formátum",
|
||||||
|
"LabelGenre": "Műfaj",
|
||||||
|
"LabelGenres": "Műfajok",
|
||||||
|
"LabelHardDeleteFile": "Fájl végleges törlése",
|
||||||
|
"LabelHasEbook": "Van e-könyve",
|
||||||
|
"LabelHasSupplementaryEbook": "Van kiegészítő e-könyve",
|
||||||
|
"LabelHighestPriority": "Legmagasabb prioritás",
|
||||||
|
"LabelHost": "Hoszt",
|
||||||
|
"LabelHour": "Óra",
|
||||||
|
"LabelIcon": "Ikon",
|
||||||
|
"LabelImageURLFromTheWeb": "Kép URL a weben",
|
||||||
|
"LabelIncludeInTracklist": "Beleértve a sávlistába",
|
||||||
|
"LabelIncomplete": "Befejezetlen",
|
||||||
|
"LabelInProgress": "Folyamatban",
|
||||||
|
"LabelInterval": "Intervallum",
|
||||||
|
"LabelIntervalCustomDailyWeekly": "Egyéni napi/heti",
|
||||||
|
"LabelIntervalEvery12Hours": "Minden 12 órában",
|
||||||
|
"LabelIntervalEvery15Minutes": "Minden 15 percben",
|
||||||
|
"LabelIntervalEvery2Hours": "Minden 2 órában",
|
||||||
|
"LabelIntervalEvery30Minutes": "Minden 30 percben",
|
||||||
|
"LabelIntervalEvery6Hours": "Minden 6 órában",
|
||||||
|
"LabelIntervalEveryDay": "Minden nap",
|
||||||
|
"LabelIntervalEveryHour": "Minden órában",
|
||||||
|
"LabelInvalidParts": "Érvénytelen részek",
|
||||||
|
"LabelInvert": "Megfordítás",
|
||||||
|
"LabelItem": "Elem",
|
||||||
|
"LabelLanguage": "Nyelv",
|
||||||
|
"LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve",
|
||||||
|
"LabelLastBookAdded": "Utolsó hozzáadott könyv",
|
||||||
|
"LabelLastBookUpdated": "Utolsó frissített könyv",
|
||||||
|
"LabelLastSeen": "Utolsó látogatás",
|
||||||
|
"LabelLastTime": "Utolsó alkalom",
|
||||||
|
"LabelLastUpdate": "Utolsó frissítés",
|
||||||
|
"LabelLayout": "Elrendezés",
|
||||||
|
"LabelLayoutSinglePage": "Egyoldalas",
|
||||||
|
"LabelLayoutSplitPage": "Kétoldalas",
|
||||||
|
"LabelLess": "Kevesebb",
|
||||||
|
"LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak",
|
||||||
|
"LabelLibrary": "Könyvtár",
|
||||||
|
"LabelLibraryItem": "Könyvtári elem",
|
||||||
|
"LabelLibraryName": "Könyvtár neve",
|
||||||
|
"LabelLimit": "Korlát",
|
||||||
|
"LabelLineSpacing": "Sorköz",
|
||||||
|
"LabelListenAgain": "Újrahallgatás",
|
||||||
|
"LabelLogLevelDebug": "Debug",
|
||||||
|
"LabelLogLevelInfo": "Információ",
|
||||||
|
"LabelLogLevelWarn": "Figyelmeztetés",
|
||||||
|
"LabelLookForNewEpisodesAfterDate": "Új epizódok keresése ezen a dátum után",
|
||||||
|
"LabelLowestPriority": "Legalacsonyabb prioritás",
|
||||||
|
"LabelMatchExistingUsersBy": "Meglévő felhasználók egyeztetése",
|
||||||
|
"LabelMatchExistingUsersByDescription": "Meglévő felhasználók összekapcsolására használt. Egyszer összekapcsolva, a felhasználók egyedülálló azonosítóval lesznek egyeztetve az Ön SSO szolgáltatójától",
|
||||||
|
"LabelMediaPlayer": "Médialejátszó",
|
||||||
|
"LabelMediaType": "Média típus",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "A magasabb prioritású metaadat-források felülírják az alacsonyabb prioritásúakat",
|
||||||
|
"LabelMetadataProvider": "Metaadat-szolgáltató",
|
||||||
|
"LabelMetaTag": "Meta címke",
|
||||||
|
"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",
|
||||||
|
"LabelMoreInfo": "További információ",
|
||||||
|
"LabelName": "Név",
|
||||||
|
"LabelNarrator": "Előadó",
|
||||||
|
"LabelNarrators": "Előadók",
|
||||||
|
"LabelNew": "Új",
|
||||||
|
"LabelNewestAuthors": "Legújabb szerzők",
|
||||||
|
"LabelNewestEpisodes": "Legújabb epizódok",
|
||||||
|
"LabelNewPassword": "Új jelszó",
|
||||||
|
"LabelNextBackupDate": "Következő biztonsági másolat dátuma",
|
||||||
|
"LabelNextScheduledRun": "Következő ütemezett futtatás",
|
||||||
|
"LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok",
|
||||||
|
"LabelNotes": "Megjegyzések",
|
||||||
|
"LabelNotFinished": "Nem befejezett",
|
||||||
|
"LabelNotificationAppriseURL": "Apprise URL(ek)",
|
||||||
|
"LabelNotificationAvailableVariables": "Elérhető változók",
|
||||||
|
"LabelNotificationBodyTemplate": "Törzs sablon",
|
||||||
|
"LabelNotificationEvent": "Értesítési esemény",
|
||||||
|
"LabelNotificationsMaxFailedAttempts": "Maximális sikertelen próbálkozások",
|
||||||
|
"LabelNotificationsMaxFailedAttemptsHelp": "Az értesítések akkor kerülnek letiltásra, ha ennyiszer nem sikerül elküldeni őket",
|
||||||
|
"LabelNotificationsMaxQueueSize": "Maximális értesítési események sorának mérete",
|
||||||
|
"LabelNotificationsMaxQueueSizeHelp": "Az események korlátozva vannak, hogy másodpercenként 1-szer történjenek. Ha a sor maximális méretű, akkor az események figyelmen kívül lesznek hagyva. Ez megakadályozza az értesítések spamelését.",
|
||||||
|
"LabelNotificationTitleTemplate": "Cím sablon",
|
||||||
|
"LabelNotStarted": "Nem indult el",
|
||||||
|
"LabelNumberOfBooks": "Könyvek száma",
|
||||||
|
"LabelNumberOfEpisodes": "Epizódok száma",
|
||||||
|
"LabelOpenRSSFeed": "RSS hírcsatorna megnyitása",
|
||||||
|
"LabelOverwrite": "Felülírás",
|
||||||
|
"LabelPassword": "Jelszó",
|
||||||
|
"LabelPath": "Útvonal",
|
||||||
|
"LabelPermissionsAccessAllLibraries": "Hozzáférhet az összes könyvtárhoz",
|
||||||
|
"LabelPermissionsAccessAllTags": "Hozzáférhet az összes címkéhez",
|
||||||
|
"LabelPermissionsAccessExplicitContent": "Hozzáférhet explicit tartalomhoz",
|
||||||
|
"LabelPermissionsDelete": "Törölhet",
|
||||||
|
"LabelPermissionsDownload": "Letölthet",
|
||||||
|
"LabelPermissionsUpdate": "Frissíthet",
|
||||||
|
"LabelPermissionsUpload": "Feltölthet",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
|
"LabelPhotoPathURL": "Fénykép útvonal/URL",
|
||||||
|
"LabelPlaylists": "Lejátszási listák",
|
||||||
|
"LabelPlayMethod": "Lejátszási módszer",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
|
"LabelPodcasts": "Podcastok",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast keresési régió",
|
||||||
|
"LabelPodcastType": "Podcast típus",
|
||||||
|
"LabelPort": "Port",
|
||||||
|
"LabelPrefixesToIgnore": "Figyelmen kívül hagyandó előtagok (nem érzékeny a kis- és nagybetűkre)",
|
||||||
|
"LabelPreventIndexing": "A hírcsatorna indexelésének megakadályozása az iTunes és a Google podcast könyvtáraiban",
|
||||||
|
"LabelPrimaryEbook": "Elsődleges e-könyv",
|
||||||
|
"LabelProgress": "Haladás",
|
||||||
|
"LabelProvider": "Szolgáltató",
|
||||||
|
"LabelPubDate": "Kiadás dátuma",
|
||||||
|
"LabelPublisher": "Kiadó",
|
||||||
|
"LabelPublishYear": "Kiadás éve",
|
||||||
|
"LabelRead": "Olvasás",
|
||||||
|
"LabelReadAgain": "Újraolvasás",
|
||||||
|
"LabelReadEbookWithoutProgress": "E-könyv olvasása haladás nélkül",
|
||||||
|
"LabelRecentlyAdded": "Nemrég hozzáadva",
|
||||||
|
"LabelRecentSeries": "Legutóbbi sorozatok",
|
||||||
|
"LabelRecommended": "Ajánlott",
|
||||||
|
"LabelRedo": "Újra",
|
||||||
|
"LabelRegion": "Régió",
|
||||||
|
"LabelReleaseDate": "Megjelenés dátuma",
|
||||||
|
"LabelRemoveCover": "Borító eltávolítása",
|
||||||
|
"LabelRowsPerPage": "Sorok száma oldalanként",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
|
||||||
|
"LabelRSSFeedOpen": "RSS hírcsatorna nyitva",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Indexelés megakadályozása",
|
||||||
|
"LabelRSSFeedSlug": "RSS hírcsatorna slug",
|
||||||
|
"LabelRSSFeedURL": "RSS hírcsatorna URL",
|
||||||
|
"LabelSearchTerm": "Keresési kifejezés",
|
||||||
|
"LabelSearchTitle": "Cím keresése",
|
||||||
|
"LabelSearchTitleOrASIN": "Cím vagy ASIN keresése",
|
||||||
|
"LabelSeason": "Évad",
|
||||||
|
"LabelSelectAllEpisodes": "Összes epizód kiválasztása",
|
||||||
|
"LabelSelectEpisodesShowing": "Kiválasztás {0} megjelenített epizód",
|
||||||
|
"LabelSelectUsers": "Felhasználók kiválasztása",
|
||||||
|
"LabelSendEbookToDevice": "E-könyv küldése...",
|
||||||
|
"LabelSequence": "Sorozat",
|
||||||
|
"LabelSeries": "Sorozat",
|
||||||
|
"LabelSeriesName": "Sorozat neve",
|
||||||
|
"LabelSeriesProgress": "Sorozat haladása",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
|
"LabelSetEbookAsPrimary": "Beállítás elsődlegesként",
|
||||||
|
"LabelSetEbookAsSupplementary": "Beállítás kiegészítőként",
|
||||||
|
"LabelSettingsAudiobooksOnly": "Csak hangoskönyvek",
|
||||||
|
"LabelSettingsAudiobooksOnlyHelp": "Ennek a beállításnak az engedélyezése figyelmen kívül hagyja az e-könyv fájlokat, kivéve, ha azok egy hangoskönyv mappában vannak, ebben az esetben kiegészítő e-könyvként lesznek beállítva",
|
||||||
|
"LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal",
|
||||||
|
"LabelSettingsChromecastSupport": "Chromecast támogatás",
|
||||||
|
"LabelSettingsDateFormat": "Dátumformátum",
|
||||||
|
"LabelSettingsDisableWatcher": "Figyelő letiltása",
|
||||||
|
"LabelSettingsDisableWatcherForLibrary": "Mappafigyelő letiltása a könyvtárban",
|
||||||
|
"LabelSettingsDisableWatcherHelp": "Letiltja az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
|
||||||
|
"LabelSettingsEnableWatcher": "Figyelő engedélyezése",
|
||||||
|
"LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban",
|
||||||
|
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
|
||||||
|
"LabelSettingsExperimentalFeatures": "Kísérleti funkciók",
|
||||||
|
"LabelSettingsExperimentalFeaturesHelp": "Fejlesztés alatt álló funkciók, amelyek visszajelzésre és tesztelésre szorulnak. Kattintson a github megbeszélés megnyitásához.",
|
||||||
|
"LabelSettingsFindCovers": "Borítók keresése",
|
||||||
|
"LabelSettingsFindCoversHelp": "Ha a hangoskönyvnek nincs beágyazott borítója vagy borítóképe a mappában, a szkenner megpróbálja megtalálni a borítót.<br>Megjegyzés: Ez meghosszabbítja a szkennelési időt",
|
||||||
|
"LabelSettingsHideSingleBookSeries": "Egykönyves sorozatok elrejtése",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"LabelSettingsPreferMatchedMetadataHelp": "Az egyeztetett adatok felülírják az elem részleteit a Gyors egyeztetés használatakor. Alapértelmezés szerint a Gyors egyeztetés csak a hiányzó részleteket tölti ki.",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithASIN": "Már ASIN-nel rendelkező könyvek egyeztetésének kihagyása",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithISBN": "Már ISBN-nel rendelkező könyvek egyeztetésének kihagyása",
|
||||||
|
"LabelSettingsSortingIgnorePrefixes": "Előtagok figyelmen kívül hagyása rendezéskor",
|
||||||
|
"LabelSettingsSortingIgnorePrefixesHelp": "például az \"a\" előtag esetén a \"A könyv címe\" könyv címe \"Könyv címe, A\" szerint rendeződik",
|
||||||
|
"LabelSettingsSquareBookCovers": "Négyzet alakú könyvborítók használata",
|
||||||
|
"LabelSettingsSquareBookCoversHelp": "Négyzet alakú borítók használata az 1,6:1 arányú standard könyvborítók helyett",
|
||||||
|
"LabelSettingsStoreCoversWithItem": "Borítók tárolása az elemmel",
|
||||||
|
"LabelSettingsStoreCoversWithItemHelp": "Alapértelmezés szerint a borítók a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a borítókat a könyvtári elem mappájában tárolja. Csak egy \"cover\" nevű fájl lesz megtartva",
|
||||||
|
"LabelSettingsStoreMetadataWithItem": "Metaadatok tárolása az elemmel",
|
||||||
|
"LabelSettingsStoreMetadataWithItemHelp": "Alapértelmezés szerint a metaadatfájlok a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a metaadatfájlokat a könyvtári elem mappáiban tárolja",
|
||||||
|
"LabelSettingsTimeFormat": "Időformátum",
|
||||||
|
"LabelShowAll": "Mindent mutat",
|
||||||
|
"LabelSize": "Méret",
|
||||||
|
"LabelSleepTimer": "Alvásidőzítő",
|
||||||
|
"LabelSlug": "Rövid cím",
|
||||||
|
"LabelStart": "Kezdés",
|
||||||
|
"LabelStarted": "Elkezdődött",
|
||||||
|
"LabelStartedAt": "Kezdés ideje",
|
||||||
|
"LabelStartTime": "Kezdési idő",
|
||||||
|
"LabelStatsAudioTracks": "Audiósávok",
|
||||||
|
"LabelStatsAuthors": "Szerzők",
|
||||||
|
"LabelStatsBestDay": "Legjobb nap",
|
||||||
|
"LabelStatsDailyAverage": "Napi átlag",
|
||||||
|
"LabelStatsDays": "Napok",
|
||||||
|
"LabelStatsDaysListened": "Hallgatott napok",
|
||||||
|
"LabelStatsHours": "Órák",
|
||||||
|
"LabelStatsInARow": "egymás után",
|
||||||
|
"LabelStatsItemsFinished": "Befejezett elemek",
|
||||||
|
"LabelStatsItemsInLibrary": "Elemek a könyvtárban",
|
||||||
|
"LabelStatsMinutes": "percek",
|
||||||
|
"LabelStatsMinutesListening": "Hallgatási percek",
|
||||||
|
"LabelStatsOverallDays": "Összes nap",
|
||||||
|
"LabelStatsOverallHours": "Összes óra",
|
||||||
|
"LabelStatsWeekListening": "Heti hallgatás",
|
||||||
|
"LabelSubtitle": "Felirat",
|
||||||
|
"LabelSupportedFileTypes": "Támogatott fájltípusok",
|
||||||
|
"LabelTag": "Címke",
|
||||||
|
"LabelTags": "Címkék",
|
||||||
|
"LabelTagsAccessibleToUser": "A felhasználó számára elérhető címkék",
|
||||||
|
"LabelTagsNotAccessibleToUser": "A felhasználó számára nem elérhető címkék",
|
||||||
|
"LabelTasks": "Futó feladatok",
|
||||||
|
"LabelTextEditorBulletedList": "Pontozott lista",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Számozott lista",
|
||||||
|
"LabelTextEditorUnlink": "Link eltávolítása",
|
||||||
|
"LabelTheme": "Téma",
|
||||||
|
"LabelThemeDark": "Sötét",
|
||||||
|
"LabelThemeLight": "Világos",
|
||||||
|
"LabelTimeBase": "Időalap",
|
||||||
|
"LabelTimeListened": "Hallgatott idő",
|
||||||
|
"LabelTimeListenedToday": "Ma hallgatott idő",
|
||||||
|
"LabelTimeRemaining": "{0} maradt",
|
||||||
|
"LabelTimeToShift": "Eltolás ideje másodpercben",
|
||||||
|
"LabelTitle": "Cím",
|
||||||
|
"LabelToolsEmbedMetadata": "Metaadatok beágyazása",
|
||||||
|
"LabelToolsEmbedMetadataDescription": "Metaadatok beágyazása az audiofájlokba, beleértve a borítóképet és a fejezeteket.",
|
||||||
|
"LabelToolsMakeM4b": "M4B Hangoskönyv fájl készítése",
|
||||||
|
"LabelToolsMakeM4bDescription": ".M4B hangoskönyv fájl generálása beágyazott metaadatokkal, borítóképpel és fejezetekkel.",
|
||||||
|
"LabelToolsSplitM4b": "M4B felosztása MP3-ra",
|
||||||
|
"LabelToolsSplitM4bDescription": "MP3 fájlok létrehozása egy M4B-ből, fejezetenként felosztva, beágyazott metaadatokkal, borítóképpel és fejezetekkel.",
|
||||||
|
"LabelTotalDuration": "Teljes időtartam",
|
||||||
|
"LabelTotalTimeListened": "Teljes hallgatási idő",
|
||||||
|
"LabelTrackFromFilename": "Sáv a fájlnévből",
|
||||||
|
"LabelTrackFromMetadata": "Sáv a metaadatokból",
|
||||||
|
"LabelTracks": "Sávok",
|
||||||
|
"LabelTracksMultiTrack": "Többsávos",
|
||||||
|
"LabelTracksNone": "Nincsenek sávok",
|
||||||
|
"LabelTracksSingleTrack": "Egysávos",
|
||||||
|
"LabelType": "Típus",
|
||||||
|
"LabelUnabridged": "Nem tömörített",
|
||||||
|
"LabelUndo": "Visszavonás",
|
||||||
|
"LabelUnknown": "Ismeretlen",
|
||||||
|
"LabelUpdateCover": "Borító frissítése",
|
||||||
|
"LabelUpdateCoverHelp": "Lehetővé teszi a meglévő borítók felülírását a kiválasztott könyveknél, amikor találatot talál",
|
||||||
|
"LabelUpdatedAt": "Frissítve",
|
||||||
|
"LabelUpdateDetails": "Részletek frissítése",
|
||||||
|
"LabelUpdateDetailsHelp": "Lehetővé teszi a meglévő részletek felülírását a kiválasztott könyveknél, amikor találatot talál",
|
||||||
|
"LabelUploaderDragAndDrop": "Fájlok vagy mappák húzása és elengedése",
|
||||||
|
"LabelUploaderDropFiles": "Fájlok elengedése",
|
||||||
|
"LabelUploaderItemFetchMetadataHelp": "Cím, szerző és sorozat automatikus lekérése",
|
||||||
|
"LabelUseChapterTrack": "Fejezetsáv használata",
|
||||||
|
"LabelUseFullTrack": "Teljes sáv használata",
|
||||||
|
"LabelUser": "Felhasználó",
|
||||||
|
"LabelUsername": "Felhasználónév",
|
||||||
|
"LabelValue": "Érték",
|
||||||
|
"LabelVersion": "Verzió",
|
||||||
|
"LabelViewBookmarks": "Könyvjelzők megtekintése",
|
||||||
|
"LabelViewChapters": "Fejezetek megtekintése",
|
||||||
|
"LabelViewQueue": "Lejátszó sor megtekintése",
|
||||||
|
"LabelVolume": "Hangerő",
|
||||||
|
"LabelWeekdaysToRun": "Futás napjai",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
|
"LabelYourAudiobookDuration": "Hangoskönyv időtartama",
|
||||||
|
"LabelYourBookmarks": "Könyvjelzőid",
|
||||||
|
"LabelYourPlaylists": "Lejátszási listáid",
|
||||||
|
"LabelYourProgress": "Haladásod",
|
||||||
|
"MessageAddToPlayerQueue": "Hozzáadás a lejátszó sorhoz",
|
||||||
|
"MessageAppriseDescription": "Ennek a funkció használatához futtatnia kell egy <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> példányt vagy egy olyan API-t, amely kezeli ezeket a kéréseket. <br />Az Apprise API URL-nek a teljes URL útvonalat kell tartalmaznia az értesítés elküldéséhez, például, ha az API példánya a <code>http://192.168.1.1:8337</code> címen szolgáltatva, akkor <code>http://192.168.1.1:8337/notify</code> értéket kell megadnia.",
|
||||||
|
"MessageBackupsDescription": "A biztonsági másolatok tartalmazzák a felhasználókat, a felhasználói haladást, a könyvtári elem részleteit, a szerver beállításait és a képeket, amelyek a <code>/metadata/items</code> és <code>/metadata/authors</code> mappákban vannak tárolva. A biztonsági másolatok <strong>nem</strong> tartalmazzák a könyvtári mappákban tárolt fájlokat.",
|
||||||
|
"MessageBatchQuickMatchDescription": "A Gyors egyeztetés megpróbálja hozzáadni a hiányzó borítókat és metaadatokat a kiválasztott elemekhez. Engedélyezze az alábbi opciókat, hogy a Gyors egyeztetés felülírhassa a meglévő borítókat és/vagy metaadatokat.",
|
||||||
|
"MessageBookshelfNoCollections": "Még nem készített gyűjteményeket",
|
||||||
|
"MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre",
|
||||||
|
"MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák",
|
||||||
|
"MessageBookshelfNoSeries": "Nincsenek sorozatai",
|
||||||
|
"MessageChapterEndIsAfter": "A fejezet vége a hangoskönyv végét követi",
|
||||||
|
"MessageChapterErrorFirstNotZero": "Az első fejezetnek 0:00-kor kell kezdődnie",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Érvénytelen kezdési idő, kevesebbnek kell lennie, mint a hangoskönyv időtartama",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Érvénytelen kezdési idő, nagyobbnak kell lennie, mint az előző fejezet kezdési ideje",
|
||||||
|
"MessageChapterStartIsAfter": "A fejezet kezdete a hangoskönyv végét követi",
|
||||||
|
"MessageCheckingCron": "Cron ellenőrzése...",
|
||||||
|
"MessageConfirmCloseFeed": "Biztosan be szeretné zárni ezt a hírcsatornát?",
|
||||||
|
"MessageConfirmDeleteBackup": "Biztosan törölni szeretné a(z) {0} biztonsági másolatot?",
|
||||||
|
"MessageConfirmDeleteFile": "Ez törölni fogja a fájlt a fájlrendszerből. Biztos benne?",
|
||||||
|
"MessageConfirmDeleteLibrary": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" könyvtárat?",
|
||||||
|
"MessageConfirmDeleteLibraryItem": "Ez eltávolítja a könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?",
|
||||||
|
"MessageConfirmDeleteLibraryItems": "Ez eltávolítja a(z) {0} könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?",
|
||||||
|
"MessageConfirmDeleteSession": "Biztosan törölni szeretné ezt a munkamenetet?",
|
||||||
|
"MessageConfirmForceReScan": "Biztosan kényszeríteni szeretné az újraszkennelést?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Biztosan meg szeretné jelölni az összes epizódot befejezettnek?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Biztosan meg szeretné jelölni az összes epizódot nem befejezettnek?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét befejezettnek?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét nem befejezettnek?",
|
||||||
|
"MessageConfirmQuickEmbed": "Figyelem! A Gyors beágyazás nem készít biztonsági másolatot az audiofájlokról. Győződjön meg arról, hogy van biztonsági másolata az audiofájlokról. <br><br>Szeretné folytatni?",
|
||||||
|
"MessageConfirmRemoveAllChapters": "Biztosan eltávolítja az összes fejezetet?",
|
||||||
|
"MessageConfirmRemoveAuthor": "Biztosan eltávolítja a(z) \"{0}\" szerzőt?",
|
||||||
|
"MessageConfirmRemoveCollection": "Biztosan eltávolítja a(z) \"{0}\" gyűjteményt?",
|
||||||
|
"MessageConfirmRemoveEpisode": "Biztosan eltávolítja a(z) \"{0}\" epizódot?",
|
||||||
|
"MessageConfirmRemoveEpisodes": "Biztosan eltávolítja a(z) {0} epizódot?",
|
||||||
|
"MessageConfirmRemoveListeningSessions": "Biztosan eltávolítja a(z) {0} hallgatási munkamenetet?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Biztosan eltávolítja a(z) \"{0}\" előadót?",
|
||||||
|
"MessageConfirmRemovePlaylist": "Biztosan eltávolítja a(z) \"{0}\" lejátszási listáját?",
|
||||||
|
"MessageConfirmRenameGenre": "Biztosan át szeretné nevezni a(z) \"{0}\" műfajt \"{1}\"-re az összes elemnél?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Megjegyzés: Ez a műfaj már létezik, így össze lesznek vonva.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező műfaj már létezik \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Biztosan át szeretné nevezni a(z) \"{0}\" címkét \"{1}\"-re az összes elemnél?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Megjegyzés: Ez a címke már létezik, így össze lesznek vonva.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező címke már létezik \"{0}\".",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Biztosan újra szeretné szkennelni a(z) {0} elemet?",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Biztosan el szeretné küldeni a(z) {0} e-könyvet a(z) \"{1}\" eszközre?",
|
||||||
|
"MessageDownloadingEpisode": "Epizód letöltése",
|
||||||
|
"MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe",
|
||||||
|
"MessageEmbedFinished": "Beágyazás befejeződött!",
|
||||||
|
"MessageEpisodesQueuedForDownload": "{0} Epizód letöltésre várakozik",
|
||||||
|
"MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz",
|
||||||
|
"MessageFetching": "Lekérés...",
|
||||||
|
"MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.",
|
||||||
|
"MessageImportantNotice": "Fontos közlemény!",
|
||||||
|
"MessageInsertChapterBelow": "Fejezet beszúrása alulra",
|
||||||
|
"MessageItemsSelected": "{0} kiválasztott elem",
|
||||||
|
"MessageItemsUpdated": "{0} frissített elem",
|
||||||
|
"MessageJoinUsOn": "Csatlakozzon hozzánk: ",
|
||||||
|
"MessageListeningSessionsInTheLastYear": "{0} hallgatási munkamenet az elmúlt évben",
|
||||||
|
"MessageLoading": "Betöltés...",
|
||||||
|
"MessageLoadingFolders": "Mappák betöltése...",
|
||||||
|
"MessageM4BFailed": "M4B sikertelen!",
|
||||||
|
"MessageM4BFinished": "M4B befejeződött!",
|
||||||
|
"MessageMapChapterTitles": "Fejezetcímek hozzárendelése a meglévő hangoskönyv fejezeteihez anélkül, hogy az időbélyegeket módosítaná",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Az összes epizód megjelölése befejezettnek",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Az összes epizód megjelölése nem befejezettnek",
|
||||||
|
"MessageMarkAsFinished": "Megjelölés befejezettnek",
|
||||||
|
"MessageMarkAsNotFinished": "Megjelölés nem befejezettnek",
|
||||||
|
"MessageMatchBooksDescription": "megpróbálja egyeztetni a könyvtár könyveit egy kiválasztott keresési szolgáltató könyvével, és kitölti az üres részleteket és a borítót. Nem írja felül a részleteket.",
|
||||||
|
"MessageNoAudioTracks": "Nincsenek audiósávok",
|
||||||
|
"MessageNoAuthors": "Nincsenek szerzők",
|
||||||
|
"MessageNoBackups": "Nincsenek biztonsági másolatok",
|
||||||
|
"MessageNoBookmarks": "Nincsenek könyvjelzők",
|
||||||
|
"MessageNoChapters": "Nincsenek fejezetek",
|
||||||
|
"MessageNoCollections": "Nincsenek gyűjtemények",
|
||||||
|
"MessageNoCoversFound": "Nem találhatóak borítók",
|
||||||
|
"MessageNoDescription": "Nincs leírás",
|
||||||
|
"MessageNoDownloadsInProgress": "Jelenleg nincsenek folyamatban lévő letöltések",
|
||||||
|
"MessageNoDownloadsQueued": "Nincsenek várakozó letöltések",
|
||||||
|
"MessageNoEpisodeMatchesFound": "Nincs találat az epizódokra",
|
||||||
|
"MessageNoEpisodes": "Nincsenek epizódok",
|
||||||
|
"MessageNoFoldersAvailable": "Nincsenek elérhető mappák",
|
||||||
|
"MessageNoGenres": "Nincsenek műfajok",
|
||||||
|
"MessageNoIssues": "Nincsenek problémák",
|
||||||
|
"MessageNoItems": "Nincsenek elemek",
|
||||||
|
"MessageNoItemsFound": "Nem találhatóak elemek",
|
||||||
|
"MessageNoListeningSessions": "Nincsenek hallgatási munkamenetek",
|
||||||
|
"MessageNoLogs": "Nincsenek naplók",
|
||||||
|
"MessageNoMediaProgress": "Nincs előrehaladás a médialejátszásban",
|
||||||
|
"MessageNoNotifications": "Nincsenek értesítések",
|
||||||
|
"MessageNoPodcastsFound": "Nem találhatóak podcastok",
|
||||||
|
"MessageNoResults": "Nincsenek eredmények",
|
||||||
|
"MessageNoSearchResultsFor": "Nincs keresési eredmény erre: \"{0}\"",
|
||||||
|
"MessageNoSeries": "Nincsenek sorozatok",
|
||||||
|
"MessageNoTags": "Nincsenek címkék",
|
||||||
|
"MessageNoTasksRunning": "Nincsenek futó feladatok",
|
||||||
|
"MessageNotYetImplemented": "Még nem implementált",
|
||||||
|
"MessageNoUpdateNecessary": "Nincs szükség frissítésre",
|
||||||
|
"MessageNoUpdatesWereNecessary": "Nem volt szükség frissítésekre",
|
||||||
|
"MessageNoUserPlaylists": "Nincsenek felhasználói lejátszási listák",
|
||||||
|
"MessageOr": "vagy",
|
||||||
|
"MessagePauseChapter": "Fejezet lejátszásának szüneteltetése",
|
||||||
|
"MessagePlayChapter": "Fejezet elejének meghallgatása",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Lejátszási lista létrehozása gyűjteményből",
|
||||||
|
"MessagePodcastHasNoRSSFeedForMatching": "A podcastnak nincs RSS hírcsatorna URL-je az egyeztetéshez",
|
||||||
|
"MessageQuickMatchDescription": "Üres elem részletek és borító feltöltése az első találati eredménnyel a(z) '{0}'-ból. Nem írja felül a részleteket, kivéve, ha a 'Preferált egyeztetett metaadatok' szerverbeállítás engedélyezve van.",
|
||||||
|
"MessageRemoveChapter": "Fejezet eltávolítása",
|
||||||
|
"MessageRemoveEpisodes": "Epizód(ok) eltávolítása: {0}",
|
||||||
|
"MessageRemoveFromPlayerQueue": "Eltávolítás a lejátszási sorból",
|
||||||
|
"MessageRemoveUserWarning": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" felhasználót?",
|
||||||
|
"MessageReportBugsAndContribute": "Hibák jelentése, funkciók kérése és hozzájárulás itt:",
|
||||||
|
"MessageResetChaptersConfirm": "Biztosan alaphelyzetbe szeretné állítani a fejezeteket és visszavonni a módosításokat?",
|
||||||
|
"MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült:",
|
||||||
|
"MessageRestoreBackupWarning": "A biztonsági mentés visszaállítása felülírja az egész adatbázist, amely a /config mappában található, valamint a borítóképeket a /metadata/items és /metadata/authors mappákban.<br /><br />A biztonsági mentések nem módosítják a könyvtár mappáiban található fájlokat. Ha engedélyezte a szerverbeállításokat a borítóképek és a metaadatok könyvtármappákban való tárolására, akkor ezek nem kerülnek biztonsági mentésre vagy felülírásra.<br /><br />A szerver használó összes kliens automatikusan frissül.",
|
||||||
|
"MessageSearchResultsFor": "Keresési eredmények",
|
||||||
|
"MessageSelected": "{0} kiválasztva",
|
||||||
|
"MessageServerCouldNotBeReached": "A szervert nem lehet elérni",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Fejezetek beállítása minden egyes hangfájlt egy fejezetként használva, és a fejezet címét a hangfájl neveként",
|
||||||
|
"MessageStartPlaybackAtTime": "\"{0}\" lejátszásának kezdése {1} -tól?",
|
||||||
|
"MessageThinking": "Gondolkodás...",
|
||||||
|
"MessageUploaderItemFailed": "A feltöltés sikertelen",
|
||||||
|
"MessageUploaderItemSuccess": "Sikeresen feltöltve!",
|
||||||
|
"MessageUploading": "Feltöltés...",
|
||||||
|
"MessageValidCronExpression": "Érvényes cron kifejezés",
|
||||||
|
"MessageWatcherIsDisabledGlobally": "A megfigyelő globálisan le van tiltva a szerver beállításokban",
|
||||||
|
"MessageXLibraryIsEmpty": "{0} könyvtár üres!",
|
||||||
|
"MessageYourAudiobookDurationIsLonger": "Az Ön hangoskönyvének hossza hosszabb, mint a talált időtartam",
|
||||||
|
"MessageYourAudiobookDurationIsShorter": "Az Ön hangoskönyvének hossza rövidebb, mint a talált időtartam",
|
||||||
|
"NoteChangeRootPassword": "A Root felhasználó az egyetlen felhasználó, akinek lehet üres jelszava",
|
||||||
|
"NoteChapterEditorTimes": "Megjegyzés: Az első fejezet kezdőidejének 0:00 kell lennie, és az utolsó fejezet kezdőideje nem haladhatja meg a hangoskönyv időtartamát.",
|
||||||
|
"NoteFolderPicker": "Megjegyzés: azok a mappák, amelyek már hozzá vannak rendelve, nem jelennek meg",
|
||||||
|
"NoteRSSFeedPodcastAppsHttps": "Figyelem: A legtöbb podcast alkalmazás megköveteli, hogy az RSS feed URL HTTPS-t használjon",
|
||||||
|
"NoteRSSFeedPodcastAppsPubDate": "Figyelem: Az egy vagy több epizódnak nincs Közzétételi dátuma. Néhány podcast alkalmazás ezt megköveteli.",
|
||||||
|
"NoteUploaderFoldersWithMediaFiles": "A médiafájlokat tartalmazó mappák külön könyvtári tételekként lesznek kezelve.",
|
||||||
|
"NoteUploaderOnlyAudioFiles": "Ha csak hangfájlokat tölt fel, akkor minden egyes hangfájl külön hangoskönyvként lesz kezelve.",
|
||||||
|
"NoteUploaderUnsupportedFiles": "A nem támogatott fájlok figyelmen kívül hagyásra kerülnek. Mappa kiválasztása vagy elengedésekor az elem mappáján kívüli egyéb fájlok figyelmen kívül lesznek hagyva.",
|
||||||
|
"PlaceholderNewCollection": "Új gyűjtemény neve",
|
||||||
|
"PlaceholderNewFolderPath": "Új mappa útvonala",
|
||||||
|
"PlaceholderNewPlaylist": "Új lejátszási lista neve",
|
||||||
|
"PlaceholderSearch": "Keresés..",
|
||||||
|
"PlaceholderSearchEpisode": "Epizód keresése..",
|
||||||
|
"ToastAccountUpdateFailed": "A fiók frissítése sikertelen",
|
||||||
|
"ToastAccountUpdateSuccess": "Fiók frissítve",
|
||||||
|
"ToastAuthorImageRemoveFailed": "A kép eltávolítása sikertelen",
|
||||||
|
"ToastAuthorImageRemoveSuccess": "Szerző képe eltávolítva",
|
||||||
|
"ToastAuthorUpdateFailed": "A szerző frissítése sikertelen",
|
||||||
|
"ToastAuthorUpdateMerged": "Szerző összevonva",
|
||||||
|
"ToastAuthorUpdateSuccess": "Szerző frissítve",
|
||||||
|
"ToastAuthorUpdateSuccessNoImageFound": "Szerző frissítve (nem található kép)",
|
||||||
|
"ToastBackupCreateFailed": "A biztonsági mentés létrehozása sikertelen",
|
||||||
|
"ToastBackupCreateSuccess": "Biztonsági mentés létrehozva",
|
||||||
|
"ToastBackupDeleteFailed": "A biztonsági mentés törlése sikertelen",
|
||||||
|
"ToastBackupDeleteSuccess": "Biztonsági mentés törölve",
|
||||||
|
"ToastBackupRestoreFailed": "A biztonsági mentés visszaállítása sikertelen",
|
||||||
|
"ToastBackupUploadFailed": "A biztonsági mentés feltöltése sikertelen",
|
||||||
|
"ToastBackupUploadSuccess": "Biztonsági mentés feltöltve",
|
||||||
|
"ToastBatchUpdateFailed": "Kötegelt frissítés sikertelen",
|
||||||
|
"ToastBatchUpdateSuccess": "Kötegelt frissítés sikeres",
|
||||||
|
"ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen",
|
||||||
|
"ToastBookmarkCreateSuccess": "Könyvjelző hozzáadva",
|
||||||
|
"ToastBookmarkRemoveFailed": "Könyvjelző eltávolítása sikertelen",
|
||||||
|
"ToastBookmarkRemoveSuccess": "Könyvjelző eltávolítva",
|
||||||
|
"ToastBookmarkUpdateFailed": "Könyvjelző frissítése sikertelen",
|
||||||
|
"ToastBookmarkUpdateSuccess": "Könyvjelző frissítve",
|
||||||
|
"ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak",
|
||||||
|
"ToastChaptersMustHaveTitles": "A fejezeteknek címekkel kell rendelkezniük",
|
||||||
|
"ToastCollectionItemsRemoveFailed": "Elem(ek) eltávolítása a gyűjteményből sikertelen",
|
||||||
|
"ToastCollectionItemsRemoveSuccess": "Elem(ek) eltávolítva a gyűjteményből",
|
||||||
|
"ToastCollectionRemoveFailed": "Gyűjtemény eltávolítása sikertelen",
|
||||||
|
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
|
||||||
|
"ToastCollectionUpdateFailed": "Gyűjtemény frissítése sikertelen",
|
||||||
|
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
|
||||||
|
"ToastItemCoverUpdateFailed": "Elem borítójának frissítése sikertelen",
|
||||||
|
"ToastItemCoverUpdateSuccess": "Elem borítója frissítve",
|
||||||
|
"ToastItemDetailsUpdateFailed": "Elem részleteinek frissítése sikertelen",
|
||||||
|
"ToastItemDetailsUpdateSuccess": "Elem részletei frissítve",
|
||||||
|
"ToastItemDetailsUpdateUnneeded": "Nincsenek szükséges frissítések a tétel részletein",
|
||||||
|
"ToastItemMarkedAsFinishedFailed": "Megjelölés Befejezettként sikertelen",
|
||||||
|
"ToastItemMarkedAsFinishedSuccess": "Elem megjelölve Befejezettként",
|
||||||
|
"ToastItemMarkedAsNotFinishedFailed": "Nem sikerült Nem Befejezettként megjelölni az elemet",
|
||||||
|
"ToastItemMarkedAsNotFinishedSuccess": "Elem megjelölve Nem Befejezettként",
|
||||||
|
"ToastLibraryCreateFailed": "Könyvtár létrehozása sikertelen",
|
||||||
|
"ToastLibraryCreateSuccess": "\"{0}\" könyvtár létrehozva",
|
||||||
|
"ToastLibraryDeleteFailed": "Könyvtár törlése sikertelen",
|
||||||
|
"ToastLibraryDeleteSuccess": "Könyvtár törölve",
|
||||||
|
"ToastLibraryScanFailedToStart": "A beolvasás elindítása sikertelen",
|
||||||
|
"ToastLibraryScanStarted": "Könyvtár beolvasása elindítva",
|
||||||
|
"ToastLibraryUpdateFailed": "Könyvtár frissítése sikertelen",
|
||||||
|
"ToastLibraryUpdateSuccess": "\"{0}\" könyvtár frissítve",
|
||||||
|
"ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen",
|
||||||
|
"ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva",
|
||||||
|
"ToastPlaylistRemoveFailed": "Lejátszási lista eltávolítása sikertelen",
|
||||||
|
"ToastPlaylistRemoveSuccess": "Lejátszási lista eltávolítva",
|
||||||
|
"ToastPlaylistUpdateFailed": "Lejátszási lista frissítése sikertelen",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Lejátszási lista frissítve",
|
||||||
|
"ToastPodcastCreateFailed": "Podcast létrehozása sikertelen",
|
||||||
|
"ToastPodcastCreateSuccess": "Podcast sikeresen létrehozva",
|
||||||
|
"ToastRemoveItemFromCollectionFailed": "Tétel eltávolítása a gyűjteményből sikertelen",
|
||||||
|
"ToastRemoveItemFromCollectionSuccess": "Tétel eltávolítva a gyűjteményből",
|
||||||
|
"ToastRSSFeedCloseFailed": "RSS feed bezárása sikertelen",
|
||||||
|
"ToastRSSFeedCloseSuccess": "RSS feed bezárva",
|
||||||
|
"ToastSendEbookToDeviceFailed": "E-könyv küldése az eszközre sikertelen",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "E-könyv elküldve az eszközre \"{0}\"",
|
||||||
|
"ToastSeriesUpdateFailed": "Sorozat frissítése sikertelen",
|
||||||
|
"ToastSeriesUpdateSuccess": "Sorozat frissítése sikeres",
|
||||||
|
"ToastSessionDeleteFailed": "Munkamenet törlése sikertelen",
|
||||||
|
"ToastSessionDeleteSuccess": "Munkamenet törölve",
|
||||||
|
"ToastSocketConnected": "Socket csatlakoztatva",
|
||||||
|
"ToastSocketDisconnected": "Socket lecsatlakoztatva",
|
||||||
|
"ToastSocketFailedToConnect": "A Socket csatlakoztatása sikertelen",
|
||||||
|
"ToastUserDeleteFailed": "Felhasználó törlése sikertelen",
|
||||||
|
"ToastUserDeleteSuccess": "Felhasználó törölve"
|
||||||
|
}
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Nascondi",
|
"ButtonHide": "Nascondi",
|
||||||
"ButtonHome": "Home",
|
"ButtonHome": "Home",
|
||||||
"ButtonIssues": "Errori",
|
"ButtonIssues": "Errori",
|
||||||
|
"ButtonJumpBackward": "Salta indietro",
|
||||||
|
"ButtonJumpForward": "Salta Avanti",
|
||||||
"ButtonLatest": "Ultimi",
|
"ButtonLatest": "Ultimi",
|
||||||
"ButtonLibrary": "Libreria",
|
"ButtonLibrary": "Libreria",
|
||||||
"ButtonLogout": "Disconnetti",
|
"ButtonLogout": "Disconnetti",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Aggiungi metadata agli Autori",
|
"ButtonMatchAllAuthors": "Aggiungi metadata agli Autori",
|
||||||
"ButtonMatchBooks": "Aggiungi metadata della Libreria",
|
"ButtonMatchBooks": "Aggiungi metadata della Libreria",
|
||||||
"ButtonNevermind": "Nevermind",
|
"ButtonNevermind": "Nevermind",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Prossimo Capitolo",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Apri Feed",
|
"ButtonOpenFeed": "Apri Feed",
|
||||||
"ButtonOpenManager": "Apri Manager",
|
"ButtonOpenManager": "Apri Manager",
|
||||||
|
"ButtonPause": "Pausa",
|
||||||
"ButtonPlay": "Play",
|
"ButtonPlay": "Play",
|
||||||
"ButtonPlaying": "In Riproduzione",
|
"ButtonPlaying": "In Riproduzione",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Playlists",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Capitolo Precendente",
|
||||||
"ButtonPurgeAllCache": "Elimina tutta la Cache",
|
"ButtonPurgeAllCache": "Elimina tutta la Cache",
|
||||||
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
|
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
|
||||||
"ButtonPurgeMediaProgress": "Elimina info dei media ascoltati",
|
"ButtonPurgeMediaProgress": "Elimina info dei media ascoltati",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
|
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
|
||||||
"ButtonQuickMatch": "Controlla Metadata Auto",
|
"ButtonQuickMatch": "Controlla Metadata Auto",
|
||||||
"ButtonRead": "Leggi",
|
"ButtonRead": "Leggi",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Rimuovi",
|
"ButtonRemove": "Rimuovi",
|
||||||
"ButtonRemoveAll": "Rimuovi Tutto",
|
"ButtonRemoveAll": "Rimuovi Tutto",
|
||||||
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
"ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce",
|
"ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Ricerca veloce",
|
"ButtonShiftTimes": "Ricerca veloce",
|
||||||
"ButtonShow": "Mostra",
|
"ButtonShow": "Mostra",
|
||||||
"ButtonStartM4BEncode": "Inizia L'Encode del M4B",
|
"ButtonStartM4BEncode": "Inizia L'Encode del M4B",
|
||||||
@ -87,15 +96,15 @@
|
|||||||
"ButtonUserEdit": "Modifica Utente {0}",
|
"ButtonUserEdit": "Modifica Utente {0}",
|
||||||
"ButtonViewAll": "Mostra Tutto",
|
"ButtonViewAll": "Mostra Tutto",
|
||||||
"ButtonYes": "Si",
|
"ButtonYes": "Si",
|
||||||
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
|
"ErrorUploadFetchMetadataAPI": "Errore Recupero metadati",
|
||||||
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
|
"ErrorUploadFetchMetadataNoResults": "Impossibile recuperare i metadati: prova a modificate il titolo e/o l'autore",
|
||||||
"ErrorUploadLacksTitle": "Must have a title",
|
"ErrorUploadLacksTitle": "Deve avere un titolo",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
"HeaderAdvanced": "Avanzate",
|
"HeaderAdvanced": "Avanzate",
|
||||||
"HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica",
|
"HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica",
|
||||||
"HeaderAudiobookTools": "Utilità Audiobook File Management",
|
"HeaderAudiobookTools": "Utilità Audiobook File Management",
|
||||||
"HeaderAudioTracks": "Tracce Audio",
|
"HeaderAudioTracks": "Tracce Audio",
|
||||||
"HeaderAuthentication": "Authentication",
|
"HeaderAuthentication": "Authenticazione",
|
||||||
"HeaderBackups": "Backup",
|
"HeaderBackups": "Backup",
|
||||||
"HeaderChangePassword": "Cambia Password",
|
"HeaderChangePassword": "Cambia Password",
|
||||||
"HeaderChapters": "Capitoli",
|
"HeaderChapters": "Capitoli",
|
||||||
@ -104,8 +113,9 @@
|
|||||||
"HeaderCollectionItems": "Elementi della Raccolta",
|
"HeaderCollectionItems": "Elementi della Raccolta",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
"HeaderCurrentDownloads": "Download Correnti",
|
"HeaderCurrentDownloads": "Download Correnti",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Dettagli",
|
"HeaderDetails": "Dettagli",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download coda",
|
||||||
"HeaderEbookFiles": "Ebook File",
|
"HeaderEbookFiles": "Ebook File",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmailSettings": "Email Settings",
|
||||||
@ -130,7 +140,7 @@
|
|||||||
"HeaderManageTags": "Gestisci Tags",
|
"HeaderManageTags": "Gestisci Tags",
|
||||||
"HeaderMapDetails": "Mappa Dettagli",
|
"HeaderMapDetails": "Mappa Dettagli",
|
||||||
"HeaderMatch": "Trova Corrispondenza",
|
"HeaderMatch": "Trova Corrispondenza",
|
||||||
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
"HeaderMetadataOrderOfPrecedence": "Priorità ordine Metadata",
|
||||||
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
||||||
"HeaderNewAccount": "Nuovo Account",
|
"HeaderNewAccount": "Nuovo Account",
|
||||||
"HeaderNewLibrary": "Nuova Libreria",
|
"HeaderNewLibrary": "Nuova Libreria",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Aggiorna Dettagli",
|
"HeaderUpdateDetails": "Aggiorna Dettagli",
|
||||||
"HeaderUpdateLibrary": "Aggiorna Libreria",
|
"HeaderUpdateLibrary": "Aggiorna Libreria",
|
||||||
"HeaderUsers": "Utenti",
|
"HeaderUsers": "Utenti",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Statistiche Personali",
|
"HeaderYourStats": "Statistiche Personali",
|
||||||
"LabelAbridged": "Abbreviato",
|
"LabelAbridged": "Abbreviato",
|
||||||
"LabelAccountType": "Tipo di Account",
|
"LabelAccountType": "Tipo di Account",
|
||||||
@ -199,12 +210,12 @@
|
|||||||
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
||||||
"LabelAuthors": "Autori",
|
"LabelAuthors": "Autori",
|
||||||
"LabelAutoDownloadEpisodes": "Auto Download Episodi",
|
"LabelAutoDownloadEpisodes": "Auto Download Episodi",
|
||||||
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
|
"LabelAutoFetchMetadata": "Auto controllo Metadata",
|
||||||
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
|
"LabelAutoFetchMetadataHelp": "Recupera i metadati per titolo, autore e serie per semplificare il caricamento. Potrebbe essere necessario abbinare metadati aggiuntivi dopo il caricamento.",
|
||||||
"LabelAutoLaunch": "Auto Launch",
|
"LabelAutoLaunch": "Auto Launch",
|
||||||
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
|
"LabelAutoLaunchDescription": "Reindirizzamento automatico al provider di autenticazione quando si accede alla pagina di accesso (percorso di sostituzione manuale <code>/login?autoLaunch=0</code>)",
|
||||||
"LabelAutoRegister": "Auto Register",
|
"LabelAutoRegister": "Auto Registrazione",
|
||||||
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
|
"LabelAutoRegisterDescription": "Crea automaticamente nuovi utenti dopo aver effettuato l'accesso",
|
||||||
"LabelBackToUser": "Torna a Utenti",
|
"LabelBackToUser": "Torna a Utenti",
|
||||||
"LabelBackupLocation": "Percorso del Backup",
|
"LabelBackupLocation": "Percorso del Backup",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico",
|
"LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico",
|
||||||
@ -215,7 +226,7 @@
|
|||||||
"LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.",
|
"LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.",
|
||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Libri",
|
"LabelBooks": "Libri",
|
||||||
"LabelButtonText": "Button Text",
|
"LabelButtonText": "Buttone Testo",
|
||||||
"LabelChangePassword": "Cambia Password",
|
"LabelChangePassword": "Cambia Password",
|
||||||
"LabelChannels": "Canali",
|
"LabelChannels": "Canali",
|
||||||
"LabelChapters": "Capitoli",
|
"LabelChapters": "Capitoli",
|
||||||
@ -271,7 +282,7 @@
|
|||||||
"LabelExample": "Esempio",
|
"LabelExample": "Esempio",
|
||||||
"LabelExplicit": "Esplicito",
|
"LabelExplicit": "Esplicito",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFetchingMetadata": "Fetching Metadata",
|
"LabelFetchingMetadata": "Recupero dei metadati",
|
||||||
"LabelFile": "File",
|
"LabelFile": "File",
|
||||||
"LabelFileBirthtime": "Data Creazione",
|
"LabelFileBirthtime": "Data Creazione",
|
||||||
"LabelFileModified": "Ultima modifica",
|
"LabelFileModified": "Ultima modifica",
|
||||||
@ -292,7 +303,7 @@
|
|||||||
"LabelHardDeleteFile": "Elimina Definitivamente",
|
"LabelHardDeleteFile": "Elimina Definitivamente",
|
||||||
"LabelHasEbook": "Un ebook",
|
"LabelHasEbook": "Un ebook",
|
||||||
"LabelHasSupplementaryEbook": "Un ebook Supplementare",
|
"LabelHasSupplementaryEbook": "Un ebook Supplementare",
|
||||||
"LabelHighestPriority": "Highest priority",
|
"LabelHighestPriority": "Priorità Massima",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Ora",
|
"LabelHour": "Ora",
|
||||||
"LabelIcon": "Icona",
|
"LabelIcon": "Icona",
|
||||||
@ -334,20 +345,22 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Allarme",
|
"LabelLogLevelWarn": "Allarme",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
||||||
"LabelLowestPriority": "Lowest Priority",
|
"LabelLowestPriority": "Priorità Minima",
|
||||||
"LabelMatchExistingUsersBy": "Match existing users by",
|
"LabelMatchExistingUsersBy": "Abbina gli utenti esistenti per",
|
||||||
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
|
"LabelMatchExistingUsersByDescription": "Utilizzato per connettere gli utenti esistenti. Una volta connessi, gli utenti verranno abbinati a un ID univoco dal tuo provider SSO",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Tipo Media",
|
"LabelMediaType": "Tipo Media",
|
||||||
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
|
"LabelMetadataOrderOfPrecedenceDescription": "Le origini di metadati con priorità più alta sovrascriveranno le origini di metadati con priorità inferiore",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Meta Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMissing": "Altro",
|
"LabelMissing": "Altro",
|
||||||
"LabelMissingParts": "Parti rimantenti",
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"LabelMissingParts": "Parti rimanenti",
|
||||||
"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.",
|
"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",
|
"LabelMore": "Molto",
|
||||||
"LabelMoreInfo": "Più Info",
|
"LabelMoreInfo": "Più Info",
|
||||||
"LabelName": "Nome",
|
"LabelName": "Nome",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Può Scaricare",
|
"LabelPermissionsDownload": "Può Scaricare",
|
||||||
"LabelPermissionsUpdate": "Può Aggiornare",
|
"LabelPermissionsUpdate": "Può Aggiornare",
|
||||||
"LabelPermissionsUpload": "Può caricare",
|
"LabelPermissionsUpload": "Può caricare",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "foto Path/URL",
|
"LabelPhotoPathURL": "foto Path/URL",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Metodo di riproduzione",
|
"LabelPlayMethod": "Metodo di riproduzione",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Area di ricerca podcast",
|
||||||
"LabelPodcastType": "Tipo di Podcast",
|
"LabelPodcastType": "Tipo di Podcast",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||||
@ -406,7 +421,7 @@
|
|||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRecommended": "Raccomandati",
|
"LabelRecommended": "Raccomandati",
|
||||||
"LabelRedo": "Redo",
|
"LabelRedo": "Rifai",
|
||||||
"LabelRegion": "Regione",
|
"LabelRegion": "Regione",
|
||||||
"LabelReleaseDate": "Data Release",
|
"LabelReleaseDate": "Data Release",
|
||||||
"LabelRemoveCover": "Rimuovi cover",
|
"LabelRemoveCover": "Rimuovi cover",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Nome Serie",
|
"LabelSeriesName": "Nome Serie",
|
||||||
"LabelSeriesProgress": "Cominciato",
|
"LabelSeriesProgress": "Cominciato",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Immposta come Primario",
|
"LabelSetEbookAsPrimary": "Immposta come Primario",
|
||||||
"LabelSetEbookAsSupplementary": "Imposta come Suplementare",
|
"LabelSetEbookAsSupplementary": "Imposta come Suplementare",
|
||||||
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
||||||
@ -468,7 +484,7 @@
|
|||||||
"LabelShowAll": "Mostra Tutto",
|
"LabelShowAll": "Mostra Tutto",
|
||||||
"LabelSize": "Dimensione",
|
"LabelSize": "Dimensione",
|
||||||
"LabelSleepTimer": "Sleep timer",
|
"LabelSleepTimer": "Sleep timer",
|
||||||
"LabelSlug": "Slug",
|
"LabelSlug": "Lento",
|
||||||
"LabelStart": "Inizo",
|
"LabelStart": "Inizo",
|
||||||
"LabelStarted": "Iniziato",
|
"LabelStarted": "Iniziato",
|
||||||
"LabelStartedAt": "Iniziato al",
|
"LabelStartedAt": "Iniziato al",
|
||||||
@ -495,9 +511,9 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
|
"LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
|
||||||
"LabelTasks": "Processi in esecuzione",
|
"LabelTasks": "Processi in esecuzione",
|
||||||
"LabelTextEditorBulletedList": "Bulleted list",
|
"LabelTextEditorBulletedList": "Elenco puntato",
|
||||||
"LabelTextEditorLink": "Link",
|
"LabelTextEditorLink": "Link",
|
||||||
"LabelTextEditorNumberedList": "Numbered list",
|
"LabelTextEditorNumberedList": "Elenco Numerato",
|
||||||
"LabelTextEditorUnlink": "Unlink",
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Scuro",
|
"LabelThemeDark": "Scuro",
|
||||||
@ -520,7 +536,7 @@
|
|||||||
"LabelTrackFromMetadata": "Traccia da Metadata",
|
"LabelTrackFromMetadata": "Traccia da Metadata",
|
||||||
"LabelTracks": "Traccia",
|
"LabelTracks": "Traccia",
|
||||||
"LabelTracksMultiTrack": "Multi-traccia",
|
"LabelTracksMultiTrack": "Multi-traccia",
|
||||||
"LabelTracksNone": "No tracks",
|
"LabelTracksNone": "Nessuna traccia",
|
||||||
"LabelTracksSingleTrack": "Traccia-singola",
|
"LabelTracksSingleTrack": "Traccia-singola",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnabridged": "Integrale",
|
"LabelUnabridged": "Integrale",
|
||||||
@ -533,7 +549,7 @@
|
|||||||
"LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
|
"LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
|
||||||
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
||||||
"LabelUploaderDropFiles": "Elimina file",
|
"LabelUploaderDropFiles": "Elimina file",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
|
"LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
|
||||||
"LabelUseChapterTrack": "Usa il Capitolo della Traccia",
|
"LabelUseChapterTrack": "Usa il Capitolo della Traccia",
|
||||||
"LabelUseFullTrack": "Usa la traccia totale",
|
"LabelUseFullTrack": "Usa la traccia totale",
|
||||||
"LabelUser": "Utente",
|
"LabelUser": "Utente",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Visualizza coda",
|
"LabelViewQueue": "Visualizza coda",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
||||||
"LabelYourBookmarks": "I tuoi Preferiti",
|
"LabelYourBookmarks": "I tuoi Preferiti",
|
||||||
"LabelYourPlaylists": "le tue Playlist",
|
"LabelYourPlaylists": "le tue Playlist",
|
||||||
@ -581,7 +599,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
"MessageConfirmRemoveListeningSessions": "Sei sicuro di voler rimuovere {0} sessioni di Ascolto?",
|
||||||
"MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
@ -661,7 +679,7 @@
|
|||||||
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
||||||
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
||||||
"MessageSearchResultsFor": "cerca risultati per",
|
"MessageSearchResultsFor": "cerca risultati per",
|
||||||
"MessageSelected": "{0} selected",
|
"MessageSelected": "{0} selezionati",
|
||||||
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
||||||
"MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio",
|
"MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio",
|
||||||
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
||||||
@ -750,7 +768,7 @@
|
|||||||
"ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo",
|
"ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo",
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito",
|
"ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito",
|
||||||
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
"ToastSeriesUpdateSuccess": "Serie Aggiornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||||
"ToastSocketConnected": "Socket connesso",
|
"ToastSocketConnected": "Socket connesso",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Slėpti",
|
"ButtonHide": "Slėpti",
|
||||||
"ButtonHome": "Pradžia",
|
"ButtonHome": "Pradžia",
|
||||||
"ButtonIssues": "Problemos",
|
"ButtonIssues": "Problemos",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Naujausias",
|
"ButtonLatest": "Naujausias",
|
||||||
"ButtonLibrary": "Biblioteka",
|
"ButtonLibrary": "Biblioteka",
|
||||||
"ButtonLogout": "Atsijungti",
|
"ButtonLogout": "Atsijungti",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Pritaikyti visus autorius",
|
"ButtonMatchAllAuthors": "Pritaikyti visus autorius",
|
||||||
"ButtonMatchBooks": "Pritaikyti knygas",
|
"ButtonMatchBooks": "Pritaikyti knygas",
|
||||||
"ButtonNevermind": "Nesvarbu",
|
"ButtonNevermind": "Nesvarbu",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Atidaryti srautą",
|
"ButtonOpenFeed": "Atidaryti srautą",
|
||||||
"ButtonOpenManager": "Atidaryti tvarkyklę",
|
"ButtonOpenManager": "Atidaryti tvarkyklę",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Groti",
|
"ButtonPlay": "Groti",
|
||||||
"ButtonPlaying": "Grojama",
|
"ButtonPlaying": "Grojama",
|
||||||
"ButtonPlaylists": "Grojaraščiai",
|
"ButtonPlaylists": "Grojaraščiai",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Valyti visą saugyklą",
|
"ButtonPurgeAllCache": "Valyti visą saugyklą",
|
||||||
"ButtonPurgeItemsCache": "Valyti elementų saugyklą",
|
"ButtonPurgeItemsCache": "Valyti elementų saugyklą",
|
||||||
"ButtonPurgeMediaProgress": "Valyti medijos progresą",
|
"ButtonPurgeMediaProgress": "Valyti medijos progresą",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Pašalinti iš eilės",
|
"ButtonQueueRemoveItem": "Pašalinti iš eilės",
|
||||||
"ButtonQuickMatch": "Greitas pritaikymas",
|
"ButtonQuickMatch": "Greitas pritaikymas",
|
||||||
"ButtonRead": "Skaityti",
|
"ButtonRead": "Skaityti",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Pašalinti",
|
"ButtonRemove": "Pašalinti",
|
||||||
"ButtonRemoveAll": "Pašalinti viską",
|
"ButtonRemoveAll": "Pašalinti viską",
|
||||||
"ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus",
|
"ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Pasirinkti aplanko kelią",
|
"ButtonSelectFolderPath": "Pasirinkti aplanko kelią",
|
||||||
"ButtonSeries": "Serijos",
|
"ButtonSeries": "Serijos",
|
||||||
"ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių",
|
"ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Perstumti laikus",
|
"ButtonShiftTimes": "Perstumti laikus",
|
||||||
"ButtonShow": "Rodyti",
|
"ButtonShow": "Rodyti",
|
||||||
"ButtonStartM4BEncode": "Pradėti M4B kodavimą",
|
"ButtonStartM4BEncode": "Pradėti M4B kodavimą",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Kolekcijos elementai",
|
"HeaderCollectionItems": "Kolekcijos elementai",
|
||||||
"HeaderCover": "Viršelis",
|
"HeaderCover": "Viršelis",
|
||||||
"HeaderCurrentDownloads": "Dabartiniai parsisiuntimai",
|
"HeaderCurrentDownloads": "Dabartiniai parsisiuntimai",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Detalės",
|
"HeaderDetails": "Detalės",
|
||||||
"HeaderDownloadQueue": "Parsisiuntimo eilė",
|
"HeaderDownloadQueue": "Parsisiuntimo eilė",
|
||||||
"HeaderEbookFiles": "Eknygos failai",
|
"HeaderEbookFiles": "Eknygos failai",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Atnaujinti informaciją",
|
"HeaderUpdateDetails": "Atnaujinti informaciją",
|
||||||
"HeaderUpdateLibrary": "Atnaujinti biblioteką",
|
"HeaderUpdateLibrary": "Atnaujinti biblioteką",
|
||||||
"HeaderUsers": "Naudotojai",
|
"HeaderUsers": "Naudotojai",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Jūsų statistika",
|
"HeaderYourStats": "Jūsų statistika",
|
||||||
"LabelAbridged": "Santrauka",
|
"LabelAbridged": "Santrauka",
|
||||||
"LabelAccountType": "Paskyros tipas",
|
"LabelAccountType": "Paskyros tipas",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta žymos",
|
"LabelMetaTags": "Meta žymos",
|
||||||
"LabelMinute": "Minutė",
|
"LabelMinute": "Minutė",
|
||||||
"LabelMissing": "Trūksta",
|
"LabelMissing": "Trūksta",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Trūkstamos dalys",
|
"LabelMissingParts": "Trūkstamos dalys",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Daugiau",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Gali atsisiųsti",
|
"LabelPermissionsDownload": "Gali atsisiųsti",
|
||||||
"LabelPermissionsUpdate": "Gali atnaujinti",
|
"LabelPermissionsUpdate": "Gali atnaujinti",
|
||||||
"LabelPermissionsUpload": "Gali įkelti",
|
"LabelPermissionsUpload": "Gali įkelti",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Nuotraukos kelias/URL",
|
"LabelPhotoPathURL": "Nuotraukos kelias/URL",
|
||||||
"LabelPlaylists": "Grojaraščiai",
|
"LabelPlaylists": "Grojaraščiai",
|
||||||
"LabelPlayMethod": "Grojimo metodas",
|
"LabelPlayMethod": "Grojimo metodas",
|
||||||
"LabelPodcast": "Tinklalaidė",
|
"LabelPodcast": "Tinklalaidė",
|
||||||
"LabelPodcasts": "Tinklalaidės",
|
"LabelPodcasts": "Tinklalaidės",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast paieškos regionas",
|
||||||
"LabelPodcastType": "Tinklalaidės tipas",
|
"LabelPodcastType": "Tinklalaidės tipas",
|
||||||
"LabelPort": "Prievadas",
|
"LabelPort": "Prievadas",
|
||||||
"LabelPrefixesToIgnore": "Ignoruojami priešdėliai (didžiosios/mažosios nesvarbu)",
|
"LabelPrefixesToIgnore": "Ignoruojami priešdėliai (didžiosios/mažosios nesvarbu)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serija",
|
"LabelSeries": "Serija",
|
||||||
"LabelSeriesName": "Serijos pavadinimas",
|
"LabelSeriesName": "Serijos pavadinimas",
|
||||||
"LabelSeriesProgress": "Serijos progresas",
|
"LabelSeriesProgress": "Serijos progresas",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Nustatyti kaip pagrindinę",
|
"LabelSetEbookAsPrimary": "Nustatyti kaip pagrindinę",
|
||||||
"LabelSetEbookAsSupplementary": "Nustatyti kaip papildomą",
|
"LabelSetEbookAsSupplementary": "Nustatyti kaip papildomą",
|
||||||
"LabelSettingsAudiobooksOnly": "Tik garso knygos",
|
"LabelSettingsAudiobooksOnly": "Tik garso knygos",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Peržiūrėti grotuvo eilę",
|
"LabelViewQueue": "Peržiūrėti grotuvo eilę",
|
||||||
"LabelVolume": "Garsumas",
|
"LabelVolume": "Garsumas",
|
||||||
"LabelWeekdaysToRun": "Dienos, kuriomis vykdyti",
|
"LabelWeekdaysToRun": "Dienos, kuriomis vykdyti",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Jūsų garso knygos trukmė",
|
"LabelYourAudiobookDuration": "Jūsų garso knygos trukmė",
|
||||||
"LabelYourBookmarks": "Jūsų skirtukai",
|
"LabelYourBookmarks": "Jūsų skirtukai",
|
||||||
"LabelYourPlaylists": "Jūsų grojaraščiai",
|
"LabelYourPlaylists": "Jūsų grojaraščiai",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Verberg",
|
"ButtonHide": "Verberg",
|
||||||
"ButtonHome": "Home",
|
"ButtonHome": "Home",
|
||||||
"ButtonIssues": "Issues",
|
"ButtonIssues": "Issues",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Meest recent",
|
"ButtonLatest": "Meest recent",
|
||||||
"ButtonLibrary": "Bibliotheek",
|
"ButtonLibrary": "Bibliotheek",
|
||||||
"ButtonLogout": "Log uit",
|
"ButtonLogout": "Log uit",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Alle auteurs matchen",
|
"ButtonMatchAllAuthors": "Alle auteurs matchen",
|
||||||
"ButtonMatchBooks": "Alle boeken matchen",
|
"ButtonMatchBooks": "Alle boeken matchen",
|
||||||
"ButtonNevermind": "Laat maar",
|
"ButtonNevermind": "Laat maar",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed openen",
|
"ButtonOpenFeed": "Feed openen",
|
||||||
"ButtonOpenManager": "Manager openen",
|
"ButtonOpenManager": "Manager openen",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Afspelen",
|
"ButtonPlay": "Afspelen",
|
||||||
"ButtonPlaying": "Speelt",
|
"ButtonPlaying": "Speelt",
|
||||||
"ButtonPlaylists": "Afspeellijsten",
|
"ButtonPlaylists": "Afspeellijsten",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Volledige cache legen",
|
"ButtonPurgeAllCache": "Volledige cache legen",
|
||||||
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
|
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
|
||||||
"ButtonPurgeMediaProgress": "Mediavoortgang legen",
|
"ButtonPurgeMediaProgress": "Mediavoortgang legen",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Uit wachtrij verwijderen",
|
"ButtonQueueRemoveItem": "Uit wachtrij verwijderen",
|
||||||
"ButtonQuickMatch": "Snelle match",
|
"ButtonQuickMatch": "Snelle match",
|
||||||
"ButtonRead": "Lees",
|
"ButtonRead": "Lees",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Verwijder",
|
"ButtonRemove": "Verwijder",
|
||||||
"ButtonRemoveAll": "Alles verwijderen",
|
"ButtonRemoveAll": "Alles verwijderen",
|
||||||
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
|
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Maplocatie selecteren",
|
"ButtonSelectFolderPath": "Maplocatie selecteren",
|
||||||
"ButtonSeries": "Series",
|
"ButtonSeries": "Series",
|
||||||
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
|
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Tijden verschuiven",
|
"ButtonShiftTimes": "Tijden verschuiven",
|
||||||
"ButtonShow": "Toon",
|
"ButtonShow": "Toon",
|
||||||
"ButtonStartM4BEncode": "Start M4B-encoding",
|
"ButtonStartM4BEncode": "Start M4B-encoding",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Collectie-objecten",
|
"HeaderCollectionItems": "Collectie-objecten",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
"HeaderCurrentDownloads": "Huidige downloads",
|
"HeaderCurrentDownloads": "Huidige downloads",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download-wachtrij",
|
"HeaderDownloadQueue": "Download-wachtrij",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Details bijwerken",
|
"HeaderUpdateDetails": "Details bijwerken",
|
||||||
"HeaderUpdateLibrary": "Bibliotheek bijwerken",
|
"HeaderUpdateLibrary": "Bibliotheek bijwerken",
|
||||||
"HeaderUsers": "Gebruikers",
|
"HeaderUsers": "Gebruikers",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Je statistieken",
|
"HeaderYourStats": "Je statistieken",
|
||||||
"LabelAbridged": "Verkort",
|
"LabelAbridged": "Verkort",
|
||||||
"LabelAccountType": "Accounttype",
|
"LabelAccountType": "Accounttype",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta-tags",
|
"LabelMetaTags": "Meta-tags",
|
||||||
"LabelMinute": "Minuut",
|
"LabelMinute": "Minuut",
|
||||||
"LabelMissing": "Ontbrekend",
|
"LabelMissing": "Ontbrekend",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Ontbrekende delen",
|
"LabelMissingParts": "Ontbrekende delen",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Meer",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Kan downloaden",
|
"LabelPermissionsDownload": "Kan downloaden",
|
||||||
"LabelPermissionsUpdate": "Kan bijwerken",
|
"LabelPermissionsUpdate": "Kan bijwerken",
|
||||||
"LabelPermissionsUpload": "Kan uploaden",
|
"LabelPermissionsUpload": "Kan uploaden",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Foto pad/URL",
|
"LabelPhotoPathURL": "Foto pad/URL",
|
||||||
"LabelPlaylists": "Afspeellijsten",
|
"LabelPlaylists": "Afspeellijsten",
|
||||||
"LabelPlayMethod": "Afspeelwijze",
|
"LabelPlayMethod": "Afspeelwijze",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast zoekregio",
|
||||||
"LabelPodcastType": "Podcasttype",
|
"LabelPodcastType": "Podcasttype",
|
||||||
"LabelPort": "Poort",
|
"LabelPort": "Poort",
|
||||||
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
|
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Naam serie",
|
"LabelSeriesName": "Naam serie",
|
||||||
"LabelSeriesProgress": "Voortgang serie",
|
"LabelSeriesProgress": "Voortgang serie",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Stel in als primair",
|
"LabelSetEbookAsPrimary": "Stel in als primair",
|
||||||
"LabelSetEbookAsSupplementary": "Stel in als supplementair",
|
"LabelSetEbookAsSupplementary": "Stel in als supplementair",
|
||||||
"LabelSettingsAudiobooksOnly": "Alleen audiobooks",
|
"LabelSettingsAudiobooksOnly": "Alleen audiobooks",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Bekijk afspeelwachtrij",
|
"LabelViewQueue": "Bekijk afspeelwachtrij",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Weekdagen om te draaien",
|
"LabelWeekdaysToRun": "Weekdagen om te draaien",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Je audioboekduur",
|
"LabelYourAudiobookDuration": "Je audioboekduur",
|
||||||
"LabelYourBookmarks": "Je boekwijzers",
|
"LabelYourBookmarks": "Je boekwijzers",
|
||||||
"LabelYourPlaylists": "Je afspeellijsten",
|
"LabelYourPlaylists": "Je afspeellijsten",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Gjøm",
|
"ButtonHide": "Gjøm",
|
||||||
"ButtonHome": "Hjem",
|
"ButtonHome": "Hjem",
|
||||||
"ButtonIssues": "Problemer",
|
"ButtonIssues": "Problemer",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Siste",
|
"ButtonLatest": "Siste",
|
||||||
"ButtonLibrary": "Bibliotek",
|
"ButtonLibrary": "Bibliotek",
|
||||||
"ButtonLogout": "Logg ut",
|
"ButtonLogout": "Logg ut",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Søk opp alle forfattere",
|
"ButtonMatchAllAuthors": "Søk opp alle forfattere",
|
||||||
"ButtonMatchBooks": "Søk opp bøker",
|
"ButtonMatchBooks": "Søk opp bøker",
|
||||||
"ButtonNevermind": "Avbryt",
|
"ButtonNevermind": "Avbryt",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Åpne Feed",
|
"ButtonOpenFeed": "Åpne Feed",
|
||||||
"ButtonOpenManager": "Åpne behandler",
|
"ButtonOpenManager": "Åpne behandler",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Spill av",
|
"ButtonPlay": "Spill av",
|
||||||
"ButtonPlaying": "Spiller av",
|
"ButtonPlaying": "Spiller av",
|
||||||
"ButtonPlaylists": "Spilleliste",
|
"ButtonPlaylists": "Spilleliste",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Tøm alle mellomlager",
|
"ButtonPurgeAllCache": "Tøm alle mellomlager",
|
||||||
"ButtonPurgeItemsCache": "Tøm mellomlager",
|
"ButtonPurgeItemsCache": "Tøm mellomlager",
|
||||||
"ButtonPurgeMediaProgress": "Slett medie fremgang",
|
"ButtonPurgeMediaProgress": "Slett medie fremgang",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Fjern fra kø",
|
"ButtonQueueRemoveItem": "Fjern fra kø",
|
||||||
"ButtonQuickMatch": "Kjapt søk",
|
"ButtonQuickMatch": "Kjapt søk",
|
||||||
"ButtonRead": "Les",
|
"ButtonRead": "Les",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Fjern",
|
"ButtonRemove": "Fjern",
|
||||||
"ButtonRemoveAll": "Fjern alle",
|
"ButtonRemoveAll": "Fjern alle",
|
||||||
"ButtonRemoveAllLibraryItems": "Fjern alle bibliotekobjekter",
|
"ButtonRemoveAllLibraryItems": "Fjern alle bibliotekobjekter",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Velg mappe",
|
"ButtonSelectFolderPath": "Velg mappe",
|
||||||
"ButtonSeries": "Serier",
|
"ButtonSeries": "Serier",
|
||||||
"ButtonSetChaptersFromTracks": "Sett kapittel fra spor",
|
"ButtonSetChaptersFromTracks": "Sett kapittel fra spor",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Forskyv tider",
|
"ButtonShiftTimes": "Forskyv tider",
|
||||||
"ButtonShow": "Vis",
|
"ButtonShow": "Vis",
|
||||||
"ButtonStartM4BEncode": "Start M4B Koding",
|
"ButtonStartM4BEncode": "Start M4B Koding",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Samlingsgjenstander",
|
"HeaderCollectionItems": "Samlingsgjenstander",
|
||||||
"HeaderCover": "Omslag",
|
"HeaderCover": "Omslag",
|
||||||
"HeaderCurrentDownloads": "Aktive nedlastinger",
|
"HeaderCurrentDownloads": "Aktive nedlastinger",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Detaljer",
|
"HeaderDetails": "Detaljer",
|
||||||
"HeaderDownloadQueue": "Last ned kø",
|
"HeaderDownloadQueue": "Last ned kø",
|
||||||
"HeaderEbookFiles": "Ebook filer",
|
"HeaderEbookFiles": "Ebook filer",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Oppdater detaljer",
|
"HeaderUpdateDetails": "Oppdater detaljer",
|
||||||
"HeaderUpdateLibrary": "Oppdater bibliotek",
|
"HeaderUpdateLibrary": "Oppdater bibliotek",
|
||||||
"HeaderUsers": "Brukere",
|
"HeaderUsers": "Brukere",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Din statistikk",
|
"HeaderYourStats": "Din statistikk",
|
||||||
"LabelAbridged": "Forkortet",
|
"LabelAbridged": "Forkortet",
|
||||||
"LabelAccountType": "Kontotype",
|
"LabelAccountType": "Kontotype",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minutt",
|
"LabelMinute": "Minutt",
|
||||||
"LabelMissing": "Mangler",
|
"LabelMissing": "Mangler",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Manglende deler",
|
"LabelMissingParts": "Manglende deler",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Mer",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Kan laste ned",
|
"LabelPermissionsDownload": "Kan laste ned",
|
||||||
"LabelPermissionsUpdate": "Kan oppdatere",
|
"LabelPermissionsUpdate": "Kan oppdatere",
|
||||||
"LabelPermissionsUpload": "Kan laste opp",
|
"LabelPermissionsUpload": "Kan laste opp",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Bilde sti/URL",
|
"LabelPhotoPathURL": "Bilde sti/URL",
|
||||||
"LabelPlaylists": "Spilleliste",
|
"LabelPlaylists": "Spilleliste",
|
||||||
"LabelPlayMethod": "Avspillingsmetode",
|
"LabelPlayMethod": "Avspillingsmetode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcaster",
|
"LabelPodcasts": "Podcaster",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast-søkeområde",
|
||||||
"LabelPodcastType": "Podcast type",
|
"LabelPodcastType": "Podcast type",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefiks som skal ignoreres (skiller ikke mellom store og små bokstaver)",
|
"LabelPrefixesToIgnore": "Prefiks som skal ignoreres (skiller ikke mellom store og små bokstaver)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serier",
|
"LabelSeries": "Serier",
|
||||||
"LabelSeriesName": "Serier Navn",
|
"LabelSeriesName": "Serier Navn",
|
||||||
"LabelSeriesProgress": "Serier fremgang",
|
"LabelSeriesProgress": "Serier fremgang",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Sett som primær",
|
"LabelSetEbookAsPrimary": "Sett som primær",
|
||||||
"LabelSetEbookAsSupplementary": "Sett som supplerende",
|
"LabelSetEbookAsSupplementary": "Sett som supplerende",
|
||||||
"LabelSettingsAudiobooksOnly": "Kun lydbøker",
|
"LabelSettingsAudiobooksOnly": "Kun lydbøker",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Vis spillerkø",
|
"LabelViewQueue": "Vis spillerkø",
|
||||||
"LabelVolume": "Volum",
|
"LabelVolume": "Volum",
|
||||||
"LabelWeekdaysToRun": "Ukedager å kjøre",
|
"LabelWeekdaysToRun": "Ukedager å kjøre",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Din lydbok lengde",
|
"LabelYourAudiobookDuration": "Din lydbok lengde",
|
||||||
"LabelYourBookmarks": "Dine bokmerker",
|
"LabelYourBookmarks": "Dine bokmerker",
|
||||||
"LabelYourPlaylists": "Dine spillelister",
|
"LabelYourPlaylists": "Dine spillelister",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Ukryj",
|
"ButtonHide": "Ukryj",
|
||||||
"ButtonHome": "Strona główna",
|
"ButtonHome": "Strona główna",
|
||||||
"ButtonIssues": "Błędy",
|
"ButtonIssues": "Błędy",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Aktualna wersja:",
|
"ButtonLatest": "Aktualna wersja:",
|
||||||
"ButtonLibrary": "Biblioteka",
|
"ButtonLibrary": "Biblioteka",
|
||||||
"ButtonLogout": "Wyloguj",
|
"ButtonLogout": "Wyloguj",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Dopasuj wszystkich autorów",
|
"ButtonMatchAllAuthors": "Dopasuj wszystkich autorów",
|
||||||
"ButtonMatchBooks": "Dopasuj książki",
|
"ButtonMatchBooks": "Dopasuj książki",
|
||||||
"ButtonNevermind": "Anuluj",
|
"ButtonNevermind": "Anuluj",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Otwórz feed",
|
"ButtonOpenFeed": "Otwórz feed",
|
||||||
"ButtonOpenManager": "Otwórz menadżera",
|
"ButtonOpenManager": "Otwórz menadżera",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Odtwarzaj",
|
"ButtonPlay": "Odtwarzaj",
|
||||||
"ButtonPlaying": "Odtwarzane",
|
"ButtonPlaying": "Odtwarzane",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Playlists",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Wyczyść dane tymczasowe",
|
"ButtonPurgeAllCache": "Wyczyść dane tymczasowe",
|
||||||
"ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji",
|
"ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji",
|
||||||
"ButtonPurgeMediaProgress": "Wyczyść postęp",
|
"ButtonPurgeMediaProgress": "Wyczyść postęp",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Usuń z kolejki",
|
"ButtonQueueRemoveItem": "Usuń z kolejki",
|
||||||
"ButtonQuickMatch": "Szybkie dopasowanie",
|
"ButtonQuickMatch": "Szybkie dopasowanie",
|
||||||
"ButtonRead": "Czytaj",
|
"ButtonRead": "Czytaj",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Usuń",
|
"ButtonRemove": "Usuń",
|
||||||
"ButtonRemoveAll": "Usuń wszystko",
|
"ButtonRemoveAll": "Usuń wszystko",
|
||||||
"ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki",
|
"ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
|
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
|
||||||
"ButtonSeries": "Seria",
|
"ButtonSeries": "Seria",
|
||||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Przesunięcie czasowe",
|
"ButtonShiftTimes": "Przesunięcie czasowe",
|
||||||
"ButtonShow": "Pokaż",
|
"ButtonShow": "Pokaż",
|
||||||
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Elementy kolekcji",
|
"HeaderCollectionItems": "Elementy kolekcji",
|
||||||
"HeaderCover": "Okładka",
|
"HeaderCover": "Okładka",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Szczegóły",
|
"HeaderDetails": "Szczegóły",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Zaktualizuj szczegóły",
|
"HeaderUpdateDetails": "Zaktualizuj szczegóły",
|
||||||
"HeaderUpdateLibrary": "Zaktualizuj bibliotekę",
|
"HeaderUpdateLibrary": "Zaktualizuj bibliotekę",
|
||||||
"HeaderUsers": "Użytkownicy",
|
"HeaderUsers": "Użytkownicy",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Twoje statystyki",
|
"HeaderYourStats": "Twoje statystyki",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Typ konta",
|
"LabelAccountType": "Typ konta",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
"LabelMinute": "Minuta",
|
"LabelMinute": "Minuta",
|
||||||
"LabelMissing": "Brakujący",
|
"LabelMissing": "Brakujący",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Brakujące cześci",
|
"LabelMissingParts": "Brakujące cześci",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Więcej",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Ma możliwość pobierania",
|
"LabelPermissionsDownload": "Ma możliwość pobierania",
|
||||||
"LabelPermissionsUpdate": "Ma możliwość aktualizowania",
|
"LabelPermissionsUpdate": "Ma możliwość aktualizowania",
|
||||||
"LabelPermissionsUpload": "Ma możliwość dodawania",
|
"LabelPermissionsUpload": "Ma możliwość dodawania",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Scieżka/URL do zdjęcia",
|
"LabelPhotoPathURL": "Scieżka/URL do zdjęcia",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Metoda odtwarzania",
|
"LabelPlayMethod": "Metoda odtwarzania",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasty",
|
"LabelPodcasts": "Podcasty",
|
||||||
|
"LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
|
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Nazwy serii",
|
"LabelSeriesName": "Nazwy serii",
|
||||||
"LabelSeriesProgress": "Postęp w serii",
|
"LabelSeriesProgress": "Postęp w serii",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Set as primary",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
|
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
|
||||||
"LabelVolume": "Głośność",
|
"LabelVolume": "Głośność",
|
||||||
"LabelWeekdaysToRun": "Dni tygodnia",
|
"LabelWeekdaysToRun": "Dni tygodnia",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Czas trwania audiobooka",
|
"LabelYourAudiobookDuration": "Czas trwania audiobooka",
|
||||||
"LabelYourBookmarks": "Twoje zakładki",
|
"LabelYourBookmarks": "Twoje zakładki",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Your Playlists",
|
||||||
|
779
client/strings/pt-br.json
Normal file
779
client/strings/pt-br.json
Normal file
@ -0,0 +1,779 @@
|
|||||||
|
{
|
||||||
|
"ButtonAdd": "Adicionar",
|
||||||
|
"ButtonAddChapters": "Adicionar Capítulos",
|
||||||
|
"ButtonAddDevice": "Adicionar Dispositivo",
|
||||||
|
"ButtonAddLibrary": "Adicionar Biblioteca",
|
||||||
|
"ButtonAddPodcasts": "Adicionar Podcasts",
|
||||||
|
"ButtonAddUser": "Adicionar Usuário",
|
||||||
|
"ButtonAddYourFirstLibrary": "Adicionar sua primeira biblioteca",
|
||||||
|
"ButtonApply": "Aplicar",
|
||||||
|
"ButtonApplyChapters": "Aplicar Capítulos",
|
||||||
|
"ButtonAuthors": "Autores",
|
||||||
|
"ButtonBrowseForFolder": "Procurar por Pasta",
|
||||||
|
"ButtonCancel": "Cancelar",
|
||||||
|
"ButtonCancelEncode": "Cancelar Codificação",
|
||||||
|
"ButtonChangeRootPassword": "Alterar senha do administrador",
|
||||||
|
"ButtonCheckAndDownloadNewEpisodes": "Verificar & Baixar Novos Episódios",
|
||||||
|
"ButtonChooseAFolder": "Escolha uma pasta",
|
||||||
|
"ButtonChooseFiles": "Escolha arquivos",
|
||||||
|
"ButtonClearFilter": "Limpar Filtro",
|
||||||
|
"ButtonCloseFeed": "Fechar Feed",
|
||||||
|
"ButtonCollections": "Coleções",
|
||||||
|
"ButtonConfigureScanner": "Configurar Verificador",
|
||||||
|
"ButtonCreate": "Criar",
|
||||||
|
"ButtonCreateBackup": "Criar Backup",
|
||||||
|
"ButtonDelete": "Apagar",
|
||||||
|
"ButtonDownloadQueue": "Fila de download",
|
||||||
|
"ButtonEdit": "Editar",
|
||||||
|
"ButtonEditChapters": "Editar Capítulos",
|
||||||
|
"ButtonEditPodcast": "Editar Podcast",
|
||||||
|
"ButtonForceReScan": "Forcar Nova Verificação",
|
||||||
|
"ButtonFullPath": "Caminho Completo",
|
||||||
|
"ButtonHide": "Ocultar",
|
||||||
|
"ButtonHome": "Principal",
|
||||||
|
"ButtonIssues": "Problemas",
|
||||||
|
"ButtonJumpBackward": "Retroceder",
|
||||||
|
"ButtonJumpForward": "Adiantar",
|
||||||
|
"ButtonLatest": "Mais Recentes",
|
||||||
|
"ButtonLibrary": "Biblioteca",
|
||||||
|
"ButtonLogout": "Logout",
|
||||||
|
"ButtonLookup": "Procurar",
|
||||||
|
"ButtonManageTracks": "Gerenciar Faixas",
|
||||||
|
"ButtonMapChapterTitles": "Designar Títulos de Capítulos",
|
||||||
|
"ButtonMatchAllAuthors": "Consultar Todos os Autores",
|
||||||
|
"ButtonMatchBooks": "Consultar Livros",
|
||||||
|
"ButtonNevermind": "Cancelar",
|
||||||
|
"ButtonNext": "Próximo",
|
||||||
|
"ButtonNextChapter": "Próximo Capítulo",
|
||||||
|
"ButtonOk": "Ok",
|
||||||
|
"ButtonOpenFeed": "Abrir Feed",
|
||||||
|
"ButtonOpenManager": "Abrir Gerenciador",
|
||||||
|
"ButtonPause": "Pausar",
|
||||||
|
"ButtonPlay": "Reproduzir",
|
||||||
|
"ButtonPlaying": "Reproduzindo",
|
||||||
|
"ButtonPlaylists": "Lista de Reprodução",
|
||||||
|
"ButtonPrevious": "Anterior",
|
||||||
|
"ButtonPreviousChapter": "Capítulo Anterior",
|
||||||
|
"ButtonPurgeAllCache": "Apagar Todo o Cache",
|
||||||
|
"ButtonPurgeItemsCache": "Apagar o Cache de Itens",
|
||||||
|
"ButtonPurgeMediaProgress": "Apagar o Progresso nas Mídias",
|
||||||
|
"ButtonQueueAddItem": "Adicionar à Lista",
|
||||||
|
"ButtonQueueRemoveItem": "Remover da Lista",
|
||||||
|
"ButtonQuickMatch": "Consulta rápida",
|
||||||
|
"ButtonRead": "Ler",
|
||||||
|
"ButtonRefresh": "Atualizar",
|
||||||
|
"ButtonRemove": "Remover",
|
||||||
|
"ButtonRemoveAll": "Remover Todos",
|
||||||
|
"ButtonRemoveAllLibraryItems": "Remover Todos os Itens da Biblioteca",
|
||||||
|
"ButtonRemoveFromContinueListening": "Remover de Continuar Escutando",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remover de Continuar Lendo",
|
||||||
|
"ButtonRemoveSeriesFromContinueSeries": "Remover Série de Continuar Série",
|
||||||
|
"ButtonReScan": "Nova Verificação",
|
||||||
|
"ButtonReset": "Resetar",
|
||||||
|
"ButtonResetToDefault": "Resetar para valores padrão",
|
||||||
|
"ButtonRestore": "Restaurar",
|
||||||
|
"ButtonSave": "Salvar",
|
||||||
|
"ButtonSaveAndClose": "Salvar & Fechar",
|
||||||
|
"ButtonSaveTracklist": "Salvar Lista de Faixas",
|
||||||
|
"ButtonScan": "Verificar",
|
||||||
|
"ButtonScanLibrary": "Verificar Biblioteca",
|
||||||
|
"ButtonSearch": "Pesquisar",
|
||||||
|
"ButtonSelectFolderPath": "Selecionar Caminho da Pasta",
|
||||||
|
"ButtonSeries": "Séries",
|
||||||
|
"ButtonSetChaptersFromTracks": "Definir Capítulos Segundo Faixas",
|
||||||
|
"ButtonShare": "Compartilhar",
|
||||||
|
"ButtonShiftTimes": "Deslocar tempos",
|
||||||
|
"ButtonShow": "Exibir",
|
||||||
|
"ButtonStartM4BEncode": "Iniciar Codificação M4B",
|
||||||
|
"ButtonStartMetadataEmbed": "Iniciar Inclusão de Metadados",
|
||||||
|
"ButtonSubmit": "Enviar",
|
||||||
|
"ButtonTest": "Testar",
|
||||||
|
"ButtonUpload": "Upload",
|
||||||
|
"ButtonUploadBackup": "Upload de Backup",
|
||||||
|
"ButtonUploadCover": "Upload de Capa",
|
||||||
|
"ButtonUploadOPMLFile": "Upload Arquivo OPML",
|
||||||
|
"ButtonUserDelete": "Apagar usuário {0}",
|
||||||
|
"ButtonUserEdit": "Editar usuário {0}",
|
||||||
|
"ButtonViewAll": "Ver tudo",
|
||||||
|
"ButtonYes": "Sim",
|
||||||
|
"ErrorUploadFetchMetadataAPI": "Erro buscando metadados",
|
||||||
|
"ErrorUploadFetchMetadataNoResults": "Não foi possível buscar metadados - tente atualizar o título e/ou autor",
|
||||||
|
"ErrorUploadLacksTitle": "É preciso ter um título",
|
||||||
|
"HeaderAccount": "Conta",
|
||||||
|
"HeaderAdvanced": "Avançado",
|
||||||
|
"HeaderAppriseNotificationSettings": "Configuração de notificações Apprise",
|
||||||
|
"HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks",
|
||||||
|
"HeaderAudioTracks": "Trilhas de áudio",
|
||||||
|
"HeaderAuthentication": "Autenticação",
|
||||||
|
"HeaderBackups": "Backups",
|
||||||
|
"HeaderChangePassword": "Trocar Senha",
|
||||||
|
"HeaderChapters": "Capítulos",
|
||||||
|
"HeaderChooseAFolder": "Escolha uma Pasta",
|
||||||
|
"HeaderCollection": "Coleção",
|
||||||
|
"HeaderCollectionItems": "Itens da Coleção",
|
||||||
|
"HeaderCover": "Capas",
|
||||||
|
"HeaderCurrentDownloads": "Downloads em andamento",
|
||||||
|
"HeaderCustomMetadataProviders": "Fontes de Metadados Customizados",
|
||||||
|
"HeaderDetails": "Detalhes",
|
||||||
|
"HeaderDownloadQueue": "Fila de Download",
|
||||||
|
"HeaderEbookFiles": "Arquivos Ebook",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Configurações de Email",
|
||||||
|
"HeaderEpisodes": "Episódios",
|
||||||
|
"HeaderEreaderDevices": "Dispositivos Ereader",
|
||||||
|
"HeaderEreaderSettings": "Configurações Ereader",
|
||||||
|
"HeaderFiles": "Arquivos",
|
||||||
|
"HeaderFindChapters": "Localizar Capítulos",
|
||||||
|
"HeaderIgnoredFiles": "Arquivos Ignorados",
|
||||||
|
"HeaderItemFiles": "Arquivos de Itens",
|
||||||
|
"HeaderItemMetadataUtils": "Utilidades para Metadados dos Itens",
|
||||||
|
"HeaderLastListeningSession": "Última sessão",
|
||||||
|
"HeaderLatestEpisodes": "Últimos episódios",
|
||||||
|
"HeaderLibraries": "Bibliotecas",
|
||||||
|
"HeaderLibraryFiles": "Arquivos da Biblioteca",
|
||||||
|
"HeaderLibraryStats": "Estatísticas da Biblioteca",
|
||||||
|
"HeaderListeningSessions": "Sessões",
|
||||||
|
"HeaderListeningStats": "Estatísticas",
|
||||||
|
"HeaderLogin": "Login",
|
||||||
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Gerenciar Gêneros",
|
||||||
|
"HeaderManageTags": "Gerenciar Etiquetas",
|
||||||
|
"HeaderMapDetails": "Designar Detalhes",
|
||||||
|
"HeaderMatch": "Consultar",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados",
|
||||||
|
"HeaderMetadataToEmbed": "Metadados a Serem Incluídos",
|
||||||
|
"HeaderNewAccount": "Nova Conta",
|
||||||
|
"HeaderNewLibrary": "Nova Biblioteca",
|
||||||
|
"HeaderNotifications": "Notificações",
|
||||||
|
"HeaderOpenIDConnectAuthentication": "Autenticação via OpenID Connect",
|
||||||
|
"HeaderOpenRSSFeed": "Abrir Feed RSS",
|
||||||
|
"HeaderOtherFiles": "Outros Arquivos",
|
||||||
|
"HeaderPasswordAuthentication": "Autenticação por Senha",
|
||||||
|
"HeaderPermissions": "Permissões",
|
||||||
|
"HeaderPlayerQueue": "Fila do reprodutor",
|
||||||
|
"HeaderPlaylist": "Lista de Reprodução",
|
||||||
|
"HeaderPlaylistItems": "Itens da lista de reprodução",
|
||||||
|
"HeaderPodcastsToAdd": "Podcasts para Adicionar",
|
||||||
|
"HeaderPreviewCover": "Visualização da Capa",
|
||||||
|
"HeaderRemoveEpisode": "Remover Episódio",
|
||||||
|
"HeaderRemoveEpisodes": "Remover {0} Episódios",
|
||||||
|
"HeaderRSSFeedGeneral": "Detalhes RSS",
|
||||||
|
"HeaderRSSFeedIsOpen": "Feed RSS está aberto",
|
||||||
|
"HeaderRSSFeeds": "Feeds RSS",
|
||||||
|
"HeaderSavedMediaProgress": "Progresso da gravação das mídias",
|
||||||
|
"HeaderSchedule": "Programação",
|
||||||
|
"HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca",
|
||||||
|
"HeaderSession": "Sessão",
|
||||||
|
"HeaderSetBackupSchedule": "Definir Programação de Backup",
|
||||||
|
"HeaderSettings": "Configurações",
|
||||||
|
"HeaderSettingsDisplay": "Exibição",
|
||||||
|
"HeaderSettingsExperimental": "Funcionalidades experimentais",
|
||||||
|
"HeaderSettingsGeneral": "Geral",
|
||||||
|
"HeaderSettingsScanner": "Verificador",
|
||||||
|
"HeaderSleepTimer": "Timer",
|
||||||
|
"HeaderStatsLargestItems": "Maiores Itens",
|
||||||
|
"HeaderStatsLongestItems": "Itens mais longos (hrs)",
|
||||||
|
"HeaderStatsMinutesListeningChart": "Minutos Escutados (últimos 7 dias)",
|
||||||
|
"HeaderStatsRecentSessions": "Sessões Recentes",
|
||||||
|
"HeaderStatsTop10Authors": "Top 10 Autores",
|
||||||
|
"HeaderStatsTop5Genres": "Top 5 Gêneros",
|
||||||
|
"HeaderTableOfContents": "Sumário",
|
||||||
|
"HeaderTools": "Ferramentas",
|
||||||
|
"HeaderUpdateAccount": "Atualizar Conta",
|
||||||
|
"HeaderUpdateAuthor": "Atualizar Autor",
|
||||||
|
"HeaderUpdateDetails": "Atualizar Detalhes",
|
||||||
|
"HeaderUpdateLibrary": "Atualizar Biblioteca",
|
||||||
|
"HeaderUsers": "Usuários",
|
||||||
|
"HeaderYearReview": "Retrospectiva de {0} ",
|
||||||
|
"HeaderYourStats": "Suas Estatísticas",
|
||||||
|
"LabelAbridged": "Versão Abreviada",
|
||||||
|
"LabelAccountType": "Tipo de Conta",
|
||||||
|
"LabelAccountTypeAdmin": "Administrador",
|
||||||
|
"LabelAccountTypeGuest": "Convidado",
|
||||||
|
"LabelAccountTypeUser": "Usuário",
|
||||||
|
"LabelActivity": "Atividade",
|
||||||
|
"LabelAdded": "Acrescentado",
|
||||||
|
"LabelAddedAt": "Acrescentado em",
|
||||||
|
"LabelAddToCollection": "Adicionar à Coleção",
|
||||||
|
"LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção",
|
||||||
|
"LabelAddToPlaylist": "Adicionar à Lista de Reprodução",
|
||||||
|
"LabelAddToPlaylistBatch": "Adicionar {0} itens à Lista de Reprodução",
|
||||||
|
"LabelAdminUsersOnly": "Apenas usuários administradores",
|
||||||
|
"LabelAll": "Todos",
|
||||||
|
"LabelAllUsers": "Todos Usuários",
|
||||||
|
"LabelAllUsersExcludingGuests": "Todos usuários exceto convidados",
|
||||||
|
"LabelAllUsersIncludingGuests": "Todos usuários incluindo convidados",
|
||||||
|
"LabelAlreadyInYourLibrary": "Já na sua biblioteca",
|
||||||
|
"LabelAppend": "Acrescentar",
|
||||||
|
"LabelAuthor": "Autor",
|
||||||
|
"LabelAuthorFirstLast": "Autor (Nome Sobrenome)",
|
||||||
|
"LabelAuthorLastFirst": "Autor (Sobrenome, Nome)",
|
||||||
|
"LabelAuthors": "Autores",
|
||||||
|
"LabelAutoDownloadEpisodes": "Download Automático de Episódios",
|
||||||
|
"LabelAutoFetchMetadata": "Buscar Metadados Automaticamente",
|
||||||
|
"LabelAutoFetchMetadataHelp": "Busca metadados de título, autor e série para otimizar o upload. Pode ser necessário buscas metadados adicionais após o upload.",
|
||||||
|
"LabelAutoLaunch": "Iniciar Automaticamente",
|
||||||
|
"LabelAutoLaunchDescription": "Redireciona para o fornecedor de autenticação automaticamente ao navegar para a tela de login (caminho para substituição manual <code>/login?autoLaunch=0</code>)",
|
||||||
|
"LabelAutoRegister": "Registrar Automaticamente",
|
||||||
|
"LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login",
|
||||||
|
"LabelBackToUser": "Voltar para Usuário",
|
||||||
|
"LabelBackupLocation": "Localização do Backup",
|
||||||
|
"LabelBackupsEnableAutomaticBackups": "Ativar backups automáticos",
|
||||||
|
"LabelBackupsEnableAutomaticBackupsHelp": "Backups salvos em /metadata/backups",
|
||||||
|
"LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB)",
|
||||||
|
"LabelBackupsMaxBackupSizeHelp": "Como proteção contra uma configuração incorreta, backups darão erro se excederem o tamanho configurado.",
|
||||||
|
"LabelBackupsNumberToKeep": "Número de backups para guardar",
|
||||||
|
"LabelBackupsNumberToKeepHelp": "Apenas 1 backup será removido por vez, então, se já existem mais backups, você deve apagá-los manualmente.",
|
||||||
|
"LabelBitrate": "Bitrate",
|
||||||
|
"LabelBooks": "Livros",
|
||||||
|
"LabelButtonText": "Texto do botão",
|
||||||
|
"LabelChangePassword": "Trocar Senha",
|
||||||
|
"LabelChannels": "Canais",
|
||||||
|
"LabelChapters": "Capítulos",
|
||||||
|
"LabelChaptersFound": "capítulos encontrados",
|
||||||
|
"LabelChapterTitle": "Título do Capítulo",
|
||||||
|
"LabelClickForMoreInfo": "Clique para mais informações",
|
||||||
|
"LabelClosePlayer": "Fechar Reprodutor",
|
||||||
|
"LabelCodec": "Codec",
|
||||||
|
"LabelCollapseSeries": "Fechar Série",
|
||||||
|
"LabelCollection": "Coleção",
|
||||||
|
"LabelCollections": "Coleções",
|
||||||
|
"LabelComplete": "Concluído",
|
||||||
|
"LabelConfirmPassword": "Confirmar Senha",
|
||||||
|
"LabelContinueListening": "Continuar Escutando",
|
||||||
|
"LabelContinueReading": "Continuar Lendo",
|
||||||
|
"LabelContinueSeries": "Continuar Série",
|
||||||
|
"LabelCover": "Capa",
|
||||||
|
"LabelCoverImageURL": "URL da Imagem da Capa",
|
||||||
|
"LabelCreatedAt": "Criado em",
|
||||||
|
"LabelCronExpression": "Expressão para o Cron",
|
||||||
|
"LabelCurrent": "Atual",
|
||||||
|
"LabelCurrently": "Atualmente:",
|
||||||
|
"LabelCustomCronExpression": "Expressão personalizada para o Cron:",
|
||||||
|
"LabelDatetime": "Data e Hora",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Apagar do sistema de arquivos (desmarcar para remover apenas da base de dados)",
|
||||||
|
"LabelDescription": "Descrição",
|
||||||
|
"LabelDeselectAll": "Desmarcar tudo",
|
||||||
|
"LabelDevice": "Dispositivo",
|
||||||
|
"LabelDeviceInfo": "Informação do Dispositivo",
|
||||||
|
"LabelDeviceIsAvailableTo": "Dispositivo está disponível para...",
|
||||||
|
"LabelDirectory": "Diretório",
|
||||||
|
"LabelDiscFromFilename": "Disco a partir do nome do arquivo",
|
||||||
|
"LabelDiscFromMetadata": "Disco a partir dos metadados",
|
||||||
|
"LabelDiscover": "Descobrir",
|
||||||
|
"LabelDownload": "Download",
|
||||||
|
"LabelDownloadNEpisodes": "Download de {0} Episódios",
|
||||||
|
"LabelDuration": "Duração",
|
||||||
|
"LabelDurationFound": "Duração comprovada:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
|
"LabelEbooks": "Ebooks",
|
||||||
|
"LabelEdit": "Editar",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "Remetente",
|
||||||
|
"LabelEmailSettingsSecure": "Seguro",
|
||||||
|
"LabelEmailSettingsSecureHelp": "Se ativado, a conexão utilizará TLS para a conexão ao servidor. Se desativado TLS será usado se o servidor suportar a extensão STARTTLS. Na maioria dos casos ative esse valor se estiver conectando pela porta 465. Para portas 587 ou 25, mantenha inativo. (de nodemailer.com/smtp/#authentication)",
|
||||||
|
"LabelEmailSettingsTestAddress": "Endereço de teste",
|
||||||
|
"LabelEmbeddedCover": "Capa Integrada",
|
||||||
|
"LabelEnable": "Habilitar",
|
||||||
|
"LabelEnd": "Fim",
|
||||||
|
"LabelEpisode": "Episódio",
|
||||||
|
"LabelEpisodeTitle": "Título do Episódio",
|
||||||
|
"LabelEpisodeType": "Tipo do Episódio",
|
||||||
|
"LabelExample": "Exemplo",
|
||||||
|
"LabelExplicit": "Explícito",
|
||||||
|
"LabelFeedURL": "URL do Feed",
|
||||||
|
"LabelFetchingMetadata": "Buscando Metadados",
|
||||||
|
"LabelFile": "Arquivo",
|
||||||
|
"LabelFileBirthtime": "Criação do Arquivo",
|
||||||
|
"LabelFileModified": "Modificação do Arquivo",
|
||||||
|
"LabelFilename": "Nome do Arquivo",
|
||||||
|
"LabelFilterByUser": "Filtrar por Usuário",
|
||||||
|
"LabelFindEpisodes": "Localizar Episódios",
|
||||||
|
"LabelFinished": "Concluído",
|
||||||
|
"LabelFolder": "Pasta",
|
||||||
|
"LabelFolders": "Pastas",
|
||||||
|
"LabelFontBold": "Negrito",
|
||||||
|
"LabelFontFamily": "Família de fonte",
|
||||||
|
"LabelFontItalic": "Itálico",
|
||||||
|
"LabelFontScale": "Escala de fonte",
|
||||||
|
"LabelFontStrikethrough": "Tachado",
|
||||||
|
"LabelFormat": "Formato",
|
||||||
|
"LabelGenre": "Gênero",
|
||||||
|
"LabelGenres": "Gêneros",
|
||||||
|
"LabelHardDeleteFile": "Apagar definitivamente",
|
||||||
|
"LabelHasEbook": "Tem ebook",
|
||||||
|
"LabelHasSupplementaryEbook": "Tem ebook complementar",
|
||||||
|
"LabelHighestPriority": "Prioridade mais alta",
|
||||||
|
"LabelHost": "Host",
|
||||||
|
"LabelHour": "Hora",
|
||||||
|
"LabelIcon": "Ícone",
|
||||||
|
"LabelImageURLFromTheWeb": "URL da imagem na internet",
|
||||||
|
"LabelIncludeInTracklist": "Incluir na Lista de Faixas",
|
||||||
|
"LabelIncomplete": "Incompleto",
|
||||||
|
"LabelInProgress": "Em Andamento",
|
||||||
|
"LabelInterval": "Intervalo",
|
||||||
|
"LabelIntervalCustomDailyWeekly": "Personalizar diário/semanal",
|
||||||
|
"LabelIntervalEvery12Hours": "A cada 12 horas",
|
||||||
|
"LabelIntervalEvery15Minutes": "A cada 15 minutos",
|
||||||
|
"LabelIntervalEvery2Hours": "A cada 2 horas",
|
||||||
|
"LabelIntervalEvery30Minutes": "A cada 30 minutos",
|
||||||
|
"LabelIntervalEvery6Hours": "A cada 6 horas",
|
||||||
|
"LabelIntervalEveryDay": "Todo dia",
|
||||||
|
"LabelIntervalEveryHour": "Toda hora",
|
||||||
|
"LabelInvalidParts": "Partes Inválidas",
|
||||||
|
"LabelInvert": "Inverter",
|
||||||
|
"LabelItem": "Item",
|
||||||
|
"LabelLanguage": "Idioma",
|
||||||
|
"LabelLanguageDefaultServer": "Idioma Padrão do Servidor",
|
||||||
|
"LabelLastBookAdded": "Último Livro Acrescentado",
|
||||||
|
"LabelLastBookUpdated": "Último Livro Atualizado",
|
||||||
|
"LabelLastSeen": "Visto pela Última Vez",
|
||||||
|
"LabelLastTime": "Progresso",
|
||||||
|
"LabelLastUpdate": "Última Atualização",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutSinglePage": "Uma página",
|
||||||
|
"LabelLayoutSplitPage": "Página dividida",
|
||||||
|
"LabelLess": "Menos",
|
||||||
|
"LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário",
|
||||||
|
"LabelLibrary": "Biblioteca",
|
||||||
|
"LabelLibraryItem": "Item da Biblioteca",
|
||||||
|
"LabelLibraryName": "Nome da Biblioteca",
|
||||||
|
"LabelLimit": "Limite",
|
||||||
|
"LabelLineSpacing": "Espaçamento entre linhas",
|
||||||
|
"LabelListenAgain": "Escutar novamente",
|
||||||
|
"LabelLogLevelDebug": "Debug",
|
||||||
|
"LabelLogLevelInfo": "Info",
|
||||||
|
"LabelLogLevelWarn": "Atenção",
|
||||||
|
"LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data",
|
||||||
|
"LabelLowestPriority": "Prioridade mais baixa",
|
||||||
|
"LabelMatchExistingUsersBy": "Consultar usuários existentes usando",
|
||||||
|
"LabelMatchExistingUsersByDescription": "Utilizado para conectar usuários já existentes. Uma vez conectados, usuários serão consultados utilizando uma identificação única do seu provedor de SSO",
|
||||||
|
"LabelMediaPlayer": "Reprodutor de mídia",
|
||||||
|
"LabelMediaType": "Tipo de Mídia",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa",
|
||||||
|
"LabelMetadataProvider": "Fonte de Metadados",
|
||||||
|
"LabelMetaTag": "Etiqueta Meta",
|
||||||
|
"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",
|
||||||
|
"LabelMoreInfo": "Mais Informações",
|
||||||
|
"LabelName": "Nome",
|
||||||
|
"LabelNarrator": "Narrador",
|
||||||
|
"LabelNarrators": "Narradores",
|
||||||
|
"LabelNew": "Novo",
|
||||||
|
"LabelNewestAuthors": "Novos Autores",
|
||||||
|
"LabelNewestEpisodes": "Episódios mais recentes",
|
||||||
|
"LabelNewPassword": "Nova Senha",
|
||||||
|
"LabelNextBackupDate": "Data do próximo backup",
|
||||||
|
"LabelNextScheduledRun": "Próxima execução programada",
|
||||||
|
"LabelNoEpisodesSelected": "Nenhum episódio selecionado",
|
||||||
|
"LabelNotes": "Notas",
|
||||||
|
"LabelNotFinished": "Não concluído",
|
||||||
|
"LabelNotificationAppriseURL": "URL(s) Apprise",
|
||||||
|
"LabelNotificationAvailableVariables": "Variáveis disponíveis",
|
||||||
|
"LabelNotificationBodyTemplate": "Modelo de Corpo",
|
||||||
|
"LabelNotificationEvent": "Evento de Notificação",
|
||||||
|
"LabelNotificationsMaxFailedAttempts": "Máximo de tentativas com falhas",
|
||||||
|
"LabelNotificationsMaxFailedAttemptsHelp": "Notificações serão desabilitadas após falharem este número de vezes",
|
||||||
|
"LabelNotificationsMaxQueueSize": "Tamanho máximo da fila de eventos de notificação",
|
||||||
|
"LabelNotificationsMaxQueueSizeHelp": "Eventos estão limitados a um disparo por segundo. Eventos serão ignorados se a fila estiver no tamanho máximo. Isso evita o excesso de notificações.",
|
||||||
|
"LabelNotificationTitleTemplate": "Modelo de Título",
|
||||||
|
"LabelNotStarted": "Não iniciado",
|
||||||
|
"LabelNumberOfBooks": "Número de Livros",
|
||||||
|
"LabelNumberOfEpisodes": "# de Episódios",
|
||||||
|
"LabelOpenRSSFeed": "Abrir Feed RSS",
|
||||||
|
"LabelOverwrite": "Sobrescrever",
|
||||||
|
"LabelPassword": "Senha",
|
||||||
|
"LabelPath": "Caminho",
|
||||||
|
"LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas",
|
||||||
|
"LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas",
|
||||||
|
"LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos",
|
||||||
|
"LabelPermissionsDelete": "Pode Apagar",
|
||||||
|
"LabelPermissionsDownload": "Pode Fazer Download",
|
||||||
|
"LabelPermissionsUpdate": "Pode Atualizar",
|
||||||
|
"LabelPermissionsUpload": "Pode Fazer Upload",
|
||||||
|
"LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})",
|
||||||
|
"LabelPhotoPathURL": "Caminho/URL para Foto",
|
||||||
|
"LabelPlaylists": "Listas de Reprodução",
|
||||||
|
"LabelPlayMethod": "Método de Reprodução",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Região de busca do podcast",
|
||||||
|
"LabelPodcastType": "Tipo de Podcast",
|
||||||
|
"LabelPort": "Porta",
|
||||||
|
"LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)",
|
||||||
|
"LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes e Google",
|
||||||
|
"LabelPrimaryEbook": "Ebook principal",
|
||||||
|
"LabelProgress": "Progresso",
|
||||||
|
"LabelProvider": "Fonte",
|
||||||
|
"LabelPubDate": "Data de Publicação",
|
||||||
|
"LabelPublisher": "Editora",
|
||||||
|
"LabelPublishYear": "Ano de Publicação",
|
||||||
|
"LabelRead": "Lido",
|
||||||
|
"LabelReadAgain": "Ler novamente",
|
||||||
|
"LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso",
|
||||||
|
"LabelRecentlyAdded": "Novidades",
|
||||||
|
"LabelRecentSeries": "Séries Recentes",
|
||||||
|
"LabelRecommended": "Recomendado",
|
||||||
|
"LabelRedo": "Refazer",
|
||||||
|
"LabelRegion": "Região",
|
||||||
|
"LabelReleaseDate": "Data de Lançamento",
|
||||||
|
"LabelRemoveCover": "Remover capa",
|
||||||
|
"LabelRowsPerPage": "Linhas por Página",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Email do dono personalizado",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Nome do dono personalizado",
|
||||||
|
"LabelRSSFeedOpen": "Feed RSS Aberto",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Impedir Indexação",
|
||||||
|
"LabelRSSFeedSlug": "Slug do Feed RSS",
|
||||||
|
"LabelRSSFeedURL": "URL do Feed RSS",
|
||||||
|
"LabelSearchTerm": "Busca por Termo",
|
||||||
|
"LabelSearchTitle": "Busca por Título",
|
||||||
|
"LabelSearchTitleOrASIN": "Busca por Título ou ASIN",
|
||||||
|
"LabelSeason": "Temporada",
|
||||||
|
"LabelSelectAllEpisodes": "Selecionar todos os Episódios",
|
||||||
|
"LabelSelectEpisodesShowing": "Selecionar os {0} Episódios Visíveis",
|
||||||
|
"LabelSelectUsers": "Selecionar usuários",
|
||||||
|
"LabelSendEbookToDevice": "Enviar Ebook para...",
|
||||||
|
"LabelSequence": "Sequência",
|
||||||
|
"LabelSeries": "Série",
|
||||||
|
"LabelSeriesName": "Nome da Série",
|
||||||
|
"LabelSeriesProgress": "Progresso da Série",
|
||||||
|
"LabelServerYearReview": "Retrospectiva Anual do Servidor ({0})",
|
||||||
|
"LabelSetEbookAsPrimary": "Definir como principal",
|
||||||
|
"LabelSetEbookAsSupplementary": "Definir como complementar",
|
||||||
|
"LabelSettingsAudiobooksOnly": "Apenas Audiobooks",
|
||||||
|
"LabelSettingsAudiobooksOnlyHelp": "Ao ativar essa configuração os arquivos de ebooks serão ignorados a não ser que estejam dentro de uma pasta com um audiobook. Nesse caso eles serão definidos como ebooks complementares",
|
||||||
|
"LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira",
|
||||||
|
"LabelSettingsChromecastSupport": "Suporte ao Chromecast",
|
||||||
|
"LabelSettingsDateFormat": "Formato de data",
|
||||||
|
"LabelSettingsDisableWatcher": "Desativar Monitoramento",
|
||||||
|
"LabelSettingsDisableWatcherForLibrary": "Desativa o monitoramento de pastas para a biblioteca",
|
||||||
|
"LabelSettingsDisableWatcherHelp": "Desativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
|
||||||
|
"LabelSettingsEnableWatcher": "Ativar Monitoramento",
|
||||||
|
"LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a biblioteca",
|
||||||
|
"LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
|
||||||
|
"LabelSettingsExperimentalFeatures": "Funcionalidade experimentais",
|
||||||
|
"LabelSettingsExperimentalFeaturesHelp": "Funcionalidade em desenvolvimento que se beneficiairam dos seus comentários e da sua ajuda para testar. Clique para abrir a discussão no github.",
|
||||||
|
"LabelSettingsFindCovers": "Localizar capas",
|
||||||
|
"LabelSettingsFindCoversHelp": "Se o seu audiobook não tiver uma capa incluída ou uma imagem de capa na pasta, o verificador tentará localizar uma capa.<br>Atenção: Isso irá estender o tempo de análise",
|
||||||
|
"LabelSettingsHideSingleBookSeries": "Ocultar séries com um só livro",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"LabelSettingsPreferMatchedMetadataHelp": "Dados consultados serão priorizados sobre os detalhes do item quando usada a Consulta Rápida. Por padrão, Consulta Rápida só preencherá os detalhes ausentes.",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithASIN": "Pular consulta de livros que já têm um ASIN",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithISBN": "Pular consulta de livros que já têm um ISBN",
|
||||||
|
"LabelSettingsSortingIgnorePrefixes": "Ignorar prefixos ao ordenar",
|
||||||
|
"LabelSettingsSortingIgnorePrefixesHelp": "ex: o prefixo \"o\" do título \"O Título do Livro\" seria ordenado como \"Título do Livro, O\"",
|
||||||
|
"LabelSettingsSquareBookCovers": "Usar capas de livro quadradas",
|
||||||
|
"LabelSettingsSquareBookCoversHelp": "Preferir capas quadradas ao invés das capas 1.6:1 padrão",
|
||||||
|
"LabelSettingsStoreCoversWithItem": "Armazenar capas com o item",
|
||||||
|
"LabelSettingsStoreCoversWithItemHelp": "Por padrão as capas são armazenadas em /metadata/items. Ao ativar essa configuração as capas serão armazenadas na pasta do item na sua biblioteca. Apenas um arquivo chamado \"cover\" será mantido",
|
||||||
|
"LabelSettingsStoreMetadataWithItem": "Armazenar metadados com o item",
|
||||||
|
"LabelSettingsStoreMetadataWithItemHelp": "Por padrão os arquivos de metadados são armazenados em /metadata/items. Ao ativar essa configuração os arquivos de metadados serão armazenadas nas pastas dos itens na sua biblioteca",
|
||||||
|
"LabelSettingsTimeFormat": "Formato da Tempo",
|
||||||
|
"LabelShowAll": "Exibir Todos",
|
||||||
|
"LabelSize": "Tamanho",
|
||||||
|
"LabelSleepTimer": "Timer",
|
||||||
|
"LabelSlug": "Slug",
|
||||||
|
"LabelStart": "Iniciar",
|
||||||
|
"LabelStarted": "Iniciado",
|
||||||
|
"LabelStartedAt": "Iniciado Em",
|
||||||
|
"LabelStartTime": "Horário do Início",
|
||||||
|
"LabelStatsAudioTracks": "Trilhas de Áudio",
|
||||||
|
"LabelStatsAuthors": "Autores",
|
||||||
|
"LabelStatsBestDay": "Melhor Dia",
|
||||||
|
"LabelStatsDailyAverage": "Média Diária",
|
||||||
|
"LabelStatsDays": "Dias",
|
||||||
|
"LabelStatsDaysListened": "Dias Escutando",
|
||||||
|
"LabelStatsHours": "Horas",
|
||||||
|
"LabelStatsInARow": "seguidos",
|
||||||
|
"LabelStatsItemsFinished": "itens Concluídos",
|
||||||
|
"LabelStatsItemsInLibrary": "itens na biblioteca",
|
||||||
|
"LabelStatsMinutes": "minutos",
|
||||||
|
"LabelStatsMinutesListening": "Minutos Escutando",
|
||||||
|
"LabelStatsOverallDays": "Total de Dias",
|
||||||
|
"LabelStatsOverallHours": "Total de Horas",
|
||||||
|
"LabelStatsWeekListening": "Tempo escutando na semana",
|
||||||
|
"LabelSubtitle": "Subtítulo",
|
||||||
|
"LabelSupportedFileTypes": "Tipos de arquivos suportados",
|
||||||
|
"LabelTag": "Etiqueta",
|
||||||
|
"LabelTags": "Etiquetas",
|
||||||
|
"LabelTagsAccessibleToUser": "Etiquetas Acessíveis ao Usuário",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário",
|
||||||
|
"LabelTasks": "Tarefas em Execuçào",
|
||||||
|
"LabelTextEditorBulletedList": "Lista com marcadores",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Lista numerada",
|
||||||
|
"LabelTextEditorUnlink": "Remover link",
|
||||||
|
"LabelTheme": "Tema",
|
||||||
|
"LabelThemeDark": "Escuro",
|
||||||
|
"LabelThemeLight": "Claro",
|
||||||
|
"LabelTimeBase": "Base de tempo",
|
||||||
|
"LabelTimeListened": "Tempo de escuta",
|
||||||
|
"LabelTimeListenedToday": "Tempo de escuta hoje",
|
||||||
|
"LabelTimeRemaining": "{0} restantes",
|
||||||
|
"LabelTimeToShift": "Deslocamento de tempo em segundos",
|
||||||
|
"LabelTitle": "Título",
|
||||||
|
"LabelToolsEmbedMetadata": "Incluir Metadados",
|
||||||
|
"LabelToolsEmbedMetadataDescription": "Incluir metadados no arquivo de áudio, com imagem da capa e capítulos.",
|
||||||
|
"LabelToolsMakeM4b": "Gerar audiobook no formato M4B",
|
||||||
|
"LabelToolsMakeM4bDescription": "Gerar um arquivo de audiobook no formato .M4B com metadados, imagem da capa e capítulos.",
|
||||||
|
"LabelToolsSplitM4b": "Dividir um M4B em MP3s",
|
||||||
|
"LabelToolsSplitM4bDescription": "Criar arquivos MP3s a partir da divisão de um M4B em capítulos, com metadados e imagem de capa.",
|
||||||
|
"LabelTotalDuration": "Duração Total",
|
||||||
|
"LabelTotalTimeListened": "Tempo Total Escutado",
|
||||||
|
"LabelTrackFromFilename": "Trilha a partir do nome do arquivo",
|
||||||
|
"LabelTrackFromMetadata": "Trilha a partir dos Metadados",
|
||||||
|
"LabelTracks": "Trilhas",
|
||||||
|
"LabelTracksMultiTrack": "Várias trilhas",
|
||||||
|
"LabelTracksNone": "Sem trilha",
|
||||||
|
"LabelTracksSingleTrack": "Trilha única",
|
||||||
|
"LabelType": "Tipo",
|
||||||
|
"LabelUnabridged": "Não Abreviada",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
|
"LabelUnknown": "Desconhecido",
|
||||||
|
"LabelUpdateCover": "Atualizar Capa",
|
||||||
|
"LabelUpdateCoverHelp": "Permite sobrescrever capas existentes para os livros selecionados quando uma consulta for localizada",
|
||||||
|
"LabelUpdatedAt": "Atualizado em",
|
||||||
|
"LabelUpdateDetails": "Atualizar Detalhes",
|
||||||
|
"LabelUpdateDetailsHelp": "Permite sobrescrever detalhes existentes para os livros selecionados quando uma consulta for localizada",
|
||||||
|
"LabelUploaderDragAndDrop": "Arraste e solte arquivos ou pastas",
|
||||||
|
"LabelUploaderDropFiles": "Solte os arquivos",
|
||||||
|
"LabelUploaderItemFetchMetadataHelp": "Busca título, autor e série automaticamente",
|
||||||
|
"LabelUseChapterTrack": "Usar a trilha do capítulo",
|
||||||
|
"LabelUseFullTrack": "Usar a trilha toda",
|
||||||
|
"LabelUser": "Usuário",
|
||||||
|
"LabelUsername": "Nome do usuário",
|
||||||
|
"LabelValue": "Valor",
|
||||||
|
"LabelVersion": "Versão",
|
||||||
|
"LabelViewBookmarks": "Ver marcadores",
|
||||||
|
"LabelViewChapters": "Ver capítulos",
|
||||||
|
"LabelViewQueue": "Ver fila do reprodutor",
|
||||||
|
"LabelVolume": "Volume",
|
||||||
|
"LabelWeekdaysToRun": "Dias da semana para executar",
|
||||||
|
"LabelYearReviewHide": "Ocultar Retrospectiva Anual",
|
||||||
|
"LabelYearReviewShow": "Exibir Retrospectiva Anual",
|
||||||
|
"LabelYourAudiobookDuration": "Duração do seu audiobook",
|
||||||
|
"LabelYourBookmarks": "Seus Marcadores",
|
||||||
|
"LabelYourPlaylists": "Suas Listas de Reprodução",
|
||||||
|
"LabelYourProgress": "Seu Progresso",
|
||||||
|
"MessageAddToPlayerQueue": "Adicionar à lista do reprodutor",
|
||||||
|
"MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API do Apprise</a> em execução ou uma api que possa tratar esses mesmos chamados. <br />A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em <code>http://192.168.1.1:8337</code> você deve utilizar <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
|
"MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>não</strong> incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.",
|
||||||
|
"MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.",
|
||||||
|
"MessageBookshelfNoCollections": "Você ainda não criou coleções",
|
||||||
|
"MessageBookshelfNoResultsForFilter": "Sem Resultados para o filtro \"{0}: {1}\"",
|
||||||
|
"MessageBookshelfNoRSSFeeds": "Não existem feeds RSS abertos",
|
||||||
|
"MessageBookshelfNoSeries": "Você não tem séries",
|
||||||
|
"MessageChapterEndIsAfter": "O final do capítulo está além do final do seu audiobook",
|
||||||
|
"MessageChapterErrorFirstNotZero": "O primeiro capítulo precisa começar no 0",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Tempo de início não é válido pois precisa ser menor do que a duração do audioboook",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Tempo de início não é válido pois precisa ser igual ou maior que o tempo de início do capítulo anterior",
|
||||||
|
"MessageChapterStartIsAfter": "Início do capítulo está além do final do seu audiobook",
|
||||||
|
"MessageCheckingCron": "Verificando o cron...",
|
||||||
|
"MessageConfirmCloseFeed": "Tem certeza de que deseja fechar esse feed?",
|
||||||
|
"MessageConfirmDeleteBackup": "Tem certeza de que deseja apagar o backup {0}?",
|
||||||
|
"MessageConfirmDeleteFile": "Essa ação apagará o arquivo do seu sistema de arquivos. Tem certeza?",
|
||||||
|
"MessageConfirmDeleteLibrary": "Tem certeza de que deseja apagar a biblioteca \"{0}\" definitivamente?",
|
||||||
|
"MessageConfirmDeleteLibraryItem": "Essa ação apagará o item da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?",
|
||||||
|
"MessageConfirmDeleteLibraryItems": "Essa ação apagará {0} itens da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?",
|
||||||
|
"MessageConfirmDeleteSession": "Tem certeza de que deseja apagar essa sessão?",
|
||||||
|
"MessageConfirmForceReScan": "Tem certeza de que deseja forçar a nova verificação?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Tem certeza de que deseja marcar todos os episódios como concluídos?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Tem certeza de que deseja marcar todos os episódios como não concluídos?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Tem certeza de que deseja marcar todos os livros nesta série como concluídos?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Tem certeza de que deseja marcar todos os livros nesta série como não concluídos?",
|
||||||
|
"MessageConfirmQuickEmbed": "Aviso! Inclusão rápida não fará backup dos seus arquivos de áudio. Verifique se tem um backup dos seus arquivos de áudio. <br><br>Quer continuar?",
|
||||||
|
"MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos os capítulos?",
|
||||||
|
"MessageConfirmRemoveAuthor": "Tem certeza de que deseja remover o autor \"{0}\"?",
|
||||||
|
"MessageConfirmRemoveCollection": "Tem certeza de que deseja remover a coleção \"{0}\"?",
|
||||||
|
"MessageConfirmRemoveEpisode": "Tem certeza de que deseja remover o episódio \"{0}\"?",
|
||||||
|
"MessageConfirmRemoveEpisodes": "Tem certeza de que deseja remover os {0} episódios?",
|
||||||
|
"MessageConfirmRemoveListeningSessions": "Tem certeza de que deseja remover as {0} sessões de escuta?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Tem certeza de que deseja remover o narrador \"{0}\"?",
|
||||||
|
"MessageConfirmRemovePlaylist": "Tem certeza de que deseja remover a sua lista de reprodução \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Tem certeza de que deseja renomear o gênero \"{0}\" para \"{1}\" em todos os itens?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Aviso: Este gênero já existe então eles serão combinados.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Atenção! Um gênero com um nome semelhante já existe \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Tem certeza de que deseja renomear a etiqueta \"{0}\" para \"{1}\" em todos os itens?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Aviso: Esta etiqueta já existe então elas serão combinadas.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Atenção! Uma etiqueta com um nome semelhante já existe \"{0}\".",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Tem certeza de que deseja uma nova verificação de {0} itens?",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Tem certeza de que deseja enviar {0} ebook(s) \"{1}\" para o dispositivo \"{2}\"?",
|
||||||
|
"MessageDownloadingEpisode": "Realizando o download do episódio",
|
||||||
|
"MessageDragFilesIntoTrackOrder": "Arraste os arquivos para ordenar as trilhas corretamente",
|
||||||
|
"MessageEmbedFinished": "Inclusão Concluída!",
|
||||||
|
"MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download",
|
||||||
|
"MessageFeedURLWillBe": "URL do Feed será {0}",
|
||||||
|
"MessageFetching": "Buscando...",
|
||||||
|
"MessageForceReScanDescription": "verificará todos os arquivos, como uma verificação nova. Etiquetas ID3 de arquivos de áudio, arquivos OPF e arquivos de texto serão tratados como novos.",
|
||||||
|
"MessageImportantNotice": "Aviso Importante!",
|
||||||
|
"MessageInsertChapterBelow": "Inserir capítulo abaixo",
|
||||||
|
"MessageItemsSelected": "{0} Itens Selecionados",
|
||||||
|
"MessageItemsUpdated": "{0} Itens Atualizados",
|
||||||
|
"MessageJoinUsOn": "Junte-se a nós",
|
||||||
|
"MessageListeningSessionsInTheLastYear": "{0} sessões de escuta no ano anterior",
|
||||||
|
"MessageLoading": "Carregando...",
|
||||||
|
"MessageLoadingFolders": "Carregando pastas...",
|
||||||
|
"MessageM4BFailed": "Falha no M4B!",
|
||||||
|
"MessageM4BFinished": "M4B Concluído!",
|
||||||
|
"MessageMapChapterTitles": "Designar títulos de capítulos a partir dos capítulos existentes no audiobook sem ajustar seus tempos",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Marcar todos os episódios como concluídos",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Marcar todos os episódios como não concluídos",
|
||||||
|
"MessageMarkAsFinished": "Marcar como Concluído",
|
||||||
|
"MessageMarkAsNotFinished": "Marcar como Não Concluído",
|
||||||
|
"MessageMatchBooksDescription": "tentará consultar os livros da biblioteca no fornecedor de busca selecionado e preencher os detalhes ausentes e a capa. Não sobrescreve os detalhes.",
|
||||||
|
"MessageNoAudioTracks": "Sem trilhas de áudio",
|
||||||
|
"MessageNoAuthors": "Sem Autores",
|
||||||
|
"MessageNoBackups": "Sem Backups",
|
||||||
|
"MessageNoBookmarks": "Sem Marcadores",
|
||||||
|
"MessageNoChapters": "Sem Capítulos",
|
||||||
|
"MessageNoCollections": "Sem Coleções",
|
||||||
|
"MessageNoCoversFound": "Nenhuma Capa Encontrada",
|
||||||
|
"MessageNoDescription": "Sem Descrições",
|
||||||
|
"MessageNoDownloadsInProgress": "Não existem downloads em andamento",
|
||||||
|
"MessageNoDownloadsQueued": "Não existem itens na fila de download",
|
||||||
|
"MessageNoEpisodeMatchesFound": "Não existem episódios correspondentes",
|
||||||
|
"MessageNoEpisodes": "Sem Episódios",
|
||||||
|
"MessageNoFoldersAvailable": "Nenhuma Pasta Disponível",
|
||||||
|
"MessageNoGenres": "Sem Gêneros",
|
||||||
|
"MessageNoIssues": "Sem Problemas",
|
||||||
|
"MessageNoItems": "Sem Itens",
|
||||||
|
"MessageNoItemsFound": "Nenhum item encontrado",
|
||||||
|
"MessageNoListeningSessions": "Sem Sessões de Escuta",
|
||||||
|
"MessageNoLogs": "Sem Logs",
|
||||||
|
"MessageNoMediaProgress": "Sem Progresso de Mídia",
|
||||||
|
"MessageNoNotifications": "Sem Notificações",
|
||||||
|
"MessageNoPodcastsFound": "Nenhum podcast encontrado",
|
||||||
|
"MessageNoResults": "Sem resultados",
|
||||||
|
"MessageNoSearchResultsFor": "Sem resultados para \"{0}\"",
|
||||||
|
"MessageNoSeries": "Sem Séries",
|
||||||
|
"MessageNoTags": "Sem etiquetas",
|
||||||
|
"MessageNoTasksRunning": "Sem Tarefas em Execução",
|
||||||
|
"MessageNotYetImplemented": "Ainda não implementado",
|
||||||
|
"MessageNoUpdateNecessary": "Não é necessária a atualização",
|
||||||
|
"MessageNoUpdatesWereNecessary": "Nenhuma atualização é necessária",
|
||||||
|
"MessageNoUserPlaylists": "Você não tem listas de reprodução",
|
||||||
|
"MessageOr": "ou",
|
||||||
|
"MessagePauseChapter": "Pausar reprodução do capítulo",
|
||||||
|
"MessagePlayChapter": "Escutar o início do capítulo",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Criar uma lista de reprodução a partir da coleção",
|
||||||
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast não tem uma URL do feed RSS para ser usada na consulta",
|
||||||
|
"MessageQuickMatchDescription": "Preenche detalhes vazios do item & capa com o primeiro resultado de '{0}'. Não sobrescreve detalhes a não ser que a configuração 'Preferir metadados consultados' do servidor esteja ativa.",
|
||||||
|
"MessageRemoveChapter": "Remover capítulo",
|
||||||
|
"MessageRemoveEpisodes": "Remover {0} episódio(s)",
|
||||||
|
"MessageRemoveFromPlayerQueue": "Remover da lista do reprodutor",
|
||||||
|
"MessageRemoveUserWarning": "Tem certeza de que deseja apagar definitivamente o usuário \"{0}\"?",
|
||||||
|
"MessageReportBugsAndContribute": "Reporte bugs, peça funcionalidades e contribua em",
|
||||||
|
"MessageResetChaptersConfirm": "Tem certeza de que deseja resetar os capítulos e desfazer as alterações realizadas?",
|
||||||
|
"MessageRestoreBackupConfirm": "Tem certeza de que deseja restaurar o backup criado em",
|
||||||
|
"MessageRestoreBackupWarning": "Restaurar um backup sobrescreverá totalmente o banco de dados localizado em /config e as imagens de capa em /metadata/items & /metadata/authors.<br /><br />Backups não alteram quaisquer arquivos nas pastas da sua biblioteca. Se a configuração do servidor de armazenar a arte da capa e os metadados nas pastas da sua biblioteca estiver ativa, esses itens não estão no backup e não serão sobrescritos.<br /><br />Todos os clientes usando o seu servidor serão atualizados automaticamente.",
|
||||||
|
"MessageSearchResultsFor": "Resultado da busca por",
|
||||||
|
"MessageSelected": "{0} selecionado(s)",
|
||||||
|
"MessageServerCouldNotBeReached": "Não foi possível estabelecer conexão com o servidor",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Definir os capítulos usando cada arquivo de áudio como um capítulo e o nome do arquivo como o título do capítulo",
|
||||||
|
"MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?",
|
||||||
|
"MessageThinking": "Pensando...",
|
||||||
|
"MessageUploaderItemFailed": "Falha no upload",
|
||||||
|
"MessageUploaderItemSuccess": "Upload realizado!",
|
||||||
|
"MessageUploading": "Realizando o upload...",
|
||||||
|
"MessageValidCronExpression": "Expressão do cron válida",
|
||||||
|
"MessageWatcherIsDisabledGlobally": "Monitoramento está desativado nas configurações do servidor",
|
||||||
|
"MessageXLibraryIsEmpty": "Biblioteca {0} está vazia!",
|
||||||
|
"MessageYourAudiobookDurationIsLonger": "A duração do seu audiobook é maior do que a duração encontrada",
|
||||||
|
"MessageYourAudiobookDurationIsShorter": "A duração do seu audiobook é menor do que a duração encontrada",
|
||||||
|
"NoteChangeRootPassword": "O usuário Admiistrador é o único usuário que pode não ter uma senha",
|
||||||
|
"NoteChapterEditorTimes": "Aviso: O tempo de início do primeiro capítulo precisa ficar em 0:00 e o tempo de início do último capítulo não pode exceder a duração deste audiobook.",
|
||||||
|
"NoteFolderPicker": "Aviso: pastas já designadas não serão exibidas",
|
||||||
|
"NoteRSSFeedPodcastAppsHttps": "Atenção: A maioria dos aplicativos de podcasts requer que a URL do feed RSS use HTTPS",
|
||||||
|
"NoteRSSFeedPodcastAppsPubDate": "Atenção: Um ou mais dos seus episódios não tem uma data de publicação. Alguns aplicativos de podcasts requerem isto.",
|
||||||
|
"NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.",
|
||||||
|
"NoteUploaderOnlyAudioFiles": "Ao subir apenas arquivos de áudio, cada arquivo será tratado como um audiobook distinto.",
|
||||||
|
"NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar uma pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.",
|
||||||
|
"PlaceholderNewCollection": "Novo nome da coleção",
|
||||||
|
"PlaceholderNewFolderPath": "Novo caminho para a pasta",
|
||||||
|
"PlaceholderNewPlaylist": "Novo nome da lista de reprodução",
|
||||||
|
"PlaceholderSearch": "Buscar..",
|
||||||
|
"PlaceholderSearchEpisode": "Buscar Episódio..",
|
||||||
|
"ToastAccountUpdateFailed": "Falha ao atualizar a conta",
|
||||||
|
"ToastAccountUpdateSuccess": "Conta atualizada",
|
||||||
|
"ToastAuthorImageRemoveFailed": "Falha ao remover imagem",
|
||||||
|
"ToastAuthorImageRemoveSuccess": "Imagem do autor removida",
|
||||||
|
"ToastAuthorUpdateFailed": "Falha ao atualizar o autor",
|
||||||
|
"ToastAuthorUpdateMerged": "Autor combinado",
|
||||||
|
"ToastAuthorUpdateSuccess": "Autor atualizado",
|
||||||
|
"ToastAuthorUpdateSuccessNoImageFound": "Autor atualizado (nenhuma imagem encontrada)",
|
||||||
|
"ToastBackupCreateFailed": "Falha ao criar backup",
|
||||||
|
"ToastBackupCreateSuccess": "Backup criado",
|
||||||
|
"ToastBackupDeleteFailed": "Falha ao apagar backup",
|
||||||
|
"ToastBackupDeleteSuccess": "Backup apagado",
|
||||||
|
"ToastBackupRestoreFailed": "Falha ao restaurar backup",
|
||||||
|
"ToastBackupUploadFailed": "Falha no upload do backup",
|
||||||
|
"ToastBackupUploadSuccess": "Upload do backup realizado",
|
||||||
|
"ToastBatchUpdateFailed": "Falha na atualização em lote",
|
||||||
|
"ToastBatchUpdateSuccess": "Atualização em lote realizada",
|
||||||
|
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
|
||||||
|
"ToastBookmarkCreateSuccess": "Marcador adicionado",
|
||||||
|
"ToastBookmarkRemoveFailed": "Falha ao remover marcador",
|
||||||
|
"ToastBookmarkRemoveSuccess": "Marcador removido",
|
||||||
|
"ToastBookmarkUpdateFailed": "Falha ao atualizar o marcador",
|
||||||
|
"ToastBookmarkUpdateSuccess": "Marcador atualizado",
|
||||||
|
"ToastChaptersHaveErrors": "Capítulos com erro",
|
||||||
|
"ToastChaptersMustHaveTitles": "Capítulos precisam ter títulos",
|
||||||
|
"ToastCollectionItemsRemoveFailed": "Falha ao remover item(ns) da coleção",
|
||||||
|
"ToastCollectionItemsRemoveSuccess": "Item(ns) removidos da coleção",
|
||||||
|
"ToastCollectionRemoveFailed": "Falha ao remover coleção",
|
||||||
|
"ToastCollectionRemoveSuccess": "Coleção removida",
|
||||||
|
"ToastCollectionUpdateFailed": "Falha ao atualizar coleção",
|
||||||
|
"ToastCollectionUpdateSuccess": "Coleção atualizada",
|
||||||
|
"ToastItemCoverUpdateFailed": "Falha ao atualizar capa do item",
|
||||||
|
"ToastItemCoverUpdateSuccess": "Capa do item atualizada",
|
||||||
|
"ToastItemDetailsUpdateFailed": "Falha ao atualizar detalhes do item",
|
||||||
|
"ToastItemDetailsUpdateSuccess": "Detalhes do item atualizados",
|
||||||
|
"ToastItemDetailsUpdateUnneeded": "Nenhuma atualização necessária para os detalhes do item",
|
||||||
|
"ToastItemMarkedAsFinishedFailed": "Falha ao marcar como Concluído",
|
||||||
|
"ToastItemMarkedAsFinishedSuccess": "Item marcado como Concluído",
|
||||||
|
"ToastItemMarkedAsNotFinishedFailed": "Falha ao marcar como Não Concluído",
|
||||||
|
"ToastItemMarkedAsNotFinishedSuccess": "Item marcado como Não Concluído",
|
||||||
|
"ToastLibraryCreateFailed": "Falha ao criar biblioteca",
|
||||||
|
"ToastLibraryCreateSuccess": "Biblioteca \"{0}\" criada",
|
||||||
|
"ToastLibraryDeleteFailed": "Falha ao apagar biblioteca",
|
||||||
|
"ToastLibraryDeleteSuccess": "Biblioteca apagada",
|
||||||
|
"ToastLibraryScanFailedToStart": "Falha ao iniciar verificação",
|
||||||
|
"ToastLibraryScanStarted": "Verificação da biblioteca iniciada",
|
||||||
|
"ToastLibraryUpdateFailed": "Falha ao atualizar a biblioteca",
|
||||||
|
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada",
|
||||||
|
"ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução",
|
||||||
|
"ToastPlaylistCreateSuccess": "Lista de reprodução criada",
|
||||||
|
"ToastPlaylistRemoveFailed": "Falha ao remover lista de reprodução",
|
||||||
|
"ToastPlaylistRemoveSuccess": "Lista de reprodução removida",
|
||||||
|
"ToastPlaylistUpdateFailed": "Falha ao atualizar lista de reprodução",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Lista de reprodução atualizada",
|
||||||
|
"ToastPodcastCreateFailed": "Falha ao criar podcast",
|
||||||
|
"ToastPodcastCreateSuccess": "Podcast criado",
|
||||||
|
"ToastRemoveItemFromCollectionFailed": "Falha ao remover item da coleção",
|
||||||
|
"ToastRemoveItemFromCollectionSuccess": "Item removido da coleção",
|
||||||
|
"ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS",
|
||||||
|
"ToastRSSFeedCloseSuccess": "Feed RSS fechado",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Falha ao enviar ebook para dispositivo",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook enviado para o dispositivo \"{0}\"",
|
||||||
|
"ToastSeriesUpdateFailed": "Falha ao atualizar série",
|
||||||
|
"ToastSeriesUpdateSuccess": "Série atualizada",
|
||||||
|
"ToastSessionDeleteFailed": "Falha ao apagar sessão",
|
||||||
|
"ToastSessionDeleteSuccess": "Sessão apagada",
|
||||||
|
"ToastSocketConnected": "Socket conectado",
|
||||||
|
"ToastSocketDisconnected": "Socket desconectado",
|
||||||
|
"ToastSocketFailedToConnect": "Falha na conexão do socket",
|
||||||
|
"ToastUserDeleteFailed": "Falha ao apagar usuário",
|
||||||
|
"ToastUserDeleteSuccess": "Usuário apagado"
|
||||||
|
}
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Скрыть",
|
"ButtonHide": "Скрыть",
|
||||||
"ButtonHome": "Домой",
|
"ButtonHome": "Домой",
|
||||||
"ButtonIssues": "Проблемы",
|
"ButtonIssues": "Проблемы",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Последнее",
|
"ButtonLatest": "Последнее",
|
||||||
"ButtonLibrary": "Библиотека",
|
"ButtonLibrary": "Библиотека",
|
||||||
"ButtonLogout": "Выход",
|
"ButtonLogout": "Выход",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Найти всех авторов",
|
"ButtonMatchAllAuthors": "Найти всех авторов",
|
||||||
"ButtonMatchBooks": "Найти книги",
|
"ButtonMatchBooks": "Найти книги",
|
||||||
"ButtonNevermind": "Не важно",
|
"ButtonNevermind": "Не важно",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Открыть канал",
|
"ButtonOpenFeed": "Открыть канал",
|
||||||
"ButtonOpenManager": "Открыть менеджер",
|
"ButtonOpenManager": "Открыть менеджер",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Слушать",
|
"ButtonPlay": "Слушать",
|
||||||
"ButtonPlaying": "Проигрывается",
|
"ButtonPlaying": "Проигрывается",
|
||||||
"ButtonPlaylists": "Плейлисты",
|
"ButtonPlaylists": "Плейлисты",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Очистить весь кэш",
|
"ButtonPurgeAllCache": "Очистить весь кэш",
|
||||||
"ButtonPurgeItemsCache": "Очистить кэш элементов",
|
"ButtonPurgeItemsCache": "Очистить кэш элементов",
|
||||||
"ButtonPurgeMediaProgress": "Очистить прогресс медиа",
|
"ButtonPurgeMediaProgress": "Очистить прогресс медиа",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Удалить из очереди",
|
"ButtonQueueRemoveItem": "Удалить из очереди",
|
||||||
"ButtonQuickMatch": "Быстрый поиск",
|
"ButtonQuickMatch": "Быстрый поиск",
|
||||||
"ButtonRead": "Читать",
|
"ButtonRead": "Читать",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Удалить",
|
"ButtonRemove": "Удалить",
|
||||||
"ButtonRemoveAll": "Удалить всё",
|
"ButtonRemoveAll": "Удалить всё",
|
||||||
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Выберите путь папки",
|
"ButtonSelectFolderPath": "Выберите путь папки",
|
||||||
"ButtonSeries": "Серии",
|
"ButtonSeries": "Серии",
|
||||||
"ButtonSetChaptersFromTracks": "Установить главы из треков",
|
"ButtonSetChaptersFromTracks": "Установить главы из треков",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Смещение",
|
"ButtonShiftTimes": "Смещение",
|
||||||
"ButtonShow": "Показать",
|
"ButtonShow": "Показать",
|
||||||
"ButtonStartM4BEncode": "Начать кодирование M4B",
|
"ButtonStartM4BEncode": "Начать кодирование M4B",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Элементы коллекции",
|
"HeaderCollectionItems": "Элементы коллекции",
|
||||||
"HeaderCover": "Обложка",
|
"HeaderCover": "Обложка",
|
||||||
"HeaderCurrentDownloads": "Текущие закачки",
|
"HeaderCurrentDownloads": "Текущие закачки",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Подробности",
|
"HeaderDetails": "Подробности",
|
||||||
"HeaderDownloadQueue": "Очередь скачивания",
|
"HeaderDownloadQueue": "Очередь скачивания",
|
||||||
"HeaderEbookFiles": "Файлы e-книг",
|
"HeaderEbookFiles": "Файлы e-книг",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Обновить детали",
|
"HeaderUpdateDetails": "Обновить детали",
|
||||||
"HeaderUpdateLibrary": "Обновить библиотеку",
|
"HeaderUpdateLibrary": "Обновить библиотеку",
|
||||||
"HeaderUsers": "Пользователи",
|
"HeaderUsers": "Пользователи",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Ваша статистика",
|
"HeaderYourStats": "Ваша статистика",
|
||||||
"LabelAbridged": "Сокращенное издание",
|
"LabelAbridged": "Сокращенное издание",
|
||||||
"LabelAccountType": "Тип учетной записи",
|
"LabelAccountType": "Тип учетной записи",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Мета теги",
|
"LabelMetaTags": "Мета теги",
|
||||||
"LabelMinute": "Минуты",
|
"LabelMinute": "Минуты",
|
||||||
"LabelMissing": "Потеряно",
|
"LabelMissing": "Потеряно",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Потерянные части",
|
"LabelMissingParts": "Потерянные части",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
|
"LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
|
||||||
"LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.",
|
"LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.",
|
||||||
"LabelMore": "Еще",
|
"LabelMore": "Еще",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Может скачивать",
|
"LabelPermissionsDownload": "Может скачивать",
|
||||||
"LabelPermissionsUpdate": "Может обновлять",
|
"LabelPermissionsUpdate": "Может обновлять",
|
||||||
"LabelPermissionsUpload": "Может закачивать",
|
"LabelPermissionsUpload": "Может закачивать",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Путь к фото/URL",
|
"LabelPhotoPathURL": "Путь к фото/URL",
|
||||||
"LabelPlaylists": "Плейлисты",
|
"LabelPlaylists": "Плейлисты",
|
||||||
"LabelPlayMethod": "Метод воспроизведения",
|
"LabelPlayMethod": "Метод воспроизведения",
|
||||||
"LabelPodcast": "Подкаст",
|
"LabelPodcast": "Подкаст",
|
||||||
"LabelPodcasts": "Подкасты",
|
"LabelPodcasts": "Подкасты",
|
||||||
|
"LabelPodcastSearchRegion": "Регион поиска подкастов",
|
||||||
"LabelPodcastType": "Тип подкаста",
|
"LabelPodcastType": "Тип подкаста",
|
||||||
"LabelPort": "Порт",
|
"LabelPort": "Порт",
|
||||||
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Серия",
|
"LabelSeries": "Серия",
|
||||||
"LabelSeriesName": "Имя серии",
|
"LabelSeriesName": "Имя серии",
|
||||||
"LabelSeriesProgress": "Прогресс серии",
|
"LabelSeriesProgress": "Прогресс серии",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Установить как основную",
|
"LabelSetEbookAsPrimary": "Установить как основную",
|
||||||
"LabelSetEbookAsSupplementary": "Установить как дополнительную",
|
"LabelSetEbookAsSupplementary": "Установить как дополнительную",
|
||||||
"LabelSettingsAudiobooksOnly": "Только аудиокниги",
|
"LabelSettingsAudiobooksOnly": "Только аудиокниги",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Очередь воспроизведения",
|
"LabelViewQueue": "Очередь воспроизведения",
|
||||||
"LabelVolume": "Громкость",
|
"LabelVolume": "Громкость",
|
||||||
"LabelWeekdaysToRun": "Дни недели для запуска",
|
"LabelWeekdaysToRun": "Дни недели для запуска",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Продолжительность Вашей книги",
|
"LabelYourAudiobookDuration": "Продолжительность Вашей книги",
|
||||||
"LabelYourBookmarks": "Ваши закладки",
|
"LabelYourBookmarks": "Ваши закладки",
|
||||||
"LabelYourPlaylists": "Ваши плейлисты",
|
"LabelYourPlaylists": "Ваши плейлисты",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Dölj",
|
"ButtonHide": "Dölj",
|
||||||
"ButtonHome": "Hem",
|
"ButtonHome": "Hem",
|
||||||
"ButtonIssues": "Problem",
|
"ButtonIssues": "Problem",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "Senaste",
|
"ButtonLatest": "Senaste",
|
||||||
"ButtonLibrary": "Bibliotek",
|
"ButtonLibrary": "Bibliotek",
|
||||||
"ButtonLogout": "Logga ut",
|
"ButtonLogout": "Logga ut",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Matcha alla författare",
|
"ButtonMatchAllAuthors": "Matcha alla författare",
|
||||||
"ButtonMatchBooks": "Matcha böcker",
|
"ButtonMatchBooks": "Matcha böcker",
|
||||||
"ButtonNevermind": "Glöm det",
|
"ButtonNevermind": "Glöm det",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Okej",
|
"ButtonOk": "Okej",
|
||||||
"ButtonOpenFeed": "Öppna flöde",
|
"ButtonOpenFeed": "Öppna flöde",
|
||||||
"ButtonOpenManager": "Öppna Manager",
|
"ButtonOpenManager": "Öppna Manager",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Spela",
|
"ButtonPlay": "Spela",
|
||||||
"ButtonPlaying": "Spelar",
|
"ButtonPlaying": "Spelar",
|
||||||
"ButtonPlaylists": "Spellistor",
|
"ButtonPlaylists": "Spellistor",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Rensa all cache",
|
"ButtonPurgeAllCache": "Rensa all cache",
|
||||||
"ButtonPurgeItemsCache": "Rensa föremåls-cache",
|
"ButtonPurgeItemsCache": "Rensa föremåls-cache",
|
||||||
"ButtonPurgeMediaProgress": "Rensa medieförlopp",
|
"ButtonPurgeMediaProgress": "Rensa medieförlopp",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "Ta bort från kön",
|
"ButtonQueueRemoveItem": "Ta bort från kön",
|
||||||
"ButtonQuickMatch": "Snabb matchning",
|
"ButtonQuickMatch": "Snabb matchning",
|
||||||
"ButtonRead": "Läs",
|
"ButtonRead": "Läs",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "Ta bort",
|
"ButtonRemove": "Ta bort",
|
||||||
"ButtonRemoveAll": "Ta bort alla",
|
"ButtonRemoveAll": "Ta bort alla",
|
||||||
"ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt",
|
"ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "Välj mappens sökväg",
|
"ButtonSelectFolderPath": "Välj mappens sökväg",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
"ButtonSetChaptersFromTracks": "Ställ in kapitel från spår",
|
"ButtonSetChaptersFromTracks": "Ställ in kapitel från spår",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "Förskjut tider",
|
"ButtonShiftTimes": "Förskjut tider",
|
||||||
"ButtonShow": "Visa",
|
"ButtonShow": "Visa",
|
||||||
"ButtonStartM4BEncode": "Starta M4B-kodning",
|
"ButtonStartM4BEncode": "Starta M4B-kodning",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "Samlingselement",
|
"HeaderCollectionItems": "Samlingselement",
|
||||||
"HeaderCover": "Omslag",
|
"HeaderCover": "Omslag",
|
||||||
"HeaderCurrentDownloads": "Aktuella nedladdningar",
|
"HeaderCurrentDownloads": "Aktuella nedladdningar",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Detaljer",
|
"HeaderDetails": "Detaljer",
|
||||||
"HeaderDownloadQueue": "Nedladdningskö",
|
"HeaderDownloadQueue": "Nedladdningskö",
|
||||||
"HeaderEbookFiles": "E-boksfiler",
|
"HeaderEbookFiles": "E-boksfiler",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "Uppdatera detaljer",
|
"HeaderUpdateDetails": "Uppdatera detaljer",
|
||||||
"HeaderUpdateLibrary": "Uppdatera bibliotek",
|
"HeaderUpdateLibrary": "Uppdatera bibliotek",
|
||||||
"HeaderUsers": "Användare",
|
"HeaderUsers": "Användare",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "Dina statistik",
|
"HeaderYourStats": "Dina statistik",
|
||||||
"LabelAbridged": "Förkortad",
|
"LabelAbridged": "Förkortad",
|
||||||
"LabelAccountType": "Kontotyp",
|
"LabelAccountType": "Kontotyp",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "Metamärken",
|
"LabelMetaTags": "Metamärken",
|
||||||
"LabelMinute": "Minut",
|
"LabelMinute": "Minut",
|
||||||
"LabelMissing": "Saknad",
|
"LabelMissing": "Saknad",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "Saknade delar",
|
"LabelMissingParts": "Saknade delar",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"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.",
|
"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",
|
"LabelMore": "Mer",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "Kan ladda ner",
|
"LabelPermissionsDownload": "Kan ladda ner",
|
||||||
"LabelPermissionsUpdate": "Kan uppdatera",
|
"LabelPermissionsUpdate": "Kan uppdatera",
|
||||||
"LabelPermissionsUpload": "Kan ladda upp",
|
"LabelPermissionsUpload": "Kan ladda upp",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "Bildsökväg/URL",
|
"LabelPhotoPathURL": "Bildsökväg/URL",
|
||||||
"LabelPlaylists": "Spellistor",
|
"LabelPlaylists": "Spellistor",
|
||||||
"LabelPlayMethod": "Spelläge",
|
"LabelPlayMethod": "Spelläge",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast-sökområde",
|
||||||
"LabelPodcastType": "Podcasttyp",
|
"LabelPodcastType": "Podcasttyp",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
|
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Serienamn",
|
"LabelSeriesName": "Serienamn",
|
||||||
"LabelSeriesProgress": "Serieframsteg",
|
"LabelSeriesProgress": "Serieframsteg",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Ange som primär",
|
"LabelSetEbookAsPrimary": "Ange som primär",
|
||||||
"LabelSetEbookAsSupplementary": "Ange som kompletterande",
|
"LabelSetEbookAsSupplementary": "Ange som kompletterande",
|
||||||
"LabelSettingsAudiobooksOnly": "Endast ljudböcker",
|
"LabelSettingsAudiobooksOnly": "Endast ljudböcker",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "Visa spellista",
|
"LabelViewQueue": "Visa spellista",
|
||||||
"LabelVolume": "Volym",
|
"LabelVolume": "Volym",
|
||||||
"LabelWeekdaysToRun": "Vardagar att köra",
|
"LabelWeekdaysToRun": "Vardagar att köra",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Din ljudboks varaktighet",
|
"LabelYourAudiobookDuration": "Din ljudboks varaktighet",
|
||||||
"LabelYourBookmarks": "Dina bokmärken",
|
"LabelYourBookmarks": "Dina bokmärken",
|
||||||
"LabelYourPlaylists": "Dina spellistor",
|
"LabelYourPlaylists": "Dina spellistor",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "隐藏",
|
"ButtonHide": "隐藏",
|
||||||
"ButtonHome": "首页",
|
"ButtonHome": "首页",
|
||||||
"ButtonIssues": "问题",
|
"ButtonIssues": "问题",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"ButtonLatest": "最新",
|
"ButtonLatest": "最新",
|
||||||
"ButtonLibrary": "媒体库",
|
"ButtonLibrary": "媒体库",
|
||||||
"ButtonLogout": "注销",
|
"ButtonLogout": "注销",
|
||||||
@ -41,12 +43,17 @@
|
|||||||
"ButtonMatchAllAuthors": "匹配所有作者",
|
"ButtonMatchAllAuthors": "匹配所有作者",
|
||||||
"ButtonMatchBooks": "匹配图书",
|
"ButtonMatchBooks": "匹配图书",
|
||||||
"ButtonNevermind": "没有关系",
|
"ButtonNevermind": "没有关系",
|
||||||
|
"ButtonNext": "Next",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "确定",
|
"ButtonOk": "确定",
|
||||||
"ButtonOpenFeed": "打开源",
|
"ButtonOpenFeed": "打开源",
|
||||||
"ButtonOpenManager": "打开管理器",
|
"ButtonOpenManager": "打开管理器",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "播放",
|
"ButtonPlay": "播放",
|
||||||
"ButtonPlaying": "正在播放",
|
"ButtonPlaying": "正在播放",
|
||||||
"ButtonPlaylists": "播放列表",
|
"ButtonPlaylists": "播放列表",
|
||||||
|
"ButtonPrevious": "Previous",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "清理所有缓存",
|
"ButtonPurgeAllCache": "清理所有缓存",
|
||||||
"ButtonPurgeItemsCache": "清理项目缓存",
|
"ButtonPurgeItemsCache": "清理项目缓存",
|
||||||
"ButtonPurgeMediaProgress": "清理媒体进度",
|
"ButtonPurgeMediaProgress": "清理媒体进度",
|
||||||
@ -54,6 +61,7 @@
|
|||||||
"ButtonQueueRemoveItem": "从队列中移除",
|
"ButtonQueueRemoveItem": "从队列中移除",
|
||||||
"ButtonQuickMatch": "快速匹配",
|
"ButtonQuickMatch": "快速匹配",
|
||||||
"ButtonRead": "读取",
|
"ButtonRead": "读取",
|
||||||
|
"ButtonRefresh": "Refresh",
|
||||||
"ButtonRemove": "移除",
|
"ButtonRemove": "移除",
|
||||||
"ButtonRemoveAll": "移除所有",
|
"ButtonRemoveAll": "移除所有",
|
||||||
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
||||||
@ -73,6 +81,7 @@
|
|||||||
"ButtonSelectFolderPath": "选择文件夹路径",
|
"ButtonSelectFolderPath": "选择文件夹路径",
|
||||||
"ButtonSeries": "系列",
|
"ButtonSeries": "系列",
|
||||||
"ButtonSetChaptersFromTracks": "将音轨设置为章节",
|
"ButtonSetChaptersFromTracks": "将音轨设置为章节",
|
||||||
|
"ButtonShare": "Share",
|
||||||
"ButtonShiftTimes": "快速调整时间",
|
"ButtonShiftTimes": "快速调整时间",
|
||||||
"ButtonShow": "显示",
|
"ButtonShow": "显示",
|
||||||
"ButtonStartM4BEncode": "开始 M4B 编码",
|
"ButtonStartM4BEncode": "开始 M4B 编码",
|
||||||
@ -104,6 +113,7 @@
|
|||||||
"HeaderCollectionItems": "收藏项目",
|
"HeaderCollectionItems": "收藏项目",
|
||||||
"HeaderCover": "封面",
|
"HeaderCover": "封面",
|
||||||
"HeaderCurrentDownloads": "当前下载",
|
"HeaderCurrentDownloads": "当前下载",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "详情",
|
"HeaderDetails": "详情",
|
||||||
"HeaderDownloadQueue": "下载队列",
|
"HeaderDownloadQueue": "下载队列",
|
||||||
"HeaderEbookFiles": "电子书文件",
|
"HeaderEbookFiles": "电子书文件",
|
||||||
@ -174,6 +184,7 @@
|
|||||||
"HeaderUpdateDetails": "更新详情",
|
"HeaderUpdateDetails": "更新详情",
|
||||||
"HeaderUpdateLibrary": "更新媒体库",
|
"HeaderUpdateLibrary": "更新媒体库",
|
||||||
"HeaderUsers": "用户",
|
"HeaderUsers": "用户",
|
||||||
|
"HeaderYearReview": "Year {0} in Review",
|
||||||
"HeaderYourStats": "你的统计数据",
|
"HeaderYourStats": "你的统计数据",
|
||||||
"LabelAbridged": "概要",
|
"LabelAbridged": "概要",
|
||||||
"LabelAccountType": "帐户类型",
|
"LabelAccountType": "帐户类型",
|
||||||
@ -345,7 +356,9 @@
|
|||||||
"LabelMetaTags": "元标签",
|
"LabelMetaTags": "元标签",
|
||||||
"LabelMinute": "分钟",
|
"LabelMinute": "分钟",
|
||||||
"LabelMissing": "丢失",
|
"LabelMissing": "丢失",
|
||||||
|
"LabelMissingEbook": "Has no ebook",
|
||||||
"LabelMissingParts": "丢失的部分",
|
"LabelMissingParts": "丢失的部分",
|
||||||
|
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||||
"LabelMobileRedirectURIs": "允许移动应用重定向 URI",
|
"LabelMobileRedirectURIs": "允许移动应用重定向 URI",
|
||||||
"LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.",
|
"LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.",
|
||||||
"LabelMore": "更多",
|
"LabelMore": "更多",
|
||||||
@ -385,11 +398,13 @@
|
|||||||
"LabelPermissionsDownload": "可以下载",
|
"LabelPermissionsDownload": "可以下载",
|
||||||
"LabelPermissionsUpdate": "可以更新",
|
"LabelPermissionsUpdate": "可以更新",
|
||||||
"LabelPermissionsUpload": "可以上传",
|
"LabelPermissionsUpload": "可以上传",
|
||||||
|
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||||
"LabelPhotoPathURL": "图片路径或 URL",
|
"LabelPhotoPathURL": "图片路径或 URL",
|
||||||
"LabelPlaylists": "播放列表",
|
"LabelPlaylists": "播放列表",
|
||||||
"LabelPlayMethod": "播放方法",
|
"LabelPlayMethod": "播放方法",
|
||||||
"LabelPodcast": "播客",
|
"LabelPodcast": "播客",
|
||||||
"LabelPodcasts": "播客",
|
"LabelPodcasts": "播客",
|
||||||
|
"LabelPodcastSearchRegion": "播客搜索地区",
|
||||||
"LabelPodcastType": "播客类型",
|
"LabelPodcastType": "播客类型",
|
||||||
"LabelPort": "端口",
|
"LabelPort": "端口",
|
||||||
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
||||||
@ -429,6 +444,7 @@
|
|||||||
"LabelSeries": "系列",
|
"LabelSeries": "系列",
|
||||||
"LabelSeriesName": "系列名称",
|
"LabelSeriesName": "系列名称",
|
||||||
"LabelSeriesProgress": "系列进度",
|
"LabelSeriesProgress": "系列进度",
|
||||||
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "设置为主",
|
"LabelSetEbookAsPrimary": "设置为主",
|
||||||
"LabelSetEbookAsSupplementary": "设置为补充",
|
"LabelSetEbookAsSupplementary": "设置为补充",
|
||||||
"LabelSettingsAudiobooksOnly": "只有有声读物",
|
"LabelSettingsAudiobooksOnly": "只有有声读物",
|
||||||
@ -545,6 +561,8 @@
|
|||||||
"LabelViewQueue": "查看播放列表",
|
"LabelViewQueue": "查看播放列表",
|
||||||
"LabelVolume": "音量",
|
"LabelVolume": "音量",
|
||||||
"LabelWeekdaysToRun": "工作日运行",
|
"LabelWeekdaysToRun": "工作日运行",
|
||||||
|
"LabelYearReviewHide": "Hide Year in Review",
|
||||||
|
"LabelYearReviewShow": "See Year in Review",
|
||||||
"LabelYourAudiobookDuration": "你的有声读物持续时间",
|
"LabelYourAudiobookDuration": "你的有声读物持续时间",
|
||||||
"LabelYourBookmarks": "你的书签",
|
"LabelYourBookmarks": "你的书签",
|
||||||
"LabelYourPlaylists": "你的播放列表",
|
"LabelYourPlaylists": "你的播放列表",
|
||||||
|
135
custom-metadata-provider-specification.yaml
Normal file
135
custom-metadata-provider-specification.yaml
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
servers:
|
||||||
|
- url: https://example.com
|
||||||
|
description: Local server
|
||||||
|
info:
|
||||||
|
license:
|
||||||
|
name: MIT
|
||||||
|
url: https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
|
||||||
|
title: Custom Metadata Provider
|
||||||
|
version: 0.1.0
|
||||||
|
security:
|
||||||
|
- api_key: []
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/search:
|
||||||
|
get:
|
||||||
|
description: Search for books
|
||||||
|
operationId: search
|
||||||
|
summary: Search for books
|
||||||
|
security:
|
||||||
|
- api_key: []
|
||||||
|
parameters:
|
||||||
|
- name: query
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: author
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
matches:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/BookMetadata"
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
BookMetadata:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
subtitle:
|
||||||
|
type: string
|
||||||
|
author:
|
||||||
|
type: string
|
||||||
|
narrator:
|
||||||
|
type: string
|
||||||
|
publisher:
|
||||||
|
type: string
|
||||||
|
publishedYear:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
cover:
|
||||||
|
type: string
|
||||||
|
description: URL to the cover image
|
||||||
|
isbn:
|
||||||
|
type: string
|
||||||
|
format: isbn
|
||||||
|
asin:
|
||||||
|
type: string
|
||||||
|
format: asin
|
||||||
|
genres:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
series:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
series:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
sequence:
|
||||||
|
type: number
|
||||||
|
format: int64
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
duration:
|
||||||
|
type: number
|
||||||
|
format: int64
|
||||||
|
description: Duration in seconds
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
securitySchemes:
|
||||||
|
api_key:
|
||||||
|
type: apiKey
|
||||||
|
name: AUTHORIZATION
|
||||||
|
in: header
|
||||||
|
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.7.2",
|
"version": "2.8.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.7.2",
|
"version": "2.8.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.7.2",
|
"version": "2.8.0",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
269
server/Auth.js
269
server/Auth.js
@ -76,12 +76,16 @@ class Auth {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom req timeout see: https://github.com/panva/node-openid-client/blob/main/docs/README.md#customizing
|
||||||
|
OpenIDClient.custom.setHttpOptionsDefaults({ timeout: 10000 })
|
||||||
|
|
||||||
const openIdIssuerClient = new OpenIDClient.Issuer({
|
const openIdIssuerClient = new OpenIDClient.Issuer({
|
||||||
issuer: global.ServerSettings.authOpenIDIssuerURL,
|
issuer: global.ServerSettings.authOpenIDIssuerURL,
|
||||||
authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL,
|
authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL,
|
||||||
token_endpoint: global.ServerSettings.authOpenIDTokenURL,
|
token_endpoint: global.ServerSettings.authOpenIDTokenURL,
|
||||||
userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL,
|
userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL,
|
||||||
jwks_uri: global.ServerSettings.authOpenIDJwksURL
|
jwks_uri: global.ServerSettings.authOpenIDJwksURL,
|
||||||
|
end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL
|
||||||
}).Client
|
}).Client
|
||||||
const openIdClient = new openIdIssuerClient({
|
const openIdClient = new openIdIssuerClient({
|
||||||
client_id: global.ServerSettings.authOpenIDClientID,
|
client_id: global.ServerSettings.authOpenIDClientID,
|
||||||
@ -153,6 +157,9 @@ class Auth {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We also have to save the id_token for later (used for logout) because we cannot set cookies here
|
||||||
|
user.openid_id_token = tokenset.id_token
|
||||||
|
|
||||||
// permit login
|
// permit login
|
||||||
return done(null, user)
|
return done(null, user)
|
||||||
}))
|
}))
|
||||||
@ -183,49 +190,48 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the client's choice how the login callback should happen in temp cookies
|
* Returns if the given auth method is API based.
|
||||||
|
*
|
||||||
|
* @param {string} authMethod
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isAuthMethodAPIBased(authMethod) {
|
||||||
|
return ['api', 'openid-mobile'].includes(authMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the client's choice of login callback method in temporary cookies.
|
||||||
|
*
|
||||||
|
* The `authMethod` parameter specifies the authentication strategy and can have the following values:
|
||||||
|
* - 'local': Standard authentication,
|
||||||
|
* - 'api': Authentication for API use
|
||||||
|
* - 'openid': OpenID authentication directly over web
|
||||||
|
* - 'openid-mobile': OpenID authentication, but done via an mobile device
|
||||||
*
|
*
|
||||||
* @param {import('express').Request} req
|
* @param {import('express').Request} req
|
||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
|
* @param {string} authMethod - The authentication method, default is 'local'.
|
||||||
*/
|
*/
|
||||||
paramsToCookies(req, res) {
|
paramsToCookies(req, res, authMethod = 'local') {
|
||||||
// Set if isRest flag is set or if mobile oauth flow is used
|
const TWO_MINUTES = 120000 // 2 minutes in milliseconds
|
||||||
if (req.query.isRest?.toLowerCase() == 'true' || req.query.redirect_uri) {
|
|
||||||
// store the isRest flag to the is_rest cookie
|
|
||||||
res.cookie('is_rest', 'true', {
|
|
||||||
maxAge: 120000, // 2 min
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// no isRest-flag set -> set is_rest cookie to false
|
|
||||||
res.cookie('is_rest', 'false', {
|
|
||||||
maxAge: 120000, // 2 min
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// persist state if passed in
|
|
||||||
if (req.query.state) {
|
|
||||||
res.cookie('auth_state', req.query.state, {
|
|
||||||
maxAge: 120000, // 2 min
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const callback = req.query.redirect_uri || req.query.callback
|
const callback = req.query.redirect_uri || req.query.callback
|
||||||
|
|
||||||
// check if we are missing a callback parameter - we need one if isRest=false
|
// Additional handling for non-API based authMethod
|
||||||
|
if (!this.isAuthMethodAPIBased(authMethod)) {
|
||||||
|
// Store 'auth_state' if present in the request
|
||||||
|
if (req.query.state) {
|
||||||
|
res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, httpOnly: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and store the callback URL
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
res.status(400).send({
|
return res.status(400).send({ message: 'No callback parameter' })
|
||||||
message: 'No callback parameter'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// store the callback url to the auth_cb cookie
|
res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true })
|
||||||
res.cookie('auth_cb', callback, {
|
|
||||||
maxAge: 120000, // 2 min
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the authentication method for long
|
||||||
|
res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -239,7 +245,7 @@ class Auth {
|
|||||||
// get userLogin json (information about the user, server and the session)
|
// get userLogin json (information about the user, server and the session)
|
||||||
const data_json = await this.getUserLoginResponsePayload(req.user)
|
const data_json = await this.getUserLoginResponsePayload(req.user)
|
||||||
|
|
||||||
if (req.cookies.is_rest === 'true') {
|
if (this.isAuthMethodAPIBased(req.cookies.auth_method)) {
|
||||||
// REST request - send data
|
// REST request - send data
|
||||||
res.json(data_json)
|
res.json(data_json)
|
||||||
} else {
|
} else {
|
||||||
@ -269,109 +275,105 @@ class Auth {
|
|||||||
|
|
||||||
// openid strategy login route (this redirects to the configured openid login provider)
|
// openid strategy login route (this redirects to the configured openid login provider)
|
||||||
router.get('/auth/openid', (req, res, next) => {
|
router.get('/auth/openid', (req, res, next) => {
|
||||||
try {
|
|
||||||
// helper function from openid-client
|
|
||||||
function pick(object, ...paths) {
|
|
||||||
const obj = {}
|
|
||||||
for (const path of paths) {
|
|
||||||
if (object[path] !== undefined) {
|
|
||||||
obj[path] = object[path]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the OIDC client from the strategy
|
// Get the OIDC client from the strategy
|
||||||
// We need to call the client manually, because the strategy does not support forwarding the code challenge
|
// We need to call the client manually, because the strategy does not support forwarding the code challenge
|
||||||
// for API or mobile clients
|
// for API or mobile clients
|
||||||
const oidcStrategy = passport._strategy('openid-client')
|
const oidcStrategy = passport._strategy('openid-client')
|
||||||
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
|
|
||||||
|
|
||||||
let mobile_redirect_uri = null
|
|
||||||
|
|
||||||
// The client wishes a different redirect_uri
|
|
||||||
// We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect
|
|
||||||
// where we will handle the redirect to it
|
|
||||||
if (req.query.redirect_uri) {
|
|
||||||
// Check if the redirect_uri is in the whitelist
|
|
||||||
if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) ||
|
|
||||||
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
|
|
||||||
oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString()
|
|
||||||
mobile_redirect_uri = req.query.redirect_uri
|
|
||||||
} else {
|
|
||||||
Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`)
|
|
||||||
return res.status(400).send('Invalid redirect_uri')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
|
|
||||||
const client = oidcStrategy._client
|
const client = oidcStrategy._client
|
||||||
const sessionKey = oidcStrategy._key
|
const sessionKey = oidcStrategy._key
|
||||||
|
|
||||||
let code_challenge
|
try {
|
||||||
let code_challenge_method
|
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
|
||||||
|
const hostUrl = new URL(`${protocol}://${req.get('host')}`)
|
||||||
|
const isMobileFlow = req.query.response_type === 'code' || req.query.redirect_uri || req.query.code_challenge
|
||||||
|
|
||||||
// If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app)
|
// Only allow code flow (for mobile clients)
|
||||||
// The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow
|
if (req.query.response_type && req.query.response_type !== 'code') {
|
||||||
// and as such will not send a code challenge, we will generate then one
|
Logger.debug(`[Auth] OIDC Invalid response_type=${req.query.response_type}`)
|
||||||
if (req.query.code_challenge) {
|
return res.status(400).send('Invalid response_type, only code supported')
|
||||||
code_challenge = req.query.code_challenge
|
|
||||||
code_challenge_method = req.query.code_challenge_method || 'S256'
|
|
||||||
|
|
||||||
if (!['S256', 'plain'].includes(code_challenge_method)) {
|
|
||||||
return res.status(400).send('Invalid code_challenge_method')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a state on web flow or if no state supplied
|
||||||
|
const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state
|
||||||
|
|
||||||
|
// Redirect URL for the SSO provider
|
||||||
|
let redirectUri
|
||||||
|
if (isMobileFlow) {
|
||||||
|
// Mobile required redirect uri
|
||||||
|
// If it is in the whitelist, we will save into this.openIdAuthSession and set the redirect uri to /auth/openid/mobile-redirect
|
||||||
|
// where we will handle the redirect to it
|
||||||
|
if (!req.query.redirect_uri || !isValidRedirectUri(req.query.redirect_uri)) {
|
||||||
|
Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri}`)
|
||||||
|
return res.status(400).send('Invalid redirect_uri')
|
||||||
|
}
|
||||||
|
// We cannot save the supplied redirect_uri in the session, because it the mobile client uses browser instead of the API
|
||||||
|
// for the request to mobile-redirect and as such the session is not shared
|
||||||
|
this.openIdAuthSession.set(state, { mobile_redirect_uri: req.query.redirect_uri })
|
||||||
|
|
||||||
|
redirectUri = new URL('/auth/openid/mobile-redirect', hostUrl).toString()
|
||||||
} else {
|
} else {
|
||||||
// If no code_challenge is provided, assume a web application flow and generate one
|
redirectUri = new URL('/auth/openid/callback', hostUrl).toString()
|
||||||
const code_verifier = OpenIDClient.generators.codeVerifier()
|
|
||||||
code_challenge = OpenIDClient.generators.codeChallenge(code_verifier)
|
|
||||||
code_challenge_method = 'S256'
|
|
||||||
|
|
||||||
// Store the code_verifier in the session for later use in the token exchange
|
if (req.query.state) {
|
||||||
req.session[sessionKey] = { ...req.session[sessionKey], code_verifier }
|
Logger.debug(`[Auth] Invalid state - not allowed on web openid flow`)
|
||||||
|
return res.status(400).send('Invalid state, not allowed on web flow')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
oidcStrategy._params.redirect_uri = redirectUri
|
||||||
|
Logger.debug(`[Auth] OIDC redirect_uri=${redirectUri}`)
|
||||||
|
|
||||||
const params = {
|
let { code_challenge, code_challenge_method, code_verifier } = generatePkce(req, isMobileFlow)
|
||||||
state: OpenIDClient.generators.random(),
|
|
||||||
// Other params by the passport strategy
|
|
||||||
...oidcStrategy._params
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.nonce && params.response_type.includes('id_token')) {
|
|
||||||
params.nonce = OpenIDClient.generators.random()
|
|
||||||
}
|
|
||||||
|
|
||||||
req.session[sessionKey] = {
|
req.session[sessionKey] = {
|
||||||
...req.session[sessionKey],
|
...req.session[sessionKey],
|
||||||
...pick(params, 'nonce', 'state', 'max_age', 'response_type'),
|
state: state,
|
||||||
|
max_age: oidcStrategy._params.max_age,
|
||||||
|
response_type: 'code',
|
||||||
|
code_verifier: code_verifier, // not null if web flow
|
||||||
mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out
|
mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out
|
||||||
sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback
|
sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback
|
||||||
}
|
}
|
||||||
|
|
||||||
// We cannot save redirect_uri in the session, because it the mobile client uses browser instead of the API
|
|
||||||
// for the request to mobile-redirect and as such the session is not shared
|
|
||||||
this.openIdAuthSession.set(params.state, { mobile_redirect_uri: mobile_redirect_uri })
|
|
||||||
|
|
||||||
// Now get the URL to direct to
|
|
||||||
const authorizationUrl = client.authorizationUrl({
|
const authorizationUrl = client.authorizationUrl({
|
||||||
...params,
|
...oidcStrategy._params,
|
||||||
scope: 'openid profile email',
|
state: state,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
code_challenge,
|
code_challenge,
|
||||||
code_challenge_method
|
code_challenge_method
|
||||||
})
|
})
|
||||||
|
|
||||||
// params (isRest, callback) to a cookie that will be send to the client
|
this.paramsToCookies(req, res, isMobileFlow ? 'openid-mobile' : 'openid')
|
||||||
this.paramsToCookies(req, res)
|
|
||||||
|
|
||||||
// Redirect the user agent (browser) to the authorization URL
|
|
||||||
res.redirect(authorizationUrl)
|
res.redirect(authorizationUrl)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[Auth] Error in /auth/openid route: ${error}`)
|
Logger.error(`[Auth] Error in /auth/openid route: ${error}`)
|
||||||
res.status(500).send('Internal Server Error')
|
res.status(500).send('Internal Server Error')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generatePkce(req, isMobileFlow) {
|
||||||
|
if (isMobileFlow) {
|
||||||
|
if (!req.query.code_challenge) {
|
||||||
|
throw new Error('code_challenge required for mobile flow (PKCE)')
|
||||||
|
}
|
||||||
|
if (req.query.code_challenge_method && req.query.code_challenge_method !== 'S256') {
|
||||||
|
throw new Error('Only S256 code_challenge_method method supported')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code_challenge: req.query.code_challenge,
|
||||||
|
code_challenge_method: req.query.code_challenge_method || 'S256'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const code_verifier = OpenIDClient.generators.codeVerifier()
|
||||||
|
const code_challenge = OpenIDClient.generators.codeChallenge(code_verifier)
|
||||||
|
return { code_challenge, code_challenge_method: 'S256', code_verifier }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidRedirectUri(uri) {
|
||||||
|
// Check if the redirect_uri is in the whitelist
|
||||||
|
return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) ||
|
||||||
|
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// This will be the oauth2 callback route for mobile clients
|
// This will be the oauth2 callback route for mobile clients
|
||||||
@ -453,6 +455,12 @@ class Auth {
|
|||||||
if (loginError) {
|
if (loginError) {
|
||||||
return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`)
|
return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The id_token does not provide access to the user, but is used to identify the user to the SSO provider
|
||||||
|
// instead it containts a JWT with userinfo like user email, username, etc.
|
||||||
|
// the client will get to know it anyway in the logout url according to the oauth2 spec
|
||||||
|
// so it is safe to send it to the client, but we use strict settings
|
||||||
|
res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' })
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -520,9 +528,48 @@ class Auth {
|
|||||||
req.logout((err) => {
|
req.logout((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.sendStatus(500)
|
res.sendStatus(500)
|
||||||
|
} else {
|
||||||
|
const authMethod = req.cookies.auth_method
|
||||||
|
|
||||||
|
res.clearCookie('auth_method')
|
||||||
|
|
||||||
|
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 (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)
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -612,7 +659,7 @@ class Auth {
|
|||||||
* Checks if a username and password tuple is valid and the user active.
|
* Checks if a username and password tuple is valid and the user active.
|
||||||
* @param {string} username
|
* @param {string} username
|
||||||
* @param {string} password
|
* @param {string} password
|
||||||
* @param {function} done
|
* @param {Promise<function>} done
|
||||||
*/
|
*/
|
||||||
async localAuthCheckUserPw(username, password, done) {
|
async localAuthCheckUserPw(username, password, done) {
|
||||||
// Load the user given it's username
|
// Load the user given it's username
|
||||||
@ -654,7 +701,7 @@ class Auth {
|
|||||||
/**
|
/**
|
||||||
* Hashes a password with bcrypt.
|
* Hashes a password with bcrypt.
|
||||||
* @param {string} password
|
* @param {string} password
|
||||||
* @returns {string} hash
|
* @returns {Promise<string>} hash
|
||||||
*/
|
*/
|
||||||
hashPass(password) {
|
hashPass(password) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@ -688,8 +735,8 @@ class Auth {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} password
|
* @param {string} password
|
||||||
* @param {*} user
|
* @param {import('./models/User')} user
|
||||||
* @returns {boolean}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
comparePassword(password, user) {
|
comparePassword(password, user) {
|
||||||
if (user.type === 'root' && !password && !user.pash) return true
|
if (user.type === 'root' && !password && !user.pash) return true
|
||||||
|
@ -132,6 +132,11 @@ class Database {
|
|||||||
return this.models.playbackSession
|
return this.models.playbackSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {typeof import('./models/CustomMetadataProvider')} */
|
||||||
|
get customMetadataProviderModel() {
|
||||||
|
return this.models.customMetadataProvider
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if db file exists
|
* Check if db file exists
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@ -245,6 +250,7 @@ class Database {
|
|||||||
require('./models/Feed').init(this.sequelize)
|
require('./models/Feed').init(this.sequelize)
|
||||||
require('./models/FeedEpisode').init(this.sequelize)
|
require('./models/FeedEpisode').init(this.sequelize)
|
||||||
require('./models/Setting').init(this.sequelize)
|
require('./models/Setting').init(this.sequelize)
|
||||||
|
require('./models/CustomMetadataProvider').init(this.sequelize)
|
||||||
|
|
||||||
return this.sequelize.sync({ force, alter: false })
|
return this.sequelize.sync({ force, alter: false })
|
||||||
}
|
}
|
||||||
@ -413,10 +419,21 @@ class Database {
|
|||||||
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save metadata file and update library item
|
||||||
|
*
|
||||||
|
* @param {import('./objects/LibraryItem')} oldLibraryItem
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
async updateLibraryItem(oldLibraryItem) {
|
async updateLibraryItem(oldLibraryItem) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await oldLibraryItem.saveMetadata()
|
await oldLibraryItem.saveMetadata()
|
||||||
return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
const updated = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
||||||
|
// Clear library filter data cache
|
||||||
|
if (updated) {
|
||||||
|
delete this.libraryFilterData[oldLibraryItem.libraryId]
|
||||||
|
}
|
||||||
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeLibraryItem(libraryItemId) {
|
async removeLibraryItem(libraryItemId) {
|
||||||
|
@ -3,13 +3,17 @@ const { LogLevel } = require('./utils/constants')
|
|||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
/** @type {import('./managers/LogManager')} */
|
||||||
|
this.logManager = null
|
||||||
|
|
||||||
this.isDev = process.env.NODE_ENV !== 'production'
|
this.isDev = process.env.NODE_ENV !== 'production'
|
||||||
this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE
|
this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE
|
||||||
this.socketListeners = []
|
this.socketListeners = []
|
||||||
|
|
||||||
this.logManager = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
get timestamp() {
|
get timestamp() {
|
||||||
return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS')
|
return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
}
|
}
|
||||||
@ -23,6 +27,9 @@ class Logger {
|
|||||||
return 'UNKNOWN'
|
return 'UNKNOWN'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
get source() {
|
get source() {
|
||||||
try {
|
try {
|
||||||
throw new Error()
|
throw new Error()
|
||||||
@ -62,24 +69,32 @@ class Logger {
|
|||||||
this.socketListeners = this.socketListeners.filter(s => s.id !== socketId)
|
this.socketListeners = this.socketListeners.filter(s => s.id !== socketId)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLog(level, args) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} level
|
||||||
|
* @param {string[]} args
|
||||||
|
* @param {string} src
|
||||||
|
*/
|
||||||
|
async handleLog(level, args, src) {
|
||||||
const logObj = {
|
const logObj = {
|
||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
source: this.source,
|
source: src,
|
||||||
message: args.join(' '),
|
message: args.join(' '),
|
||||||
levelName: this.getLogLevelString(level),
|
levelName: this.getLogLevelString(level),
|
||||||
level
|
level
|
||||||
}
|
}
|
||||||
|
|
||||||
if (level >= this.logLevel && this.logManager) {
|
// Emit log to sockets that are listening to log events
|
||||||
this.logManager.logToFile(logObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.socketListeners.forEach((socketListener) => {
|
this.socketListeners.forEach((socketListener) => {
|
||||||
if (socketListener.level <= level) {
|
if (socketListener.level <= level) {
|
||||||
socketListener.socket.emit('log', logObj)
|
socketListener.socket.emit('log', logObj)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Save log to file
|
||||||
|
if (level >= this.logLevel) {
|
||||||
|
await this.logManager.logToFile(logObj)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLogLevel(level) {
|
setLogLevel(level) {
|
||||||
@ -90,41 +105,47 @@ class Logger {
|
|||||||
trace(...args) {
|
trace(...args) {
|
||||||
if (this.logLevel > LogLevel.TRACE) return
|
if (this.logLevel > LogLevel.TRACE) return
|
||||||
console.trace(`[${this.timestamp}] TRACE:`, ...args)
|
console.trace(`[${this.timestamp}] TRACE:`, ...args)
|
||||||
this.handleLog(LogLevel.TRACE, args)
|
this.handleLog(LogLevel.TRACE, args, this.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(...args) {
|
debug(...args) {
|
||||||
if (this.logLevel > LogLevel.DEBUG) return
|
if (this.logLevel > LogLevel.DEBUG) return
|
||||||
console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`)
|
console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.DEBUG, args)
|
this.handleLog(LogLevel.DEBUG, args, this.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
info(...args) {
|
info(...args) {
|
||||||
if (this.logLevel > LogLevel.INFO) return
|
if (this.logLevel > LogLevel.INFO) return
|
||||||
console.info(`[${this.timestamp}] INFO:`, ...args)
|
console.info(`[${this.timestamp}] INFO:`, ...args)
|
||||||
this.handleLog(LogLevel.INFO, args)
|
this.handleLog(LogLevel.INFO, args, this.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(...args) {
|
warn(...args) {
|
||||||
if (this.logLevel > LogLevel.WARN) return
|
if (this.logLevel > LogLevel.WARN) return
|
||||||
console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`)
|
console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.WARN, args)
|
this.handleLog(LogLevel.WARN, args, this.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
error(...args) {
|
error(...args) {
|
||||||
if (this.logLevel > LogLevel.ERROR) return
|
if (this.logLevel > LogLevel.ERROR) return
|
||||||
console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`)
|
console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.ERROR, args)
|
this.handleLog(LogLevel.ERROR, args, this.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fatal errors are ones that exit the process
|
||||||
|
* Fatal logs are saved to crash_logs.txt
|
||||||
|
*
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
fatal(...args) {
|
fatal(...args) {
|
||||||
console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`)
|
console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.FATAL, args)
|
return this.handleLog(LogLevel.FATAL, args, this.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
note(...args) {
|
note(...args) {
|
||||||
console.log(`[${this.timestamp}] NOTE:`, ...args)
|
console.log(`[${this.timestamp}] NOTE:`, ...args)
|
||||||
this.handleLog(LogLevel.NOTE, args)
|
this.handleLog(LogLevel.NOTE, args, this.source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new Logger()
|
module.exports = new Logger()
|
@ -2,9 +2,9 @@ const Path = require('path')
|
|||||||
const Sequelize = require('sequelize')
|
const Sequelize = require('sequelize')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
|
const util = require('util')
|
||||||
const fs = require('./libs/fsExtra')
|
const fs = require('./libs/fsExtra')
|
||||||
const fileUpload = require('./libs/expressFileupload')
|
const fileUpload = require('./libs/expressFileupload')
|
||||||
const rateLimit = require('./libs/expressRateLimit')
|
|
||||||
const cookieParser = require("cookie-parser")
|
const cookieParser = require("cookie-parser")
|
||||||
|
|
||||||
const { version } = require('../package.json')
|
const { version } = require('../package.json')
|
||||||
@ -21,11 +21,11 @@ const SocketAuthority = require('./SocketAuthority')
|
|||||||
const ApiRouter = require('./routers/ApiRouter')
|
const ApiRouter = require('./routers/ApiRouter')
|
||||||
const HlsRouter = require('./routers/HlsRouter')
|
const HlsRouter = require('./routers/HlsRouter')
|
||||||
|
|
||||||
|
const LogManager = require('./managers/LogManager')
|
||||||
const NotificationManager = require('./managers/NotificationManager')
|
const NotificationManager = require('./managers/NotificationManager')
|
||||||
const EmailManager = require('./managers/EmailManager')
|
const EmailManager = require('./managers/EmailManager')
|
||||||
const AbMergeManager = require('./managers/AbMergeManager')
|
const AbMergeManager = require('./managers/AbMergeManager')
|
||||||
const CacheManager = require('./managers/CacheManager')
|
const CacheManager = require('./managers/CacheManager')
|
||||||
const LogManager = require('./managers/LogManager')
|
|
||||||
const BackupManager = require('./managers/BackupManager')
|
const BackupManager = require('./managers/BackupManager')
|
||||||
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
||||||
const PodcastManager = require('./managers/PodcastManager')
|
const PodcastManager = require('./managers/PodcastManager')
|
||||||
@ -67,7 +67,6 @@ class Server {
|
|||||||
this.notificationManager = new NotificationManager()
|
this.notificationManager = new NotificationManager()
|
||||||
this.emailManager = new EmailManager()
|
this.emailManager = new EmailManager()
|
||||||
this.backupManager = new BackupManager()
|
this.backupManager = new BackupManager()
|
||||||
this.logManager = new LogManager()
|
|
||||||
this.abMergeManager = new AbMergeManager()
|
this.abMergeManager = new AbMergeManager()
|
||||||
this.playbackSessionManager = new PlaybackSessionManager()
|
this.playbackSessionManager = new PlaybackSessionManager()
|
||||||
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
|
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
|
||||||
@ -81,7 +80,7 @@ class Server {
|
|||||||
this.apiRouter = new ApiRouter(this)
|
this.apiRouter = new ApiRouter(this)
|
||||||
this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager)
|
this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager)
|
||||||
|
|
||||||
Logger.logManager = this.logManager
|
Logger.logManager = new LogManager()
|
||||||
|
|
||||||
this.server = null
|
this.server = null
|
||||||
this.io = null
|
this.io = null
|
||||||
@ -102,10 +101,13 @@ class Server {
|
|||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
Logger.info('[Server] Init v' + version)
|
Logger.info('[Server] Init v' + version)
|
||||||
|
|
||||||
await this.playbackSessionManager.removeOrphanStreams()
|
await this.playbackSessionManager.removeOrphanStreams()
|
||||||
|
|
||||||
await Database.init(false)
|
await Database.init(false)
|
||||||
|
|
||||||
|
await Logger.logManager.init()
|
||||||
|
|
||||||
// Create token secret if does not exist (Added v2.1.0)
|
// Create token secret if does not exist (Added v2.1.0)
|
||||||
if (!Database.serverSettings.tokenSecret) {
|
if (!Database.serverSettings.tokenSecret) {
|
||||||
await this.auth.initTokenSecret()
|
await this.auth.initTokenSecret()
|
||||||
@ -115,7 +117,6 @@ class Server {
|
|||||||
await CacheManager.ensureCachePaths()
|
await CacheManager.ensureCachePaths()
|
||||||
|
|
||||||
await this.backupManager.init()
|
await this.backupManager.init()
|
||||||
await this.logManager.init()
|
|
||||||
await this.rssFeedManager.init()
|
await this.rssFeedManager.init()
|
||||||
|
|
||||||
const libraries = await Database.libraryModel.getAllOldLibraries()
|
const libraries = await Database.libraryModel.getAllOldLibraries()
|
||||||
@ -135,8 +136,41 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for SIGINT and uncaught exceptions
|
||||||
|
*/
|
||||||
|
initProcessEventListeners() {
|
||||||
|
let sigintAlreadyReceived = false
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
if (!sigintAlreadyReceived) {
|
||||||
|
sigintAlreadyReceived = true
|
||||||
|
Logger.info('SIGINT (Ctrl+C) received. Shutting down...')
|
||||||
|
await this.stop()
|
||||||
|
Logger.info('Server stopped. Exiting.')
|
||||||
|
} else {
|
||||||
|
Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.')
|
||||||
|
}
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://nodejs.org/api/process.html#event-uncaughtexceptionmonitor
|
||||||
|
*/
|
||||||
|
process.on('uncaughtExceptionMonitor', async (error, origin) => {
|
||||||
|
await Logger.fatal(`[Server] Uncaught exception origin: ${origin}, error:`, util.format('%O', error))
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* @see https://nodejs.org/api/process.html#event-unhandledrejection
|
||||||
|
*/
|
||||||
|
process.on('unhandledRejection', async (reason, promise) => {
|
||||||
|
await Logger.fatal(`[Server] Unhandled rejection: ${reason}, promise:`, util.format('%O', promise))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
Logger.info('=== Starting Server ===')
|
Logger.info('=== Starting Server ===')
|
||||||
|
this.initProcessEventListeners()
|
||||||
await this.init()
|
await this.init()
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
@ -252,8 +286,6 @@ class Server {
|
|||||||
]
|
]
|
||||||
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
||||||
|
|
||||||
// router.post('/login', passport.authenticate('local', this.auth.login), this.auth.loginResult.bind(this))
|
|
||||||
// router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
|
||||||
router.post('/init', (req, res) => {
|
router.post('/init', (req, res) => {
|
||||||
if (Database.hasRootUser) {
|
if (Database.hasRootUser) {
|
||||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
||||||
@ -284,19 +316,6 @@ class Server {
|
|||||||
})
|
})
|
||||||
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
||||||
|
|
||||||
let sigintAlreadyReceived = false
|
|
||||||
process.on('SIGINT', async () => {
|
|
||||||
if (!sigintAlreadyReceived) {
|
|
||||||
sigintAlreadyReceived = true
|
|
||||||
Logger.info('SIGINT (Ctrl+C) received. Shutting down...')
|
|
||||||
await this.stop()
|
|
||||||
Logger.info('Server stopped. Exiting.')
|
|
||||||
} else {
|
|
||||||
Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.')
|
|
||||||
}
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.server.listen(this.Port, this.Host, () => {
|
this.server.listen(this.Port, this.Host, () => {
|
||||||
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
||||||
else Logger.info(`Listening on port :${this.Port}`)
|
else Logger.info(`Listening on port :${this.Port}`)
|
||||||
@ -379,30 +398,6 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First time login rate limit is hit
|
|
||||||
loginLimitReached(req, res, options) {
|
|
||||||
Logger.error(`[Server] Login rate limit (${options.max}) was hit for ip ${req.ip}`)
|
|
||||||
options.message = 'Too many attempts. Login temporarily locked.'
|
|
||||||
}
|
|
||||||
|
|
||||||
getLoginRateLimiter() {
|
|
||||||
return rateLimit({
|
|
||||||
windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
|
|
||||||
max: Database.serverSettings.rateLimitLoginRequests,
|
|
||||||
skipSuccessfulRequests: true,
|
|
||||||
onLimitReached: this.loginLimitReached
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(req, res) {
|
|
||||||
if (req.body.socketId) {
|
|
||||||
Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${req.body.socketId}`)
|
|
||||||
SocketAuthority.logout(req.body.socketId)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.sendStatus(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gracefully stop server
|
* Gracefully stop server
|
||||||
* Stops watcher and socket server
|
* Stops watcher and socket server
|
||||||
|
@ -116,7 +116,6 @@ class SocketAuthority {
|
|||||||
// Logs
|
// Logs
|
||||||
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
||||||
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
|
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
|
||||||
socket.on('fetch_daily_logs', () => this.Server.logManager.socketRequestDailyLogs(socket))
|
|
||||||
|
|
||||||
// Sent automatically from socket.io clients
|
// Sent automatically from socket.io clients
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
@ -220,25 +219,6 @@ class SocketAuthority {
|
|||||||
client.socket.emit('init', initialPayload)
|
client.socket.emit('init', initialPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(socketId) {
|
|
||||||
// Strip user and client from client and client socket
|
|
||||||
if (socketId && this.clients[socketId]) {
|
|
||||||
const client = this.clients[socketId]
|
|
||||||
const clientSocket = client.socket
|
|
||||||
Logger.debug(`[SocketAuthority] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`)
|
|
||||||
|
|
||||||
if (client.user) {
|
|
||||||
Logger.debug('[SocketAuthority] User Offline ' + client.user.username)
|
|
||||||
this.adminEmitter('user_offline', client.user.toJSONForPublic())
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.clients[socketId].user
|
|
||||||
if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
|
|
||||||
} else if (socketId) {
|
|
||||||
Logger.warn(`[SocketAuthority] No client for socket ${socketId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelScan(id) {
|
cancelScan(id) {
|
||||||
Logger.debug('[SocketAuthority] Cancel scan', id)
|
Logger.debug('[SocketAuthority] Cancel scan', id)
|
||||||
this.Server.cancelLibraryScan(id)
|
this.Server.cancelLibraryScan(id)
|
||||||
|
117
server/controllers/CustomMetadataProviderController.js
Normal file
117
server/controllers/CustomMetadataProviderController.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
|
const { validateUrl } = require('../utils/index')
|
||||||
|
|
||||||
|
//
|
||||||
|
// This is a controller for routes that don't have a home yet :(
|
||||||
|
//
|
||||||
|
class CustomMetadataProviderController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/custom-metadata-providers
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async getAll(req, res) {
|
||||||
|
const providers = await Database.customMetadataProviderModel.findAll()
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
providers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: /api/custom-metadata-providers
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async create(req, res) {
|
||||||
|
const { name, url, mediaType, authHeaderValue } = req.body
|
||||||
|
|
||||||
|
if (!name || !url || !mediaType) {
|
||||||
|
return res.status(400).send('Invalid request body')
|
||||||
|
}
|
||||||
|
|
||||||
|
const validUrl = validateUrl(url)
|
||||||
|
if (!validUrl) {
|
||||||
|
Logger.error(`[CustomMetadataProviderController] Invalid url "${url}"`)
|
||||||
|
return res.status(400).send('Invalid url')
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = await Database.customMetadataProviderModel.create({
|
||||||
|
name,
|
||||||
|
mediaType,
|
||||||
|
url,
|
||||||
|
authHeaderValue: !authHeaderValue ? null : authHeaderValue,
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Necessary to emit to all clients?
|
||||||
|
SocketAuthority.emitter('custom_metadata_provider_added', provider.toClientJson())
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
provider
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE: /api/custom-metadata-providers/:id
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async delete(req, res) {
|
||||||
|
const slug = `custom-${req.params.id}`
|
||||||
|
|
||||||
|
/** @type {import('../models/CustomMetadataProvider')} */
|
||||||
|
const provider = req.customMetadataProvider
|
||||||
|
const providerClientJson = provider.toClientJson()
|
||||||
|
|
||||||
|
const fallbackProvider = provider.mediaType === 'book' ? 'google' : 'itunes'
|
||||||
|
|
||||||
|
await provider.destroy()
|
||||||
|
|
||||||
|
// Libraries using this provider fallback to default provider
|
||||||
|
await Database.libraryModel.update({
|
||||||
|
provider: fallbackProvider
|
||||||
|
}, {
|
||||||
|
where: {
|
||||||
|
provider: slug
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Necessary to emit to all clients?
|
||||||
|
SocketAuthority.emitter('custom_metadata_provider_removed', providerClientJson)
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware that requires admin or up
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
* @param {import('express').NextFunction} next
|
||||||
|
*/
|
||||||
|
async middleware(req, res, next) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.warn(`[CustomMetadataProviderController] Non-admin user "${req.user.username}" attempted access route "${req.path}"`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If id param then add req.customMetadataProvider
|
||||||
|
if (req.params.id) {
|
||||||
|
req.customMetadataProvider = await Database.customMetadataProviderModel.findByPk(req.params.id)
|
||||||
|
if (!req.customMetadataProvider) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new CustomMetadataProviderController()
|
@ -33,6 +33,14 @@ class LibraryController {
|
|||||||
return res.status(500).send('Invalid request')
|
return res.status(500).send('Invalid request')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that the custom provider exists if given any
|
||||||
|
if (newLibraryPayload.provider?.startsWith('custom-')) {
|
||||||
|
if (!await Database.customMetadataProviderModel.checkExistsBySlug(newLibraryPayload.provider)) {
|
||||||
|
Logger.error(`[LibraryController] Custom metadata provider "${newLibraryPayload.provider}" does not exist`)
|
||||||
|
return res.status(400).send('Custom metadata provider does not exist')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate folder paths exist or can be created & resolve rel paths
|
// Validate folder paths exist or can be created & resolve rel paths
|
||||||
// returns 400 if a folder fails to access
|
// returns 400 if a folder fails to access
|
||||||
newLibraryPayload.folders = newLibraryPayload.folders.map(f => {
|
newLibraryPayload.folders = newLibraryPayload.folders.map(f => {
|
||||||
@ -86,19 +94,27 @@ class LibraryController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/libraries/:id
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const includeArray = (req.query.include || '').split(',')
|
const includeArray = (req.query.include || '').split(',')
|
||||||
if (includeArray.includes('filterdata')) {
|
if (includeArray.includes('filterdata')) {
|
||||||
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
|
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
|
||||||
|
const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
filterdata,
|
filterdata,
|
||||||
issues: filterdata.numIssues,
|
issues: filterdata.numIssues,
|
||||||
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
||||||
|
customMetadataProviders,
|
||||||
library: req.library
|
library: req.library
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return res.json(req.library)
|
res.json(req.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,6 +131,14 @@ class LibraryController {
|
|||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const library = req.library
|
const library = req.library
|
||||||
|
|
||||||
|
// Validate that the custom provider exists if given any
|
||||||
|
if (req.body.provider?.startsWith('custom-')) {
|
||||||
|
if (!await Database.customMetadataProviderModel.checkExistsBySlug(req.body.provider)) {
|
||||||
|
Logger.error(`[LibraryController] Custom metadata provider "${req.body.provider}" does not exist`)
|
||||||
|
return res.status(400).send('Custom metadata provider does not exist')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate new folder paths exist or can be created & resolve rel paths
|
// Validate new folder paths exist or can be created & resolve rel paths
|
||||||
// returns 400 if a new folder fails to access
|
// returns 400 if a new folder fails to access
|
||||||
if (req.body.folders) {
|
if (req.body.folders) {
|
||||||
|
@ -124,6 +124,11 @@ class LibraryItemController {
|
|||||||
const libraryItem = req.libraryItem
|
const libraryItem = req.libraryItem
|
||||||
const mediaPayload = req.body
|
const mediaPayload = req.body
|
||||||
|
|
||||||
|
if (mediaPayload.url) {
|
||||||
|
await LibraryItemController.prototype.uploadCover.bind(this)(req, res, false)
|
||||||
|
if (res.writableEnded) return
|
||||||
|
}
|
||||||
|
|
||||||
// Book specific
|
// Book specific
|
||||||
if (libraryItem.isBook) {
|
if (libraryItem.isBook) {
|
||||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
||||||
@ -146,7 +151,7 @@ class LibraryItemController {
|
|||||||
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
|
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdates = libraryItem.media.update(mediaPayload)
|
const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
|
|
||||||
@ -171,7 +176,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/items/:id/cover
|
// POST: api/items/:id/cover
|
||||||
async uploadCover(req, res) {
|
async uploadCover(req, res, updateAndReturnJson = true) {
|
||||||
if (!req.user.canUpload) {
|
if (!req.user.canUpload) {
|
||||||
Logger.warn('User attempted to upload a cover without permission', req.user)
|
Logger.warn('User attempted to upload a cover without permission', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
@ -196,6 +201,7 @@ class LibraryItemController {
|
|||||||
return res.status(500).send('Unknown error occurred')
|
return res.status(500).send('Unknown error occurred')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateAndReturnJson) {
|
||||||
await Database.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
res.json({
|
res.json({
|
||||||
@ -203,6 +209,7 @@ class LibraryItemController {
|
|||||||
cover: result.cover
|
cover: result.cover
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PATCH: api/items/:id/cover
|
// PATCH: api/items/:id/cover
|
||||||
async updateCover(req, res) {
|
async updateCover(req, res) {
|
||||||
@ -276,6 +283,9 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.query.ts)
|
||||||
|
res.set('Cache-Control', 'private, max-age=86400')
|
||||||
|
|
||||||
if (raw) { // any value
|
if (raw) { // any value
|
||||||
if (global.XAccel) {
|
if (global.XAccel) {
|
||||||
const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath)
|
const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath)
|
||||||
|
@ -336,7 +336,7 @@ class MeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: /api/stats/year/:year
|
* GET: /api/me/stats/year/:year
|
||||||
*
|
*
|
||||||
* @param {import('express').Request} req
|
* @param {import('express').Request} req
|
||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
|
@ -633,7 +633,7 @@ class MiscController {
|
|||||||
} else if (key === 'authOpenIDMobileRedirectURIs') {
|
} else if (key === 'authOpenIDMobileRedirectURIs') {
|
||||||
function isValidRedirectURI(uri) {
|
function isValidRedirectURI(uri) {
|
||||||
if (typeof uri !== 'string') return false
|
if (typeof uri !== 'string') return false
|
||||||
const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i')
|
const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i')
|
||||||
return pattern.test(uri)
|
return pattern.test(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -699,7 +699,7 @@ class MiscController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: /api/me/stats/year/:year
|
* GET: /api/stats/year/:year
|
||||||
*
|
*
|
||||||
* @param {import('express').Request} req
|
* @param {import('express').Request} req
|
||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
@ -717,5 +717,23 @@ class MiscController {
|
|||||||
const stats = await adminStats.getStatsForYear(year)
|
const stats = await adminStats.getStatsForYear(year)
|
||||||
res.json(stats)
|
res.json(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/logger-data
|
||||||
|
* admin or up
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async getLoggerData(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get logger data`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
currentDailyLogs: Logger.logManager.getMostRecentCurrentDailyLogs()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new MiscController()
|
module.exports = new MiscController()
|
||||||
|
@ -43,12 +43,15 @@ class SearchController {
|
|||||||
*/
|
*/
|
||||||
async findPodcasts(req, res) {
|
async findPodcasts(req, res) {
|
||||||
const term = req.query.term
|
const term = req.query.term
|
||||||
|
const country = req.query.country || 'us'
|
||||||
if (!term) {
|
if (!term) {
|
||||||
Logger.error('[SearchController] Invalid request query param "term" is required')
|
Logger.error('[SearchController] Invalid request query param "term" is required')
|
||||||
return res.status(400).send('Invalid request query param "term" is required')
|
return res.status(400).send('Invalid request query param "term" is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await PodcastFinder.search(term)
|
const results = await PodcastFinder.search(term, {
|
||||||
|
country
|
||||||
|
})
|
||||||
res.json(results)
|
res.json(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ class SessionController {
|
|||||||
* @typedef batchDeleteReqBody
|
* @typedef batchDeleteReqBody
|
||||||
* @property {string[]} sessions
|
* @property {string[]} sessions
|
||||||
*
|
*
|
||||||
* @param {import('express').Request<{}, {}, batchDeleteReqBody, {}} req
|
* @param {import('express').Request<{}, {}, batchDeleteReqBody, {}>} req
|
||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
*/
|
*/
|
||||||
async batchDelete(req, res) {
|
async batchDelete(req, res) {
|
||||||
|
@ -194,6 +194,23 @@ class UserController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH: /api/users/:id/openid-unlink
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async unlinkFromOpenID(req, res) {
|
||||||
|
Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`)
|
||||||
|
req.reqUser.authOpenIDSub = null
|
||||||
|
if (await Database.userModel.updateFromOld(req.reqUser)) {
|
||||||
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toJSONForBrowser())
|
||||||
|
res.sendStatus(200)
|
||||||
|
} else {
|
||||||
|
res.sendStatus(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET: api/users/:id/listening-sessions
|
// GET: api/users/:id/listening-sessions
|
||||||
async getListeningSessions(req, res) {
|
async getListeningSessions(req, res) {
|
||||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
||||||
|
@ -15,12 +15,19 @@ class AuthorFinder {
|
|||||||
return this.audnexus.findAuthorByASIN(asin, region)
|
return this.audnexus.findAuthorByASIN(asin, region)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} region
|
||||||
|
* @param {Object} [options={}]
|
||||||
|
* @returns {Promise<import('../providers/Audnexus').AuthorSearchObj>}
|
||||||
|
*/
|
||||||
async findAuthorByName(name, region, options = {}) {
|
async findAuthorByName(name, region, options = {}) {
|
||||||
if (!name) return null
|
if (!name) return null
|
||||||
const maxLevenshtein = !isNaN(options.maxLevenshtein) ? Number(options.maxLevenshtein) : 3
|
const maxLevenshtein = !isNaN(options.maxLevenshtein) ? Number(options.maxLevenshtein) : 3
|
||||||
|
|
||||||
const author = await this.audnexus.findAuthorByName(name, region, maxLevenshtein)
|
const author = await this.audnexus.findAuthorByName(name, region, maxLevenshtein)
|
||||||
if (!author || !author.name) {
|
if (!author?.name) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return author
|
return author
|
||||||
|
@ -5,6 +5,7 @@ const iTunes = require('../providers/iTunes')
|
|||||||
const Audnexus = require('../providers/Audnexus')
|
const Audnexus = require('../providers/Audnexus')
|
||||||
const FantLab = require('../providers/FantLab')
|
const FantLab = require('../providers/FantLab')
|
||||||
const AudiobookCovers = require('../providers/AudiobookCovers')
|
const AudiobookCovers = require('../providers/AudiobookCovers')
|
||||||
|
const CustomProviderAdapter = require('../providers/CustomProviderAdapter')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ class BookFinder {
|
|||||||
this.audnexus = new Audnexus()
|
this.audnexus = new Audnexus()
|
||||||
this.fantLab = new FantLab()
|
this.fantLab = new FantLab()
|
||||||
this.audiobookCovers = new AudiobookCovers()
|
this.audiobookCovers = new AudiobookCovers()
|
||||||
|
this.customProviderAdapter = new CustomProviderAdapter()
|
||||||
|
|
||||||
this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audible', 'audible.ca', 'audible.uk', 'audible.au', 'audible.fr', 'audible.de', 'audible.jp', 'audible.it', 'audible.in', 'audible.es']
|
this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audible', 'audible.ca', 'audible.uk', 'audible.au', 'audible.fr', 'audible.de', 'audible.jp', 'audible.it', 'audible.in', 'audible.es']
|
||||||
|
|
||||||
@ -147,6 +149,20 @@ class BookFinder {
|
|||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} providerSlug
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async getCustomProviderResults(title, author, providerSlug) {
|
||||||
|
const books = await this.customProviderAdapter.search(title, author, providerSlug, 'book')
|
||||||
|
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
|
||||||
|
|
||||||
|
return books
|
||||||
|
}
|
||||||
|
|
||||||
static TitleCandidates = class {
|
static TitleCandidates = class {
|
||||||
|
|
||||||
constructor(cleanAuthor) {
|
constructor(cleanAuthor) {
|
||||||
@ -315,6 +331,11 @@ class BookFinder {
|
|||||||
const maxFuzzySearches = !isNaN(options.maxFuzzySearches) ? Number(options.maxFuzzySearches) : 5
|
const maxFuzzySearches = !isNaN(options.maxFuzzySearches) ? Number(options.maxFuzzySearches) : 5
|
||||||
let numFuzzySearches = 0
|
let numFuzzySearches = 0
|
||||||
|
|
||||||
|
// Custom providers are assumed to be correct
|
||||||
|
if (provider.startsWith('custom-')) {
|
||||||
|
return this.getCustomProviderResults(title, author, provider)
|
||||||
|
}
|
||||||
|
|
||||||
if (!title)
|
if (!title)
|
||||||
return books
|
return books
|
||||||
|
|
||||||
@ -397,8 +418,7 @@ class BookFinder {
|
|||||||
books = await this.getFantLabResults(title, author)
|
books = await this.getFantLabResults(title, author)
|
||||||
} else if (provider === 'audiobookcovers') {
|
} else if (provider === 'audiobookcovers') {
|
||||||
books = await this.getAudiobookCoversResults(title)
|
books = await this.getAudiobookCoversResults(title)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
books = await this.getGoogleBooksResults(title, author)
|
books = await this.getGoogleBooksResults(title, author)
|
||||||
}
|
}
|
||||||
return books
|
return books
|
||||||
|
@ -6,10 +6,16 @@ class PodcastFinder {
|
|||||||
this.iTunesApi = new iTunes()
|
this.iTunesApi = new iTunes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} term
|
||||||
|
* @param {{country:string}} options
|
||||||
|
* @returns {Promise<import('../providers/iTunes').iTunesPodcastSearchResult[]>}
|
||||||
|
*/
|
||||||
async search(term, options = {}) {
|
async search(term, options = {}) {
|
||||||
if (!term) return null
|
if (!term) return null
|
||||||
Logger.debug(`[iTunes] Searching for podcast with term "${term}"`)
|
Logger.debug(`[iTunes] Searching for podcast with term "${term}"`)
|
||||||
var results = await this.iTunesApi.searchPodcasts(term, options)
|
const results = await this.iTunesApi.searchPodcasts(term, options)
|
||||||
Logger.debug(`[iTunes] Podcast search for "${term}" returned ${results.length} results`)
|
Logger.debug(`[iTunes] Podcast search for "${term}" returned ${results.length} results`)
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# MIT License
|
|
||||||
|
|
||||||
Copyright 2021 Nathan Friedly
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,196 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
//
|
|
||||||
// modified for use in audiobookshelf
|
|
||||||
// Source: https://github.com/nfriedly/express-rate-limit
|
|
||||||
//
|
|
||||||
|
|
||||||
const MemoryStore = require("./memory-store");
|
|
||||||
|
|
||||||
function RateLimit(options) {
|
|
||||||
options = Object.assign(
|
|
||||||
{
|
|
||||||
windowMs: 60 * 1000, // milliseconds - how long to keep records of requests in memory
|
|
||||||
max: 5, // max number of recent connections during `window` milliseconds before sending a 429 response
|
|
||||||
message: "Too many requests, please try again later.",
|
|
||||||
statusCode: 429, // 429 status = Too Many Requests (RFC 6585)
|
|
||||||
headers: true, //Send custom rate limit header with limit and remaining
|
|
||||||
draft_polli_ratelimit_headers: false, //Support for the new RateLimit standardization headers
|
|
||||||
// ability to manually decide if request was successful. Used when `skipSuccessfulRequests` and/or `skipFailedRequests` are set to `true`
|
|
||||||
requestWasSuccessful: function (req, res) {
|
|
||||||
return res.statusCode < 400;
|
|
||||||
},
|
|
||||||
skipFailedRequests: false, // Do not count failed requests
|
|
||||||
skipSuccessfulRequests: false, // Do not count successful requests
|
|
||||||
// allows to create custom keys (by default user IP is used)
|
|
||||||
keyGenerator: function (req /*, res*/) {
|
|
||||||
if (!req.ip) {
|
|
||||||
console.error(
|
|
||||||
"express-rate-limit: req.ip is undefined - you can avoid this by providing a custom keyGenerator function, but it may be indicative of a larger issue."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return req.ip;
|
|
||||||
},
|
|
||||||
skip: function (/*req, res*/) {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
handler: function (req, res /*, next, optionsUsed*/) {
|
|
||||||
res.status(options.statusCode).send(options.message);
|
|
||||||
},
|
|
||||||
onLimitReached: function (/*req, res, optionsUsed*/) { },
|
|
||||||
requestPropertyName: "rateLimit", // Parameter name appended to req object
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
// store to use for persisting rate limit data
|
|
||||||
options.store = options.store || new MemoryStore(options.windowMs);
|
|
||||||
|
|
||||||
// ensure that the store has the incr method
|
|
||||||
if (
|
|
||||||
typeof options.store.incr !== "function" ||
|
|
||||||
typeof options.store.resetKey !== "function" ||
|
|
||||||
(options.skipFailedRequests &&
|
|
||||||
typeof options.store.decrement !== "function")
|
|
||||||
) {
|
|
||||||
throw new Error("The store is not valid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
["global", "delayMs", "delayAfter"].forEach((key) => {
|
|
||||||
// note: this doesn't trigger if delayMs or delayAfter are set to 0, because that essentially disables them
|
|
||||||
if (options[key]) {
|
|
||||||
throw new Error(
|
|
||||||
`The ${key} option was removed from express-rate-limit v3.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function rateLimit(req, res, next) {
|
|
||||||
Promise.resolve(options.skip(req, res))
|
|
||||||
.then((skip) => {
|
|
||||||
if (skip) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = options.keyGenerator(req, res);
|
|
||||||
|
|
||||||
options.store.incr(key, function (err, current, resetTime) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxResult =
|
|
||||||
typeof options.max === "function"
|
|
||||||
? options.max(req, res)
|
|
||||||
: options.max;
|
|
||||||
|
|
||||||
Promise.resolve(maxResult)
|
|
||||||
.then((max) => {
|
|
||||||
req[options.requestPropertyName] = {
|
|
||||||
limit: max,
|
|
||||||
current: current,
|
|
||||||
remaining: Math.max(max - current, 0),
|
|
||||||
resetTime: resetTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.headers && !res.headersSent) {
|
|
||||||
res.setHeader("X-RateLimit-Limit", max);
|
|
||||||
res.setHeader(
|
|
||||||
"X-RateLimit-Remaining",
|
|
||||||
req[options.requestPropertyName].remaining
|
|
||||||
);
|
|
||||||
if (resetTime instanceof Date) {
|
|
||||||
// if we have a resetTime, also provide the current date to help avoid issues with incorrect clocks
|
|
||||||
res.setHeader("Date", new Date().toUTCString());
|
|
||||||
res.setHeader(
|
|
||||||
"X-RateLimit-Reset",
|
|
||||||
Math.ceil(resetTime.getTime() / 1000)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options.draft_polli_ratelimit_headers && !res.headersSent) {
|
|
||||||
res.setHeader("RateLimit-Limit", max);
|
|
||||||
res.setHeader(
|
|
||||||
"RateLimit-Remaining",
|
|
||||||
req[options.requestPropertyName].remaining
|
|
||||||
);
|
|
||||||
if (resetTime) {
|
|
||||||
const deltaSeconds = Math.ceil(
|
|
||||||
(resetTime.getTime() - Date.now()) / 1000
|
|
||||||
);
|
|
||||||
res.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
options.skipFailedRequests ||
|
|
||||||
options.skipSuccessfulRequests
|
|
||||||
) {
|
|
||||||
let decremented = false;
|
|
||||||
const decrementKey = () => {
|
|
||||||
if (!decremented) {
|
|
||||||
options.store.decrement(key);
|
|
||||||
decremented = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.skipFailedRequests) {
|
|
||||||
res.on("finish", function () {
|
|
||||||
if (!options.requestWasSuccessful(req, res)) {
|
|
||||||
decrementKey();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("close", () => {
|
|
||||||
if (!res.finished) {
|
|
||||||
decrementKey();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("error", () => decrementKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.skipSuccessfulRequests) {
|
|
||||||
res.on("finish", function () {
|
|
||||||
if (options.requestWasSuccessful(req, res)) {
|
|
||||||
options.store.decrement(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max && current === max + 1) {
|
|
||||||
options.onLimitReached(req, res, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max && current > max) {
|
|
||||||
if (options.headers && !res.headersSent) {
|
|
||||||
res.setHeader(
|
|
||||||
"Retry-After",
|
|
||||||
Math.ceil(options.windowMs / 1000)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return options.handler(req, res, next, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.catch(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.catch(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
rateLimit.resetKey = options.store.resetKey.bind(options.store);
|
|
||||||
|
|
||||||
// Backward compatibility function
|
|
||||||
rateLimit.resetIp = rateLimit.resetKey;
|
|
||||||
|
|
||||||
return rateLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = RateLimit;
|
|
@ -1,47 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function calculateNextResetTime(windowMs) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setMilliseconds(d.getMilliseconds() + windowMs);
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MemoryStore(windowMs) {
|
|
||||||
let hits = {};
|
|
||||||
let resetTime = calculateNextResetTime(windowMs);
|
|
||||||
|
|
||||||
this.incr = function (key, cb) {
|
|
||||||
if (hits[key]) {
|
|
||||||
hits[key]++;
|
|
||||||
} else {
|
|
||||||
hits[key] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, hits[key], resetTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.decrement = function (key) {
|
|
||||||
if (hits[key]) {
|
|
||||||
hits[key]--;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// export an API to allow hits all IPs to be reset
|
|
||||||
this.resetAll = function () {
|
|
||||||
hits = {};
|
|
||||||
resetTime = calculateNextResetTime(windowMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
// export an API to allow hits from one IP to be reset
|
|
||||||
this.resetKey = function (key) {
|
|
||||||
delete hits[key];
|
|
||||||
};
|
|
||||||
|
|
||||||
// simply reset ALL hits every windowMs
|
|
||||||
const interval = setInterval(this.resetAll, windowMs);
|
|
||||||
if (interval.unref) {
|
|
||||||
interval.unref();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = MemoryStore;
|
|
@ -1,3 +1,6 @@
|
|||||||
|
const child_process = require('child_process')
|
||||||
|
const { promisify } = require('util')
|
||||||
|
const exec = promisify(child_process.exec)
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const which = require('../libs/which')
|
const which = require('../libs/which')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
@ -8,67 +11,143 @@ const fileUtils = require('../utils/fileUtils')
|
|||||||
class BinaryManager {
|
class BinaryManager {
|
||||||
|
|
||||||
defaultRequiredBinaries = [
|
defaultRequiredBinaries = [
|
||||||
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' },
|
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1'] },
|
||||||
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH' }
|
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1'] }
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor(requiredBinaries = this.defaultRequiredBinaries) {
|
constructor(requiredBinaries = this.defaultRequiredBinaries) {
|
||||||
this.requiredBinaries = requiredBinaries
|
this.requiredBinaries = requiredBinaries
|
||||||
this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot
|
this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot
|
||||||
this.altInstallPath = global.ConfigPath
|
this.altInstallPath = global.ConfigPath
|
||||||
|
this.initialized = false
|
||||||
|
this.exec = exec
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (this.initialized) return
|
if (this.initialized) return
|
||||||
const missingBinaries = await this.findRequiredBinaries()
|
const missingBinaries = await this.findRequiredBinaries()
|
||||||
if (missingBinaries.length == 0) return
|
if (missingBinaries.length == 0) return
|
||||||
|
await this.removeOldBinaries(missingBinaries)
|
||||||
await this.install(missingBinaries)
|
await this.install(missingBinaries)
|
||||||
const missingBinariesAfterInstall = await this.findRequiredBinaries()
|
const missingBinariesAfterInstall = await this.findRequiredBinaries()
|
||||||
if (missingBinariesAfterInstall.length != 0) {
|
if (missingBinariesAfterInstall.length) {
|
||||||
Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`)
|
Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove old/invalid binaries in main or alt install path
|
||||||
|
*
|
||||||
|
* @param {string[]} binaryNames
|
||||||
|
*/
|
||||||
|
async removeOldBinaries(binaryNames) {
|
||||||
|
for (const binaryName of binaryNames) {
|
||||||
|
const executable = this.getExecutableFileName(binaryName)
|
||||||
|
const mainInstallPath = path.join(this.mainInstallPath, executable)
|
||||||
|
if (await fs.pathExists(mainInstallPath)) {
|
||||||
|
Logger.debug(`[BinaryManager] Removing old binary: ${mainInstallPath}`)
|
||||||
|
await fs.remove(mainInstallPath)
|
||||||
|
}
|
||||||
|
const altInstallPath = path.join(this.altInstallPath, executable)
|
||||||
|
if (await fs.pathExists(altInstallPath)) {
|
||||||
|
Logger.debug(`[BinaryManager] Removing old binary: ${altInstallPath}`)
|
||||||
|
await fs.remove(altInstallPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find required binaries and return array of binary names that are missing
|
||||||
|
*
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
async findRequiredBinaries() {
|
async findRequiredBinaries() {
|
||||||
const missingBinaries = []
|
const missingBinaries = []
|
||||||
for (const binary of this.requiredBinaries) {
|
for (const binary of this.requiredBinaries) {
|
||||||
const binaryPath = await this.findBinary(binary.name, binary.envVariable)
|
const binaryPath = await this.findBinary(binary.name, binary.envVariable, binary.validVersions)
|
||||||
if (binaryPath) {
|
if (binaryPath) {
|
||||||
Logger.info(`[BinaryManager] Found ${binary.name} at ${binaryPath}`)
|
Logger.info(`[BinaryManager] Found valid binary ${binary.name} at ${binaryPath}`)
|
||||||
if (process.env[binary.envVariable] !== binaryPath) {
|
if (process.env[binary.envVariable] !== binaryPath) {
|
||||||
Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`)
|
Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`)
|
||||||
process.env[binary.envVariable] = binaryPath
|
process.env[binary.envVariable] = binaryPath
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[BinaryManager] ${binary.name} not found`)
|
Logger.info(`[BinaryManager] ${binary.name} not found or version too old`)
|
||||||
missingBinaries.push(binary.name)
|
missingBinaries.push(binary.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return missingBinaries
|
return missingBinaries
|
||||||
}
|
}
|
||||||
|
|
||||||
async findBinary(name, envVariable) {
|
/**
|
||||||
const executable = name + (process.platform == 'win32' ? '.exe' : '')
|
* Find absolute path for binary
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} envVariable
|
||||||
|
* @param {string[]} [validVersions]
|
||||||
|
* @returns {Promise<string>} Path to binary
|
||||||
|
*/
|
||||||
|
async findBinary(name, envVariable, validVersions = []) {
|
||||||
|
const executable = this.getExecutableFileName(name)
|
||||||
|
// 1. check path specified in environment variable
|
||||||
const defaultPath = process.env[envVariable]
|
const defaultPath = process.env[envVariable]
|
||||||
if (defaultPath && await fs.pathExists(defaultPath)) return defaultPath
|
if (await this.isBinaryGood(defaultPath, validVersions)) return defaultPath
|
||||||
|
// 2. find the first instance of the binary in the PATH environment variable
|
||||||
const whichPath = which.sync(executable, { nothrow: true })
|
const whichPath = which.sync(executable, { nothrow: true })
|
||||||
if (whichPath) return whichPath
|
if (await this.isBinaryGood(whichPath, validVersions)) return whichPath
|
||||||
|
// 3. check main install path (binary root dir)
|
||||||
const mainInstallPath = path.join(this.mainInstallPath, executable)
|
const mainInstallPath = path.join(this.mainInstallPath, executable)
|
||||||
if (await fs.pathExists(mainInstallPath)) return mainInstallPath
|
if (await this.isBinaryGood(mainInstallPath, validVersions)) return mainInstallPath
|
||||||
|
// 4. check alt install path (/config)
|
||||||
const altInstallPath = path.join(this.altInstallPath, executable)
|
const altInstallPath = path.join(this.altInstallPath, executable)
|
||||||
if (await fs.pathExists(altInstallPath)) return altInstallPath
|
if (await this.isBinaryGood(altInstallPath, validVersions)) return altInstallPath
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check binary path exists and optionally check version is valid
|
||||||
|
*
|
||||||
|
* @param {string} binaryPath
|
||||||
|
* @param {string[]} [validVersions]
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async isBinaryGood(binaryPath, validVersions = []) {
|
||||||
|
if (!binaryPath || !await fs.pathExists(binaryPath)) return false
|
||||||
|
if (!validVersions.length) return true
|
||||||
|
try {
|
||||||
|
const { stdout } = await this.exec('"' + binaryPath + '"' + ' -version')
|
||||||
|
const version = stdout.match(/version\s([\d\.]+)/)?.[1]
|
||||||
|
if (!version) return false
|
||||||
|
return validVersions.some(validVersion => version.startsWith(validVersion))
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`[BinaryManager] Failed to check version of ${binaryPath}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string[]} binaries
|
||||||
|
*/
|
||||||
async install(binaries) {
|
async install(binaries) {
|
||||||
if (binaries.length == 0) return
|
if (!binaries.length) return
|
||||||
Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`)
|
Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`)
|
||||||
let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath
|
let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath
|
||||||
await ffbinaries.downloadBinaries(binaries, { destination })
|
await ffbinaries.downloadBinaries(binaries, { destination, version: '5.1', force: true })
|
||||||
Logger.info(`[BinaryManager] Binaries installed to ${destination}`)
|
Logger.info(`[BinaryManager] Binaries installed to ${destination}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append .exe to binary name for Windows
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getExecutableFileName(name) {
|
||||||
|
return name + (process.platform == 'win32' ? '.exe' : '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = BinaryManager
|
module.exports = BinaryManager
|
@ -1,19 +1,34 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
|
const Logger = require('../Logger')
|
||||||
const DailyLog = require('../objects/DailyLog')
|
const DailyLog = require('../objects/DailyLog')
|
||||||
|
|
||||||
const Logger = require('../Logger')
|
const { LogLevel } = require('../utils/constants')
|
||||||
|
|
||||||
const TAG = '[LogManager]'
|
const TAG = '[LogManager]'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef LogObject
|
||||||
|
* @property {string} timestamp
|
||||||
|
* @property {string} source
|
||||||
|
* @property {string} message
|
||||||
|
* @property {string} levelName
|
||||||
|
* @property {number} level
|
||||||
|
*/
|
||||||
|
|
||||||
class LogManager {
|
class LogManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
||||||
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
||||||
|
|
||||||
|
/** @type {DailyLog} */
|
||||||
this.currentDailyLog = null
|
this.currentDailyLog = null
|
||||||
|
|
||||||
|
/** @type {LogObject[]} */
|
||||||
this.dailyLogBuffer = []
|
this.dailyLogBuffer = []
|
||||||
|
|
||||||
|
/** @type {string[]} */
|
||||||
this.dailyLogFiles = []
|
this.dailyLogFiles = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,12 +41,12 @@ class LogManager {
|
|||||||
await fs.ensureDir(this.ScanLogPath)
|
await fs.ensureDir(this.ScanLogPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureScanLogDir() {
|
/**
|
||||||
if (!(await fs.pathExists(this.ScanLogPath))) {
|
* 1. Ensure log directories exist
|
||||||
await fs.mkdir(this.ScanLogPath)
|
* 2. Load daily log files
|
||||||
}
|
* 3. Remove old daily log files
|
||||||
}
|
* 4. Create/set current daily log file
|
||||||
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
await this.ensureLogDirs()
|
await this.ensureLogDirs()
|
||||||
|
|
||||||
@ -46,11 +61,11 @@ class LogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set current daily log file or create if does not exist
|
||||||
const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename()
|
const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename()
|
||||||
Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`)
|
Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`)
|
||||||
|
|
||||||
this.currentDailyLog = new DailyLog()
|
this.currentDailyLog = new DailyLog(this.DailyLogPath)
|
||||||
this.currentDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
|
|
||||||
|
|
||||||
if (this.dailyLogFiles.includes(currentDailyLogFilename)) {
|
if (this.dailyLogFiles.includes(currentDailyLogFilename)) {
|
||||||
Logger.debug(TAG, `Daily log file already exists - set in Logger`)
|
Logger.debug(TAG, `Daily log file already exists - set in Logger`)
|
||||||
@ -59,7 +74,7 @@ class LogManager {
|
|||||||
this.dailyLogFiles.push(this.currentDailyLog.filename)
|
this.dailyLogFiles.push(this.currentDailyLog.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log buffered Logs
|
// Log buffered daily logs
|
||||||
if (this.dailyLogBuffer.length) {
|
if (this.dailyLogBuffer.length) {
|
||||||
this.dailyLogBuffer.forEach((logObj) => {
|
this.dailyLogBuffer.forEach((logObj) => {
|
||||||
this.currentDailyLog.appendLog(logObj)
|
this.currentDailyLog.appendLog(logObj)
|
||||||
@ -68,9 +83,12 @@ class LogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all daily log filenames in /metadata/logs/daily
|
||||||
|
*/
|
||||||
async scanLogFiles() {
|
async scanLogFiles() {
|
||||||
const dailyFiles = await fs.readdir(this.DailyLogPath)
|
const dailyFiles = await fs.readdir(this.DailyLogPath)
|
||||||
if (dailyFiles && dailyFiles.length) {
|
if (dailyFiles?.length) {
|
||||||
dailyFiles.forEach((logFile) => {
|
dailyFiles.forEach((logFile) => {
|
||||||
if (Path.extname(logFile) === '.txt') {
|
if (Path.extname(logFile) === '.txt') {
|
||||||
Logger.debug('Daily Log file found', logFile)
|
Logger.debug('Daily Log file found', logFile)
|
||||||
@ -83,30 +101,38 @@ class LogManager {
|
|||||||
this.dailyLogFiles.sort()
|
this.dailyLogFiles.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeOldestLog() {
|
/**
|
||||||
if (!this.dailyLogFiles.length) return
|
*
|
||||||
const oldestLog = this.dailyLogFiles[0]
|
* @param {string} filename
|
||||||
return this.removeLogFile(oldestLog)
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
async removeLogFile(filename) {
|
async removeLogFile(filename) {
|
||||||
const fullPath = Path.join(this.DailyLogPath, filename)
|
const fullPath = Path.join(this.DailyLogPath, filename)
|
||||||
const exists = await fs.pathExists(fullPath)
|
const exists = await fs.pathExists(fullPath)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
Logger.error(TAG, 'Invalid log dne ' + fullPath)
|
Logger.error(TAG, 'Invalid log dne ' + fullPath)
|
||||||
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename)
|
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename)
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await fs.unlink(fullPath)
|
await fs.unlink(fullPath)
|
||||||
Logger.info(TAG, 'Removed daily log: ' + filename)
|
Logger.info(TAG, 'Removed daily log: ' + filename)
|
||||||
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename)
|
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(TAG, 'Failed to unlink log file ' + fullPath)
|
Logger.error(TAG, 'Failed to unlink log file ' + fullPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logToFile(logObj) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {LogObject} logObj
|
||||||
|
*/
|
||||||
|
async logToFile(logObj) {
|
||||||
|
// Fatal crashes get logged to a separate file
|
||||||
|
if (logObj.level === LogLevel.FATAL) {
|
||||||
|
await this.logCrashToFile(logObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer when logging before daily logs have been initialized
|
||||||
if (!this.currentDailyLog) {
|
if (!this.currentDailyLog) {
|
||||||
this.dailyLogBuffer.push(logObj)
|
this.dailyLogBuffer.push(logObj)
|
||||||
return
|
return
|
||||||
@ -114,25 +140,39 @@ class LogManager {
|
|||||||
|
|
||||||
// Check log rolls to next day
|
// Check log rolls to next day
|
||||||
if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) {
|
if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) {
|
||||||
const newDailyLog = new DailyLog()
|
this.currentDailyLog = new DailyLog(this.DailyLogPath)
|
||||||
newDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
|
|
||||||
this.currentDailyLog = newDailyLog
|
|
||||||
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
||||||
this.removeOldestLog()
|
// Remove oldest log
|
||||||
|
this.removeLogFile(this.dailyLogFiles[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append log line to log file
|
// Append log line to log file
|
||||||
this.currentDailyLog.appendLog(logObj)
|
return this.currentDailyLog.appendLog(logObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
socketRequestDailyLogs(socket) {
|
/**
|
||||||
if (!this.currentDailyLog) {
|
*
|
||||||
return
|
* @param {LogObject} logObj
|
||||||
|
*/
|
||||||
|
async logCrashToFile(logObj) {
|
||||||
|
const line = JSON.stringify(logObj) + '\n'
|
||||||
|
|
||||||
|
const logsDir = Path.join(global.MetadataPath, 'logs')
|
||||||
|
await fs.ensureDir(logsDir)
|
||||||
|
const crashLogPath = Path.join(logsDir, 'crash_logs.txt')
|
||||||
|
return fs.writeFile(crashLogPath, line, { flag: "a+" }).catch((error) => {
|
||||||
|
console.log('[LogManager] Appended crash log', error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastLogs = this.currentDailyLog.logs.slice(-5000)
|
/**
|
||||||
socket.emit('daily_logs', lastLogs)
|
* Most recent 5000 daily logs
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getMostRecentCurrentDailyLogs() {
|
||||||
|
return this.currentDailyLog?.logs.slice(-5000) || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = LogManager
|
module.exports = LogManager
|
103
server/models/CustomMetadataProvider.js
Normal file
103
server/models/CustomMetadataProvider.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ClientCustomMetadataProvider
|
||||||
|
* @property {UUIDV4} id
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} url
|
||||||
|
* @property {string} slug
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CustomMetadataProvider extends Model {
|
||||||
|
constructor(values, options) {
|
||||||
|
super(values, options)
|
||||||
|
|
||||||
|
/** @type {UUIDV4} */
|
||||||
|
this.id
|
||||||
|
/** @type {string} */
|
||||||
|
this.mediaType
|
||||||
|
/** @type {string} */
|
||||||
|
this.name
|
||||||
|
/** @type {string} */
|
||||||
|
this.url
|
||||||
|
/** @type {string} */
|
||||||
|
this.authHeaderValue
|
||||||
|
/** @type {Object} */
|
||||||
|
this.extraData
|
||||||
|
/** @type {Date} */
|
||||||
|
this.createdAt
|
||||||
|
/** @type {Date} */
|
||||||
|
this.updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
getSlug() {
|
||||||
|
return `custom-${this.id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe for clients
|
||||||
|
* @returns {ClientCustomMetadataProvider}
|
||||||
|
*/
|
||||||
|
toClientJson() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
slug: this.getSlug()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get providers for client by media type
|
||||||
|
* Currently only available for "book" media type
|
||||||
|
*
|
||||||
|
* @param {string} mediaType
|
||||||
|
* @returns {Promise<ClientCustomMetadataProvider[]>}
|
||||||
|
*/
|
||||||
|
static async getForClientByMediaType(mediaType) {
|
||||||
|
if (mediaType !== 'book') return []
|
||||||
|
const customMetadataProviders = await this.findAll({
|
||||||
|
where: {
|
||||||
|
mediaType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return customMetadataProviders.map(cmp => cmp.toClientJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if provider exists by slug
|
||||||
|
*
|
||||||
|
* @param {string} providerSlug
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
static async checkExistsBySlug(providerSlug) {
|
||||||
|
const providerId = providerSlug?.split?.('custom-')[1]
|
||||||
|
if (!providerId) return false
|
||||||
|
|
||||||
|
return (await this.count({ where: { id: providerId } })) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize model
|
||||||
|
* @param {import('../Database').sequelize} sequelize
|
||||||
|
*/
|
||||||
|
static init(sequelize) {
|
||||||
|
super.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
mediaType: DataTypes.STRING,
|
||||||
|
url: DataTypes.STRING,
|
||||||
|
authHeaderValue: DataTypes.STRING,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'customMetadataProvider'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CustomMetadataProvider
|
@ -225,6 +225,12 @@ class LibraryItem extends Model {
|
|||||||
return newLibraryItem
|
return newLibraryItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates libraryItem, book, authors and series from old library item
|
||||||
|
*
|
||||||
|
* @param {oldLibraryItem} oldLibraryItem
|
||||||
|
* @returns {Promise<boolean>} true if updates were made
|
||||||
|
*/
|
||||||
static async fullUpdateFromOld(oldLibraryItem) {
|
static async fullUpdateFromOld(oldLibraryItem) {
|
||||||
const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
|
const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
|
||||||
include: [
|
include: [
|
||||||
@ -306,17 +312,18 @@ class LibraryItem extends Model {
|
|||||||
const existingAuthors = libraryItemExpanded.media.authors || []
|
const existingAuthors = libraryItemExpanded.media.authors || []
|
||||||
const existingSeriesAll = libraryItemExpanded.media.series || []
|
const existingSeriesAll = libraryItemExpanded.media.series || []
|
||||||
const updatedAuthors = oldLibraryItem.media.metadata.authors || []
|
const updatedAuthors = oldLibraryItem.media.metadata.authors || []
|
||||||
|
const uniqueUpdatedAuthors = updatedAuthors.filter((au, idx) => updatedAuthors.findIndex(a => a.id === au.id) === idx)
|
||||||
const updatedSeriesAll = oldLibraryItem.media.metadata.series || []
|
const updatedSeriesAll = oldLibraryItem.media.metadata.series || []
|
||||||
|
|
||||||
for (const existingAuthor of existingAuthors) {
|
for (const existingAuthor of existingAuthors) {
|
||||||
// Author was removed from Book
|
// Author was removed from Book
|
||||||
if (!updatedAuthors.some(au => au.id === existingAuthor.id)) {
|
if (!uniqueUpdatedAuthors.some(au => au.id === existingAuthor.id)) {
|
||||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
|
||||||
await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
|
await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const updatedAuthor of updatedAuthors) {
|
for (const updatedAuthor of uniqueUpdatedAuthors) {
|
||||||
// Author was added
|
// Author was added
|
||||||
if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
|
if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
|
||||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
|
||||||
@ -372,6 +379,9 @@ class LibraryItem extends Model {
|
|||||||
if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) {
|
if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) {
|
||||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
|
||||||
hasLibraryItemUpdates = true
|
hasLibraryItemUpdates = true
|
||||||
|
if (key === 'updatedAt') {
|
||||||
|
libraryItemExpanded.changed('updatedAt', true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasLibraryItemUpdates) {
|
if (hasLibraryItemUpdates) {
|
||||||
@ -399,6 +409,7 @@ class LibraryItem extends Model {
|
|||||||
isInvalid: !!oldLibraryItem.isInvalid,
|
isInvalid: !!oldLibraryItem.isInvalid,
|
||||||
mtime: oldLibraryItem.mtimeMs,
|
mtime: oldLibraryItem.mtimeMs,
|
||||||
ctime: oldLibraryItem.ctimeMs,
|
ctime: oldLibraryItem.ctimeMs,
|
||||||
|
updatedAt: oldLibraryItem.updatedAt,
|
||||||
birthtime: oldLibraryItem.birthtimeMs,
|
birthtime: oldLibraryItem.birthtimeMs,
|
||||||
size: oldLibraryItem.size,
|
size: oldLibraryItem.size,
|
||||||
lastScan: oldLibraryItem.lastScan,
|
lastScan: oldLibraryItem.lastScan,
|
||||||
|
@ -118,7 +118,9 @@ class PlaybackSession extends Model {
|
|||||||
|
|
||||||
static createFromOld(oldPlaybackSession) {
|
static createFromOld(oldPlaybackSession) {
|
||||||
const playbackSession = this.getFromOld(oldPlaybackSession)
|
const playbackSession = this.getFromOld(oldPlaybackSession)
|
||||||
return this.create(playbackSession)
|
return this.create(playbackSession, {
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static updateFromOld(oldPlaybackSession) {
|
static updateFromOld(oldPlaybackSession) {
|
||||||
@ -126,7 +128,8 @@ class PlaybackSession extends Model {
|
|||||||
return this.update(playbackSession, {
|
return this.update(playbackSession, {
|
||||||
where: {
|
where: {
|
||||||
id: playbackSession.id
|
id: playbackSession.id
|
||||||
}
|
},
|
||||||
|
silent: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,28 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const date = require('../libs/dateAndTime')
|
const date = require('../libs/dateAndTime')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const { readTextFile } = require('../utils/fileUtils')
|
const fileUtils = require('../utils/fileUtils')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class DailyLog {
|
class DailyLog {
|
||||||
constructor() {
|
/**
|
||||||
this.id = null
|
*
|
||||||
this.datePretty = null
|
* @param {string} dailyLogDirPath Path to daily logs /metadata/logs/daily
|
||||||
|
*/
|
||||||
|
constructor(dailyLogDirPath) {
|
||||||
|
this.id = date.format(new Date(), 'YYYY-MM-DD')
|
||||||
|
|
||||||
this.dailyLogDirPath = null
|
this.dailyLogDirPath = dailyLogDirPath
|
||||||
this.filename = null
|
this.filename = this.id + '.txt'
|
||||||
this.path = null
|
this.fullPath = Path.join(this.dailyLogDirPath, this.filename)
|
||||||
this.fullPath = null
|
|
||||||
|
|
||||||
this.createdAt = null
|
this.createdAt = Date.now()
|
||||||
|
|
||||||
|
/** @type {import('../managers/LogManager').LogObject[]} */
|
||||||
this.logs = []
|
this.logs = []
|
||||||
|
/** @type {string[]} */
|
||||||
this.bufferedLogLines = []
|
this.bufferedLogLines = []
|
||||||
|
|
||||||
this.locked = false
|
this.locked = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +37,6 @@ class DailyLog {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
datePretty: this.datePretty,
|
|
||||||
path: this.path,
|
|
||||||
dailyLogDirPath: this.dailyLogDirPath,
|
dailyLogDirPath: this.dailyLogDirPath,
|
||||||
fullPath: this.fullPath,
|
fullPath: this.fullPath,
|
||||||
filename: this.filename,
|
filename: this.filename,
|
||||||
@ -41,36 +44,34 @@ class DailyLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
/**
|
||||||
this.id = date.format(new Date(), 'YYYY-MM-DD')
|
* Append all buffered lines to daily log file
|
||||||
this.datePretty = date.format(new Date(), 'ddd, MMM D YYYY')
|
*/
|
||||||
|
appendBufferedLogs() {
|
||||||
this.dailyLogDirPath = data.dailyLogDirPath
|
let buffered = [...this.bufferedLogLines]
|
||||||
|
|
||||||
this.filename = this.id + '.txt'
|
|
||||||
this.path = Path.join('backups', this.filename)
|
|
||||||
this.fullPath = Path.join(this.dailyLogDirPath, this.filename)
|
|
||||||
|
|
||||||
this.createdAt = Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
async appendBufferedLogs() {
|
|
||||||
var buffered = [...this.bufferedLogLines]
|
|
||||||
this.bufferedLogLines = []
|
this.bufferedLogLines = []
|
||||||
|
|
||||||
var oneBigLog = ''
|
let oneBigLog = ''
|
||||||
buffered.forEach((logLine) => {
|
buffered.forEach((logLine) => {
|
||||||
oneBigLog += logLine
|
oneBigLog += logLine
|
||||||
})
|
})
|
||||||
this.appendLogLine(oneBigLog)
|
return this.appendLogLine(oneBigLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
async appendLog(logObj) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../managers/LogManager').LogObject} logObj
|
||||||
|
*/
|
||||||
|
appendLog(logObj) {
|
||||||
this.logs.push(logObj)
|
this.logs.push(logObj)
|
||||||
var line = JSON.stringify(logObj) + '\n'
|
return this.appendLogLine(JSON.stringify(logObj) + '\n')
|
||||||
this.appendLogLine(line)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append log to daily log file
|
||||||
|
*
|
||||||
|
* @param {string} line
|
||||||
|
*/
|
||||||
async appendLogLine(line) {
|
async appendLogLine(line) {
|
||||||
if (this.locked) {
|
if (this.locked) {
|
||||||
this.bufferedLogLines.push(line)
|
this.bufferedLogLines.push(line)
|
||||||
@ -84,24 +85,29 @@ class DailyLog {
|
|||||||
|
|
||||||
this.locked = false
|
this.locked = false
|
||||||
if (this.bufferedLogLines.length) {
|
if (this.bufferedLogLines.length) {
|
||||||
this.appendBufferedLogs()
|
await this.appendBufferedLogs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all logs from file
|
||||||
|
* Parses lines and re-saves the file if bad lines are removed
|
||||||
|
*/
|
||||||
async loadLogs() {
|
async loadLogs() {
|
||||||
var exists = await fs.pathExists(this.fullPath)
|
if (!await fs.pathExists(this.fullPath)) {
|
||||||
if (!exists) {
|
|
||||||
console.error('Daily log does not exist')
|
console.error('Daily log does not exist')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = await readTextFile(this.fullPath)
|
const text = await fileUtils.readTextFile(this.fullPath)
|
||||||
|
|
||||||
var hasFailures = false
|
let hasFailures = false
|
||||||
|
|
||||||
var logLines = text.split(/\r?\n/)
|
let logLines = text.split(/\r?\n/)
|
||||||
// remove last log if empty
|
// remove last log if empty
|
||||||
if (logLines.length && !logLines[logLines.length - 1]) logLines = logLines.slice(0, -1)
|
if (logLines.length && !logLines[logLines.length - 1]) logLines = logLines.slice(0, -1)
|
||||||
|
|
||||||
|
// JSON parse log lines
|
||||||
this.logs = logLines.map(t => {
|
this.logs = logLines.map(t => {
|
||||||
if (!t) {
|
if (!t) {
|
||||||
hasFailures = true
|
hasFailures = true
|
||||||
@ -118,7 +124,7 @@ class DailyLog {
|
|||||||
|
|
||||||
// Rewrite log file to remove errors
|
// Rewrite log file to remove errors
|
||||||
if (hasFailures) {
|
if (hasFailures) {
|
||||||
var newLogLines = this.logs.map(l => JSON.stringify(l)).join('\n') + '\n'
|
const newLogLines = this.logs.map(l => JSON.stringify(l)).join('\n') + '\n'
|
||||||
await fs.writeFile(this.fullPath, newLogLines)
|
await fs.writeFile(this.fullPath, newLogLines)
|
||||||
console.log('Re-Saved log file to remove bad lines')
|
console.log('Re-Saved log file to remove bad lines')
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,13 @@ class Podcast {
|
|||||||
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
|
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
|
||||||
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
||||||
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
|
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
|
||||||
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3
|
|
||||||
|
// Default is 3 but 0 is allowed
|
||||||
|
if (typeof podcast.maxNewEpisodesToDownload !== 'number') {
|
||||||
|
this.maxNewEpisodesToDownload = 3
|
||||||
|
} else {
|
||||||
|
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
@ -10,6 +10,7 @@ class LibrarySettings {
|
|||||||
this.audiobooksOnly = false
|
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.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
||||||
|
this.podcastSearchRegion = 'us'
|
||||||
|
|
||||||
if (settings) {
|
if (settings) {
|
||||||
this.construct(settings)
|
this.construct(settings)
|
||||||
@ -30,6 +31,7 @@ class LibrarySettings {
|
|||||||
// Added in v2.4.5
|
// Added in v2.4.5
|
||||||
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
||||||
}
|
}
|
||||||
|
this.podcastSearchRegion = settings.podcastSearchRegion || 'us'
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -41,7 +43,8 @@ class LibrarySettings {
|
|||||||
autoScanCronExpression: this.autoScanCronExpression,
|
autoScanCronExpression: this.autoScanCronExpression,
|
||||||
audiobooksOnly: this.audiobooksOnly,
|
audiobooksOnly: this.audiobooksOnly,
|
||||||
hideSingleBookSeries: this.hideSingleBookSeries,
|
hideSingleBookSeries: this.hideSingleBookSeries,
|
||||||
metadataPrecedence: [...this.metadataPrecedence]
|
metadataPrecedence: [...this.metadataPrecedence],
|
||||||
|
podcastSearchRegion: this.podcastSearchRegion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ class ServerSettings {
|
|||||||
this.version = settings.version || null
|
this.version = settings.version || null
|
||||||
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
|
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
|
||||||
|
|
||||||
this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.7.3
|
this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0
|
||||||
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
|
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
|
||||||
|
|
||||||
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null
|
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null
|
||||||
|
@ -117,7 +117,8 @@ class User {
|
|||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsSelected: [...this.itemTagsSelected]
|
itemTagsSelected: [...this.itemTagsSelected],
|
||||||
|
hasOpenIDLink: !!this.authOpenIDSub
|
||||||
}
|
}
|
||||||
if (minimal) {
|
if (minimal) {
|
||||||
delete json.mediaProgress
|
delete json.mediaProgress
|
||||||
|
@ -2,15 +2,30 @@ const axios = require('axios')
|
|||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef AuthorSearchObj
|
||||||
|
* @property {string} asin
|
||||||
|
* @property {string} description
|
||||||
|
* @property {string} image
|
||||||
|
* @property {string} name
|
||||||
|
*/
|
||||||
|
|
||||||
class Audnexus {
|
class Audnexus {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = 'https://api.audnex.us'
|
this.baseUrl = 'https://api.audnex.us'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} region
|
||||||
|
* @returns {Promise<{asin:string, name:string}[]>}
|
||||||
|
*/
|
||||||
authorASINsRequest(name, region) {
|
authorASINsRequest(name, region) {
|
||||||
name = encodeURIComponent(name)
|
const searchParams = new URLSearchParams()
|
||||||
const regionQuery = region ? `®ion=${region}` : ''
|
searchParams.set('name', name)
|
||||||
const authorRequestUrl = `${this.baseUrl}/authors?name=${name}${regionQuery}`
|
if (region) searchParams.set('region', region)
|
||||||
|
const authorRequestUrl = `${this.baseUrl}/authors?${searchParams.toString()}`
|
||||||
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
||||||
return axios.get(authorRequestUrl).then((res) => {
|
return axios.get(authorRequestUrl).then((res) => {
|
||||||
return res.data || []
|
return res.data || []
|
||||||
@ -20,6 +35,12 @@ class Audnexus {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} asin
|
||||||
|
* @param {string} region
|
||||||
|
* @returns {Promise<AuthorSearchObj>}
|
||||||
|
*/
|
||||||
authorRequest(asin, region) {
|
authorRequest(asin, region) {
|
||||||
asin = encodeURIComponent(asin)
|
asin = encodeURIComponent(asin)
|
||||||
const regionQuery = region ? `?region=${region}` : ''
|
const regionQuery = region ? `?region=${region}` : ''
|
||||||
@ -33,6 +54,12 @@ class Audnexus {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} asin
|
||||||
|
* @param {string} region
|
||||||
|
* @returns {Promise<AuthorSearchObj>}
|
||||||
|
*/
|
||||||
async findAuthorByASIN(asin, region) {
|
async findAuthorByASIN(asin, region) {
|
||||||
const author = await this.authorRequest(asin, region)
|
const author = await this.authorRequest(asin, region)
|
||||||
if (!author) {
|
if (!author) {
|
||||||
@ -46,14 +73,28 @@ class Audnexus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} region
|
||||||
|
* @param {number} maxLevenshtein
|
||||||
|
* @returns {Promise<AuthorSearchObj>}
|
||||||
|
*/
|
||||||
async findAuthorByName(name, region, maxLevenshtein = 3) {
|
async findAuthorByName(name, region, maxLevenshtein = 3) {
|
||||||
Logger.debug(`[Audnexus] Looking up author by name ${name}`)
|
Logger.debug(`[Audnexus] Looking up author by name ${name}`)
|
||||||
const asins = await this.authorASINsRequest(name, region)
|
const authorAsinObjs = await this.authorASINsRequest(name, region)
|
||||||
const matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein)
|
|
||||||
if (!matchingAsin) {
|
let closestMatch = null
|
||||||
|
authorAsinObjs.forEach((authorAsinObj) => {
|
||||||
|
authorAsinObj.levenshteinDistance = levenshteinDistance(authorAsinObj.name, name)
|
||||||
|
if (!closestMatch || closestMatch.levenshteinDistance > authorAsinObj.levenshteinDistance) {
|
||||||
|
closestMatch = authorAsinObj
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!closestMatch || closestMatch.levenshteinDistance > maxLevenshtein) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const author = await this.authorRequest(matchingAsin.asin)
|
const author = await this.authorRequest(closestMatch.asin)
|
||||||
if (!author) {
|
if (!author) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
93
server/providers/CustomProviderAdapter.js
Normal file
93
server/providers/CustomProviderAdapter.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
const Database = require('../Database')
|
||||||
|
const axios = require('axios')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class CustomProviderAdapter {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} providerSlug
|
||||||
|
* @param {string} mediaType
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async search(title, author, providerSlug, mediaType) {
|
||||||
|
const providerId = providerSlug.split('custom-')[1]
|
||||||
|
const provider = await Database.customMetadataProviderModel.findByPk(providerId)
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error("Custom provider not found for the given id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup query params
|
||||||
|
const queryObj = {
|
||||||
|
mediaType,
|
||||||
|
query: title
|
||||||
|
}
|
||||||
|
if (author) {
|
||||||
|
queryObj.author = author
|
||||||
|
}
|
||||||
|
const queryString = (new URLSearchParams(queryObj)).toString()
|
||||||
|
|
||||||
|
// Setup headers
|
||||||
|
const axiosOptions = {}
|
||||||
|
if (provider.authHeaderValue) {
|
||||||
|
axiosOptions.headers = {
|
||||||
|
'Authorization': provider.authHeaderValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = await axios.get(`${provider.url}/search?${queryString}}`, axiosOptions).then((res) => {
|
||||||
|
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
||||||
|
return res.data.matches
|
||||||
|
}).catch(error => {
|
||||||
|
Logger.error('[CustomMetadataProvider] Search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
throw new Error("Custom provider returned malformed response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-map keys to throw out
|
||||||
|
return matches.map(({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
author,
|
||||||
|
narrator,
|
||||||
|
publisher,
|
||||||
|
publishedYear,
|
||||||
|
description,
|
||||||
|
cover,
|
||||||
|
isbn,
|
||||||
|
asin,
|
||||||
|
genres,
|
||||||
|
tags,
|
||||||
|
series,
|
||||||
|
language,
|
||||||
|
duration
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
author,
|
||||||
|
narrator,
|
||||||
|
publisher,
|
||||||
|
publishedYear,
|
||||||
|
description,
|
||||||
|
cover,
|
||||||
|
isbn,
|
||||||
|
asin,
|
||||||
|
genres,
|
||||||
|
tags: tags?.join(',') || null,
|
||||||
|
series: series?.length ? series : null,
|
||||||
|
language,
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CustomProviderAdapter
|
@ -2,16 +2,46 @@ const axios = require('axios')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef iTunesSearchParams
|
||||||
|
* @property {string} term
|
||||||
|
* @property {string} country
|
||||||
|
* @property {string} media
|
||||||
|
* @property {string} entity
|
||||||
|
* @property {number} limit
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef iTunesPodcastSearchResult
|
||||||
|
* @property {string} id
|
||||||
|
* @property {string} artistId
|
||||||
|
* @property {string} title
|
||||||
|
* @property {string} artistName
|
||||||
|
* @property {string} description
|
||||||
|
* @property {string} descriptionPlain
|
||||||
|
* @property {string} releaseDate
|
||||||
|
* @property {string[]} genres
|
||||||
|
* @property {string} cover
|
||||||
|
* @property {string} feedUrl
|
||||||
|
* @property {string} pageUrl
|
||||||
|
* @property {boolean} explicit
|
||||||
|
*/
|
||||||
|
|
||||||
class iTunes {
|
class iTunes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
// https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
|
/**
|
||||||
|
* @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
|
||||||
|
*
|
||||||
|
* @param {iTunesSearchParams} options
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
search(options) {
|
search(options) {
|
||||||
if (!options.term) {
|
if (!options.term) {
|
||||||
Logger.error('[iTunes] Invalid search options - no term')
|
Logger.error('[iTunes] Invalid search options - no term')
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
var query = {
|
const query = {
|
||||||
term: options.term,
|
term: options.term,
|
||||||
media: options.media,
|
media: options.media,
|
||||||
entity: options.entity,
|
entity: options.entity,
|
||||||
@ -82,6 +112,11 @@ class iTunes {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} data
|
||||||
|
* @returns {iTunesPodcastSearchResult}
|
||||||
|
*/
|
||||||
cleanPodcast(data) {
|
cleanPodcast(data) {
|
||||||
return {
|
return {
|
||||||
id: data.collectionId,
|
id: data.collectionId,
|
||||||
@ -100,6 +135,12 @@ class iTunes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} term
|
||||||
|
* @param {{country:string}} options
|
||||||
|
* @returns {Promise<iTunesPodcastSearchResult[]>}
|
||||||
|
*/
|
||||||
searchPodcasts(term, options = {}) {
|
searchPodcasts(term, options = {}) {
|
||||||
return this.search({ term, entity: 'podcast', media: 'podcast', ...options }).then((results) => {
|
return this.search({ term, entity: 'podcast', media: 'podcast', ...options }).then((results) => {
|
||||||
return results.map(this.cleanPodcast.bind(this))
|
return results.map(this.cleanPodcast.bind(this))
|
||||||
|
@ -28,6 +28,7 @@ const SearchController = require('../controllers/SearchController')
|
|||||||
const CacheController = require('../controllers/CacheController')
|
const CacheController = require('../controllers/CacheController')
|
||||||
const ToolsController = require('../controllers/ToolsController')
|
const ToolsController = require('../controllers/ToolsController')
|
||||||
const RSSFeedController = require('../controllers/RSSFeedController')
|
const RSSFeedController = require('../controllers/RSSFeedController')
|
||||||
|
const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController')
|
||||||
const MiscController = require('../controllers/MiscController')
|
const MiscController = require('../controllers/MiscController')
|
||||||
|
|
||||||
const Author = require('../objects/entities/Author')
|
const Author = require('../objects/entities/Author')
|
||||||
@ -129,7 +130,7 @@ class ApiRouter {
|
|||||||
this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this))
|
this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this))
|
||||||
this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this))
|
this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this))
|
||||||
this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this))
|
this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this))
|
||||||
|
this.router.patch('/users/:id/openid-unlink', UserController.middleware.bind(this), UserController.unlinkFromOpenID.bind(this))
|
||||||
this.router.get('/users/:id/listening-sessions', UserController.middleware.bind(this), UserController.getListeningSessions.bind(this))
|
this.router.get('/users/:id/listening-sessions', UserController.middleware.bind(this), UserController.getListeningSessions.bind(this))
|
||||||
this.router.get('/users/:id/listening-stats', UserController.middleware.bind(this), UserController.getListeningStats.bind(this))
|
this.router.get('/users/:id/listening-stats', UserController.middleware.bind(this), UserController.getListeningStats.bind(this))
|
||||||
|
|
||||||
@ -299,6 +300,14 @@ class ApiRouter {
|
|||||||
this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this))
|
this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this))
|
||||||
this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this))
|
this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this))
|
||||||
|
|
||||||
|
//
|
||||||
|
// Custom Metadata Provider routes
|
||||||
|
//
|
||||||
|
this.router.get('/custom-metadata-providers', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.getAll.bind(this))
|
||||||
|
this.router.post('/custom-metadata-providers', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.create.bind(this))
|
||||||
|
this.router.delete('/custom-metadata-providers/:id', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.delete.bind(this))
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Misc Routes
|
// Misc Routes
|
||||||
//
|
//
|
||||||
@ -318,6 +327,7 @@ class ApiRouter {
|
|||||||
this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this))
|
this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this))
|
||||||
this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this))
|
this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this))
|
||||||
this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this))
|
this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this))
|
||||||
|
this.router.get('/logger-data', MiscController.getLoggerData.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user